Ich hatte nicht gedacht, dass ich bei diesem Projekt bereits auf ein derart großes Hindernis geraten würde. Ich kann mir eigentlich kein simpleres Spiel vorstellen, das erraten einer Zahl, die der Computer generiert hat.
Die meiste Zeit ist hier natürlich nicht in die Spiellogik geflossen,
sondern ist an einer anderen unscheinbaren Stelle nötig geworden.
sondern ist an einer anderen unscheinbaren Stelle nötig geworden.
Projekt Basis
Dieses Projekt habe ich auf das Windows Stars Screensaver aufgebaut.
Neue Klassen und Funktionen
Update Controller Response
Eine kleine Helfermethode um ControllerResponses in ein ControllerresponseType zu verwandeln, damit immer auf die gleiche Weise darauf zugegriffen werden kann, ohne alle Varianten einer ControllerResponse zu beachten.
export function update_controller_response<T extends ControllerRouteResponseType>(base: T, response: ControllerRouteResponse): T {
if (response === null) {
return base;
}
if (is_view_interface(response)) {
base.view = response;
return base;
}
if (is_event_controller_interface(response)) {
base.controller = response;
return base;
}
if (is_controller_event(response)) {
if (base.events === undefined) base.events = [];
base.events.push(response);
return base;
}
base.controller = response.controller;
base.view = response.view;
if (response.events !== undefined) {
if (base.events === undefined) base.events = [];
base.events.push(...response.events);
}
return base;
}PromiseController
Der PromiseController vereinfacht eine Abfolge verschiedener Views, ohne viel Mehraufwand. Den Namen muss ich vermutlich nochmal überdenken.
export class PromiseController implements ControllerRouteResponseType {
protected next: PromiseController | null = null;
protected resolver: PromiseResolver;
protected cached_response: ControllerRouteResponseType | null = null;
public constructor(resolver: PromiseResolver | PromisableControllerRouteResponseType) {
this.resolver = this.create_resolver_function(resolver);
}
protected create_resolver_function(resolver: PromiseResolver | PromisableControllerRouteResponseType): PromiseResolver {
return () => {
const response: PromisableControllerRouteResponseType = (typeof resolver === "object") ? resolver : resolver();
response.controller.next = this.create_controller_next_function(response);
return response;
};
}
protected create_controller_next_function(response: PromisableControllerRouteResponseType) {
return () => {
if (this.next === null) return this;
this.resolver = this.next.resolver;
this.cached_response = null;
this.next = this.next.next;
return this;
};
}
protected get response(): ControllerRouteResponseType {
if (this.cached_response) return this.cached_response;
const response: (ControllerRouteResponse) = this.resolver();
this.cached_response = {};
update_controller_response(this.cached_response, response);
return this.cached_response;
}
public get view(): ViewInterface | null | undefined {
return this.response?.view;
}
public get controller(): ControllerInterface | null | undefined {
return this.response?.controller;
}
public get events(): Array<ControllerEvent> | undefined {
return this.response?.events;
}
public then(resolve: PromiseResolver | PromisableControllerRouteResponseType | PromiseController): PromiseController {
if (this.next) {
this.next.then(resolve);
} else {
if (resolve instanceof PromiseController) {
this.next = resolve;
} else {
this.next = new PromiseController(resolve);
}
}
return this;
}
public finaly(resolve: NonPromiseResolver | ControllerRouteResponseType): PromiseController {
if (this.next) {
this.next.finaly(resolve);
} else {
this.next = new FinalPromiseController(resolve);
}
return this;
}
}
export type PromisableControllerRouteResponseType = ControllerRouteResponseType & { controller: PromisableController };
export type PromisableControllerRouteResponse = null | ViewInterface | (EventControllerInterface & PromisableController) | ControllerEvent | PromisableControllerRouteResponseType;
type PromiseResolver = () => PromisableControllerRouteResponseType;
type NonPromiseResolver = () => (ControllerRouteResponseType);Controller die in einem Promise verwendet werden sollen müssen das PromisableController Interface implementiert. Es liegt nahe, dass diese auch EventController sind, ist aber nicht notwendig.
Notwendig hingegen ist allerdings, das die im Interface deklarierte "next" Methode innerhalb des Controllers aufgerufen wird. Mit diesem Aufruf wird der nächste Abschnitt des PromiseControllers ausgespielt.
export interface PromisableController {
next: null | (() => ControllerRouteResponseType);
}Darüber hinaus gilt es zu beachten das ein PromiseController mit einem "finaly" endet, indem auch nicht PromisableController verwendet werden können.
Herangehensweise
Ich wollte mich zuerst um die Texterklärungen zu beginn des Spieles kümmern. Es sollten zuerst die Regeln erklärt und schließlich eine Runde des Spiels gestartet werden.
Einleitungstexte darstellen
Ich habe also mehre verschiedene Texte, bevor ich zum eigentlichen Spiel gelange, also habe ich zuerst eine TextView erstellt, die nur etwas Text ausgeben kann.
export class TextView extends CanvasView<ViewCollection> {
public bg_color = new ChainProperty<this, RgbColor>(this, tools.commons.Colors.BLACK);
public fg_color = new ChainProperty<this, RgbColor>(this, tools.commons.Colors.WHITE);
/// The Text on the screen
public text = new ChainProperty<this, Array<string> | string>(this, "");
public draw(): void {
this.reset_canvas_state();
const text = this.text.get();
if (text instanceof Array) {
this.context.textAlign = "left";
const lines = text.length;
text.forEach((line, index) => {
this.context.fillText(line, 50, 300 - lines * 15 + index * 30);
});
} else {
this.context.textAlign = "center";
this.context.fillText(text, 400, 300);
}
}
/**
* Reset default canvas state and paint the background
*/
protected reset_canvas_state() {
super.reset_canvas_state();
this.context.fillStyle = this.bg_color.get().to_hex();
this.context.fillRect(0, 0, 800, 600);
this.context.imageSmoothingEnabled = false;
this.context.fillStyle = this.fg_color.get().to_hex();
this.context.strokeStyle = this.fg_color.get().to_hex();
this.context.font = "16px monospace";
}
}Nun müsste ich nur noch in einem EventController auf die Eingabetaste horchen lassen, um den nächsten Text anzeigen zu lassen.
Aber wo definiere ich den neuen Text?
Es schien fast, als müsste ich für jeden Text einen eigenen EventController schreiben, der zum nächsten Text mit seinem eigenem EventController führt.
export class GameController extends BaseController {
/**
* Start a new game
*/
public new_game() {
return {
controller: this.controllers.for_event.text_rules_controller,
view: this.views.text.reset().text.set([
"In diesem Spiel musst du die Zahl,",
"die der Computer sich ausdenkt erraten.",
"Nach jedem Versuch gibt der Computer dir einen Tip,",
"mit dem du die Möglichkeiten eingrenzen kannst.",
]),
};
}
}export class TextRulesEventController extends BaseController implements EventControllerInterface {
public key_pressed(event: KeyboardEvent): ControllerRouteResponse {
if (event.key === "Enter" && this.next) return {
view: this.views.text.text.set("Los gehts => Enter zum Fortfahren"),
controller: this.controllers.for_event.text_next_round
};
return null;
}
}export class TextNextRoundEventController extends BaseController implements EventControllerInterface {
public key_pressed(event: KeyboardEvent): ControllerRouteResponse {
if (event.key === "Enter" && this.next) return {
view: this.views.text.text.set("Los gehts => Enter zum Fortfahren"),
controller: this.controllers.for_event.game
};
return null;
}
}PromiseController
Das schien mir etwas sehr umständlich und excessive für nur einige Seiten Text. Ich wollte eher etwas, in dem ich sofort sagen kann, welcher Text ausgegeben wird in welcher Reihenfolge. Und das alles in einer ControllerResponse. Mein Ziel war etwa folgendes zu erreichen:
public new_game(): PromiseController {
return new PromiseController(() => {
return {
view: this.views.text.text.set("..."),
}
}).then(() => {
return {
view: this.views.text.text.set("..."),
}
}).finaly(() => {
return {
controller: this.controller.game
}
});
}Dazu musste ich also einen PromiseController implementieren,
der in der Lage ist sich den derzeitigen Status zu merken und zum nächsten Status zu wechseln, wenn die Enter Taste gedrückt wurde.
Wann das wechseln geschehen würde, musste natürlich dynamisch bleiben, wenn ich diese Klasse in anderen Projekten verwenden wollte.
Ich entschied mich also dazu, dass der PromiseController sich immer in den aktiven EventController hooken würde, indem es eine "next"-funktion überschreibt. Der EventController muss jetzt nur noch diese Funktion aufrufen, wann immer nötig, um die nächste Response im Promise zu aktivieren.
public key_pressed(event: KeyboardEvent): ControllerRouteResponse {
if (event.key === "Enter" && this.next) return this.next();
return null;
}Das ganze klingt erstmal nicht so kompliziert, ich hatte mich dabei jedoch mehrmals verzettelt und Schwierigkeiten, dafür zu sorgen, dass ich gleichzeitig bei "next" den nächsten Promise then-callback aufrufe und dabei den Rückgabewert des letzten "finaly" Aufruf des Promise auch als ControllerResponse zu erlauben.
Am ende hatte ich mindesten 4 Stunden mit der Promise-Klasse gerungen, bis sie mir einigermaßen gefiel. Nun finde ich zumindest den Auruf und die Funktionalität gut, die interne Umsetzung könnte noch besser sein.
Spiel Logik
Nachdem der PromiseController erledigt war, habe ich die Spiellogik extra kurz gehalten.
Der generierte Zufallszahl befindet sich im GameModel,
wo ich auch mittels der "next_round" Methode eine neue Runde beginnen kann.
export enum Comparison {
IS_BIGGER,
IS_SMALLER,
IS_EQUAL,
}
export class GameModel extends Model<ModelCollection> {
public hidden_number: number = 0;
public tries: number = 0;
public update(delta_seconds: number) {
}
/**
* @param guess
* The players guess
* @return number
* (hidden === guess) => IS_EQUAL
* (hidden > guess) => IS_BIGGER
* (hidden < guess) => IS_SMALLER
*/
public compare_with(guess: number): Comparison {
if (this.hidden_number === guess) return Comparison.IS_EQUAL;
if (this.hidden_number < guess) return Comparison.IS_SMALLER;
return Comparison.IS_BIGGER;
}
/**
* starts a new round
*/
public next_round() {
this.tries = 0;
this.hidden_number = Math.trunc(Math.random() * 10);
}
}Die "compare_with" Funktion wird dann im GameEventController verwendet, um festzustellen, welche View ich als nächstes anzeigen muss.
public player_guess(number: number): ControllerRouteResponse {
switch (this.models.game.compare_with(number)) {
case Comparison.IS_EQUAL:
return this.player_won();
case Comparison.IS_BIGGER:
return this.views.text.text.set("My number is bigger");
case Comparison.IS_SMALLER:
return this.views.text.text.set("My number is smaller");
}
}Fazit
Ich war zwischenzeitlich schon am verzweifeln mit dem Problemen, die ich mit dem PromiseController hatte. Zum Glück hat sich der Knoten letzendlich doch gelegt. Ich denke mit dem PromiseController habe ich ein wirklich hilfreiches Tools geschrieben, das in allen Folgeprojekten Verwendung finden wird.
Die Spiellogik ist hier jetzt wirklich sehr begrenzt.
Es nur Zahlen von 0 - 9 möglich zu raten, da ich für größere Zahlen den Input Puffern müsste, dass könnte man bei Zeiten nachbessern.
Neue Ideen
Neue Projektideee => TextEditor
Comments