Lauter Lösungen
Im letzten Beitrag haben wir die Möglichkeit zur Bewegung der Puzzleteile eingebaut. Dabei kam es aber zum Problem: Die Bewegung ging so schnell vonstatten, dass wir häufig zwei Teile gleichzeitig bewegt haben. Wie können wir das Problem jetzt lösen?
Die Joystickabfrage optimieren
Wir könnten z. B. eine Verzögerung einbauen, so dass der Spieler die Möglichkeit erhält den Joystick ggf. loszulassen. Ich möchte aber einen anderen Ansatz verfolgen, wir lassen einfach nur einen Schritt zur Zeit zu.
Fügt die unten markierten Zeilen zu checkInput hinzu:
!zone checkInput checkInput ldx inputState ;letzte Eingabe ins X-Register jsr joystickInput ;Aktuellen Joystick-Status holen and #%00011111 ;Nur die unteren 5-BIT sind von Interesse eor #%00011111 ;Da Low-Aktiv, umkehren beq .exit ;keine Eingabe, dann ENDE sta inputState ;sonst merken wir uns den angepassten Wert cpx inputState ;hat sich die Eingabe geändert? beq .exit ;wenn, nein -> ENDE
Direkt zu Beginn holen wir uns nun die letzte Eingabe ins X-Register. Nach dem Prüfen der aktuellen Eingabe, kontrollieren wir ganz zum Schluß, ob sich die Eingabe geändert hat. Dazu vergleichen wir X mit der aktuellen Eingabe, falls sie sich nicht geändert hat, springen wir direkt zum Ende der Routine, anderenfalls verarbeiten wir die Eingabe.
Durch diese Änderung müssen wir jetzt den Joystick jedes mal wieder loslassen oder in eine andere Richtung drücken, damit die Routine auf eine Eingabe reagiert.
Wenn ihr das Programm nun startet, könnt ihr die Puzzleteile endlich kontrolliert bewegen.
Das Mischen optimieren
Das nächste Problem ist unsere shuffleTiles-Routine. Dort wurden die Puzzleteile reinzufällig verteilt, dabei kann es spätestens bei größeren Spielfeldern zu unlösbaren Puzzles kommen. Dies müssen wir natürlich verhindern! Also werden wir jetzt das Puzzle aus der gelösten Position per zufälliger virtueller Joystick-Eingaben durcheinanderwürfeln.
Ihr könnt nun die bisherige Funktion shuffleTiles komplett gegen diese ersetzen:
;******************************************************************************* ;*** Die Puzzleteile zufällig verteilen ;******************************************************************************* ;*** Übergabe: - ;******************************************************************************* ;*** Rückgabe: Neue Position der Puzzleteile in der Tabelle tileOrder ;******************************************************************************* ;*** ändert : A, X, Y, SR ;******************************************************************************* !zone shuffleTiles shuffleTiles ldx #$20 ;Anzahl der Schritte, je höher je schwerer .loop txa ;X auf den pha ;Stack .loop1 jsr rndTimer ;Zufallszahl holen and #%00000011 ;wir brauchen nur die beiden unteren BITs tay ;ins Y-Register lda #JOY_FIRE ;nur BIT 4 im Akku setzen .loop2 lsr ;Akku bitweise nach rechts dey ;Y verringern bpl .loop2 ;solange positiv, weiter verschieben ldx freetilePos ;Pos des freien Feldes ins X-Reg. and inputHelper,X ;erlaubte Richtungen prüfen beq .loop1 ;wenn die Richtung verboten ist, nochmal jsr performInput ;sonst, Eingabe verarbeiten pla ;gemerktes X tax ;wieder vom Stack holen dex ;verringern bne .loop ;und ggf. weiter 'verwürfeln' rts ;zurück
Am Anfang legen wir im X-Register die Anzahl der Schritte fest, die wir fürs Verwürfeln nutzen möchten. Je höher ihr diesen Wert setzt, desto länger dauert das Mischen und um so schwieriger wird es. Da wir X gleich für etwas Anderes benötigen, merken wir uns den Schleifenzähler auf dem Stack.
Dann holen wir uns eine Zufallszahl. Da wir nur die vier Hauptrichtungen des Joysticks verarbeiten, brauchen wir auch nur die unteren beiden Bits. Dann kopieren wir die Zufallszahl aus dem Akku ins Y-Register und füllen den Akku mit dem Wert für den Feuerknopf (%00010000). Um nun an unsere Joystick-Richtung zu gelangen, verschieben wir den Akku um die ermittelte Zufallszahl nach rechts. Schaut ggf. nochmal auf die JOY_…-Konstanten, dann werdet ihr sehen, dass die Richtungen rechts vom Feuerknopf liegen.
Sobald wir unsere zufällige Richtung haben, müssen wir natürlich noch prüfen, ob diese überhaupt erlaubt ist. Dazu laden wir die Position des freien Teils ins X-Register und prüfen mit inputHelper, ob die Richtung aktuell erlaubt ist. Falls nicht holen wir uns eine neue Richtung, anderenfalls lassen wir die Eingabe bei performInput verarbeiten.
Am Ende holen wir unseren Schleifenzähler wieder vom Stack ins X-Register, verringern X und wiederholen alles, bis X den Wert 0 annimmt.
Damit alles korrekt funktioniert, müsst ihr noch die Variablen korrekt initialisieren. Dies macht bitte zunächst direkt im Source.
Kontrolliert, ob eure Werte, wiefolgt aussehen:
;*** Wo befindet sich das leere Feld freetilePos !byte $08
;*** Was befindet sich auf dem jeweiligen Feld? ;*** 0-7: Sprite-Nr. = Puzzleteil ;*** $ff: leeres Feld tileOrder ;Feld 1 2 3 ------- !byte $00, $01, $02 ;|1|2|3| ;Feld 4 5 6 ------- !byte $03, $04, $05 ;|4|5|6| ;Feld 7 8 9 ------- !byte $06, $07, $ff ;|7|8|9| ;-------
In freetilePos muss also eine 8 stehen und unter tileOrder müssen alle Puzzleteile aufsteigend gespeichert sein, so wie es bei der korrekten Lösung der Fall ist.
Startet ihr das Programm, dann sollten die Puzzleteile nun lösbar verteilt sein. Da wir performInput verwenden, können wir das Mischen sogar verfolgen, es sieht aber noch nicht besonders schick aus (irgendwie fehlen unsere Sprites). Wir müssen in puzzleMain nur die Zeile jsr shuffleTiles vor jsr loadSprites verschieben, damit werden die Sprites initialisiert und wir sehen jetzt beim Mischen auch die Sprites. Wer möchte kann dies natürlich abschalten, wenn es für den Spieler nicht sichtbar sein soll oder verlangsamen, damit der Spieler das Mischen besser verfolgen kann.
Etwas zufälligere Zufallszahlen 😉
Mir ist bei meinen Tests aufgefallen, dass die Verteilung der Puzzleteile nicht immer sehr vorteilhaft ist. Das liegt evtl. daran, dass wir in shuffleTiles sehr schnell Zufallszahlen ermitteln (vgl. Zufallszahlen in Assembler). Ich möchte daher eine eigene Quelle für die Zufallszahlen verwenden.
Fügt in direkt hinter main die beiden markierten Zeilen ein:
main lda VICRASTERROWPOS ;aktuelle Rasterzeile für rndSeed holen sta rndSeed ;und unter rndSeed speichern jsr showScreen_Title ;Startbildschirm anzeigen jsr puzzleMain ;das Puzzle starten rts ;Zurück zum BASIC
VICRASTERROWPOS ist eine neue Konstante, die ihr oben bei den anderen VIC-Konstanten einfügen solltet.
VICRASTERROWPOS = $d012 ;(18) Aktuelle Rasterzeile lesen oder setzen
Wie ihr seht, hat sie den Wert $d012, ihr könnt also auch direkt lda $d012 verwenden, falls ihr auf die Konstante verzichten wollt.
Unsere Quelle merken wir uns bei rndSeed, für die spätere Verwendung. Dafür benötigen wir eine Variable für ein Byte, fügt diese am Besten hinter calc16Bit ein.
;*** Basis für die Zufallszahlen rndSeed !byte $00
Jetzt müssen wir noch rndTimer anpassen. Fügt dort gleich vor dem rts diese zwei Anweisung ein:
eor rndSeed ;noch mit unserem Quellwert verknüpfen sta rndSeed ;und Rückgabe als neue Quelle speichern
Wir verknüpfen unsere Zufallszahl noch mit unserer Quelle und speichern die eben ermittelte Zufallszahl als neue Quelle.
Prüfen, ob das Puzzle gelöst wurde
Ein Problem haben wir allerdings immer noch, wir müssen verhindern, dass beim Mischen zufällig ein bereits gelöstest Puzzle herauskommt. Wir benötigen aber sowieso noch eine Routine, die das prüft. Lasst uns doch direkt die neue Funktion checkPuzzle, hinter puzzleMain einfügen.
;******************************************************************************* ;*** Prüfen, ob das Puzzle gelöst wurde ;******************************************************************************* ;*** Übergabe: - ;******************************************************************************* ;*** Rückgabe: A = 0 (NICHT gelöst) ;*** A = 1 (gelöst) ;******************************************************************************* ;*** ändert : A, X, SR ;******************************************************************************* !zone checkPuzzle checkpPuzzle ldx tileOrder+8 ;steht das leere Feld rechts unten? bpl .exit ;falls nein zum Exit springen ldx #$07 ;sonst die restlichen Felder prüfen .loop txa ;X für Vergleich in den Akku cmp tileOrder,X ;Akku mit Puzzle vergleichen bne .exit ;wenn NICHT identisch zum EXIT dex ;nächstes Puzzleteil bne .loop ;prüfen, solange > 0 lda #$01 ;hier ist alles OK, also 1 in den Akku rts ;zurück .exit lda #$00 ;nicht gelöst, 0 in den Akku rts ;zurück
Als Erstes prüfen wir, ob das freie Feld rechts unten steht, nur dann müssen wir überhaupt weiter testen. Wenn das freie Feld sich an der richtigen Position befindet, kontrollieren wir, ob alle anderen Felder aufsteigend sortiert sind. Dazu holen wir uns die Anzahl der restlichen Felder (#$07, da 0 basierend) ins X-Register. Wir kopieren für die Prüfung X in den Akku und vergleichen jetzt ganz einfach, ob an der über X angegebenen Position in tileOrder der selbe Wert wie im X-Register (bzw. Akku) steht. Sind die Werte unterschiedlich, brechen wir ab, sonst verringern wir X und prüfen weiter rückwärts bis X = 0 ist. Das letzte Puzzleteil brauchen wir natürlich nicht mehr zu prüfen, wenn das leere Feld korrekt platziert ist und alle anderen Teile auch stimmen, muss zwangläufig das letzte Puzzleteil auch an der richtigen Position stehen.
Ist das Puzzle gelöst, gibt die Funktion eine 1 im Akku zurück, sonst eine 0.
Damit wir verhindern, dass unser Mischen dem Spieler ein gelöstest Puzzle präsentiert, bauen wir die neue Funktion checkPuzzle dort gleich mal ein.
Fügt also zu shuffleTiles direkt vor dem rts am Ende, noch die folgenden beiden Zeilen ein:
jsr checkPuzzle ;prüfen ob das Puzzle zufällig gelöst wurde bne shuffleTiles ;falls gelöst, nochmal von vorne
Jetzt starten wir die Routine einfach noch mal neu, falls das Puzzle gleich gelöst ist.
Wer möchte, kann die Prüfung, ob das Puzzle gelöst wurde, ja noch in checkInput einbauen. Ändern wir doch die Rahmenfarbe, sobald das Puzzle fertig ist. Schreibt einfach folgende Zeilen hinter das jsr performInput in checkInput.
jsr checkPuzzle ;nach jedem Zug prüfen, ob es gelöst wurde beq .exit ;steht eine 0 im Akku, dann nicht gelöst dec VICBORDERCOLOR ;sonst sind wir fertig und verändern erstmal die Rahmenfarbe
Natürlich ist das nur eine kleine optische Anzeige. Das Spiel läuft erstmal einfach weiter.
Damit sind wir am Ende angelangt. Das Puzzle schreitet langsam voran, aber ein paar Sachen fehlen natürlich noch. Wir sollten den Spieler durch eine Zugbeschränkung oder einen Timer etwas unter Druck setzen. Wenn wir nett sind, könnten wir z. B. noch Punkte fürs Lösen des Puzzles vergeben und wir müssen natürlich auch ein Scheitern des Spielers behandeln.
Schritt 8 - Puzzle 008