Restore the Unused Memory Game - pret/pokecrystal GitHub Wiki
Crystal Version had hidden within its code an unused memory card game that wasn't quite finished. Luckily it is close enough to finished that we can fix it up and use it in our games!
Contents
1. Make the Game Playable
1.1. Make the Game Accessible
The first step is to give the developer (and eventually the player) a way to access the game. Let's add it into the Goldenrod Game Corner for now.
Edit maps/GoldenrodGameCorner.asm:
...
GoldenrodGameCornerCardFlipMachineScript:
...
end
+GoldenrodGameCornerMemoryGameScript:
+ reanchormap
+ special UnusedMemoryGame
+ closetext
+ end
GoldenrodGameCornerPrizeVendorIntroText:
text "Welcome!"
...
GoldenrodGameCorner_MapEvents:
...
def_coord_events
def_bg_events
+ bg_event 0, 11, BGEVENT_READ, GoldenrodGameCornerMemoryGameScript
bg_event 6, 6, BGEVENT_READ, GoldenrodGameCornerSlotsMachineScript
...
Now you can access the minigame by interacting with this plant here! Aweso- ... Oh, dear.
1.2. Make the Game Work
Well, it looks like we have more work to do. There's a mistake in the routine responsible for setting up the game.
Edit engine/games/memory_game.asm:
...
MemoryGame_SampleTilePlacement:
...
jr nz, .loop
ld [hl], c
- dec b
+ dec c
jr nz, .loop
pop hl
inc hl
ret
...
Ah, that's better. The game is now playable.
1.3. Make the Game Quittable
But there's no way to exit. The player is stuck playing forever. A routine exists to ask the player if they'd like to continue, but the code for it is missing and just restarts the game instead. Let's fix that. We can make use of some of the strings from the other Game Corner games to help us.
Edit engine/games/memory_game.asm:
...
.AskPlayAgain:
- call UnusedCursor_InterpretJoypad_AnimateCursor
+ ld hl, .SlotsPlayAgainText
+ call PrintText
+ call YesNoBox
jr nc, .restart
ld hl, wJumptableIndex
set JUMPTABLE_EXIT_F, [hl]
ret
.restart
xor a
ld [wJumptableIndex], a
ret
+
+.SlotsPlayAgainText
+ text_far _SlotsPlayAgainText
+ text_end
...
And now the player can exit after they run out of tries! Technically the minigame is ready to be added to your game without causing any issues. But it looks terrible and doesn't play very well.
1.4. (OPTIONAL) Make the Game Cost Coins
You might want to charge the player Game Corner coins to play the game. We'll borrow some code from the Card Flip game to do this.
...
.RestartGame:
+ ld hl, .CardFlipPlayWithThreeCoinsText
+ call PrintText
+ call CardFlip_PrintCoinBalance
+ call YesNoBox
+ jr c, .NotPlaying
+ call .DeductCoins
+ jr c, .NotPlaying
call MemoryGame_InitStrings
ld hl, wJumptableIndex
inc [hl]
ret
+
+.NotPlaying:
+ ld hl, wJumptableIndex
+ set JUMPTABLE_EXIT_F, [hl]
+ ret
+
+.CardFlipPlayWithThreeCoinsText:
+ text_far _CardFlipPlayWithThreeCoinsText
+ text_end
+
+.DeductCoins:
+ ld a, [wCoins]
+ ld h, a
+ ld a, [wCoins + 1]
+ ld l, a
+ ld a, h
+ and a
+ jr nz, .deduct ; You have at least 256 coins.
+ ld a, l
+ cp 3
+ jr nc, .deduct ; You have at least 3 coins.
+ ld hl, .CardFlipNotEnoughCoinsText
+ call PrintText
+ scf
+ ret
+
+.deduct
+ ld de, -3
+ add hl, de
+ ld a, h
+ ld [wCoins], a
+ ld a, l
+ ld [wCoins + 1], a
+ ld de, SFX_TRANSACTION
+ call PlaySFX
+ call CardFlip_PrintCoinBalance
+ call WaitSFX
+ xor a
+ ret
+
+.CardFlipNotEnoughCoinsText:
+ text_far _CardFlipNotEnoughCoinsText
+ text_end
.ResetBoard
...
.finish_round
call WaitPressAorB_BlinkCursor
- ld hl, wJumptableIndex
- inc [hl]
.AskPlayAgain:
- ld hl, .SlotsPlayAgainText ; These lines are only here if you did the Make the game quittable step
- call PrintText ; The important thing is just to have nothing but the
- call YesNoBox ; lines below
- jr nc, .restart
- ld hl, wJumptableIndex
- set JUMPTABLE_EXIT_F, [hl]
- ret
-
-.restart
xor a
ld [wJumptableIndex], a
ret
-
-.SlotsPlayAgainText
- text_far _SlotsPlayAgainText
- text_end
...
2. Cleaning Things Up
Now we have a working game, let's make it look pretty.
2.1. Untranslated Text
The most obvious thing upon opening the game is the glitched text in the top corners. The text in the top left indicates that any cards that appear next to it are matches you've made. This is pretty obvious and probably doesn't need a label. The text in the top right is a counter for how many tries you have left. There's also that awkward empty text box at the bottom. Let's fix this up.
Again edit engine/games/memory_game.asm:
...
.CheckTriesRemaining:
- ld a, [wMemoryGameNumberTriesRemaining]
- hlcoord 17, 0
- add '0'
- ld [hl], a
ld hl, wMemoryGameNumberTriesRemaining
ld a, [hl]
and a
jr nz, .next_try
...
.next_try
+ push hl
+ ld hl, CardFlipChooseACardText
+ call PrintText
+ call MemoryGame_PrintTries
+ pop hl
dec [hl]
...
MemoryGame_CheckMatch:
ld hl, wMemoryGameCard1
ld a, [hli]
cp [hl]
jr nz, .no_match
+ ld hl, .VictoryText
+ call PrintText
+ call MemoryGame_PrintTries
+ call WaitPressAorB_BlinkCursor
...
.find_empty_slot
ld a, [hli]
...
inc [hl]
inc [hl]
ld d, 0
- hlcoord 5, 0
+ hlcoord 0, 0
add hl, de
call MemoryGame_PlaceCard
- ld hl, .VictoryText
- call PrintText
ret
.no_match
xor a
...
ld hl, MemoryGameDarnText
call PrintText
+ call MemoryGame_PrintTries
+ call WaitPressAorB_BlinkCursor
ret
...
MemoryGame_InitStrings:
hlcoord 0, 0
ld bc, SCREEN_AREA
ld a, $1
call ByteFill
- hlcoord 0, 0
- ld de, .japstr1
- call PlaceString
- hlcoord 15, 0
- ld de, .japstr2
- call PlaceString
- ld hl, .dummy_text
- call PrintText
ret
-.dummy_text
- db "@"
-.japstr1
- db "とったもの@"
-.japstr2
- db "あと かい@"
+CardFlipChooseACardText:
+ text_far _CardFlipChooseACardText
+ text_end
+
+MemoryGame_PrintTries:
+ hlcoord 9, 15
+ lb bc, 1, 9
+ call Textbox
+ hlcoord 10, 16
+ ld de, .tries_text
+ call PlaceString
+ hlcoord 17, 16
+ ld de, wMemoryGameNumberTriesRemaining
+ lb bc, PRINTNUM_LEADINGZEROS | 1, 2
+ call PrintNum
+ ret
+
+.tries_text:
+ db "TRIES@"
MemoryGame_Card2Coord:
...
Much better. MemoryGame_PrintTries is intended to look like the "COIN" indicator box from the card flip game.
2.2. Fixing the Cursor
Now it's time to fix that glitchy blob that's supposed to be a cursor. The problem is the memory being used for the graphic isn't graphical data, it's reading the card flip game's code and interpreting it as a graphic, so it's a glitchy mess. We'll bring back the graphic that was used for the game in the Spaceworld demo.
Save this image as gfx/battle_anims/pointer.png
Again edit engine/games/memory_game.asm:
...
MemoryGameLZ:
INCBIN "gfx/memory_game/memory_game.2bpp.lz"
+
+MemoryGameGFX:
+INCBIN "gfx/battle_anims/pointer.2bpp"
Edit engine/games/card_flip.asm:
DEF CARDFLIP_LIGHT_OFF EQU '♂' ; $ef
DEF CARDFLIP_LIGHT_ON EQU '♀' ; $f5
-MemoryGameGFX:
-; Graphics for an unused Game Corner
-; game were meant to be here.
-
UnusedCursor_InterpretJoypad_AnimateCursor:
ret
Now we've got a nice pretty pointer.
2.3. Player can move the cursor at the wrong times
The player moves the cursor to select a card, but there are times when the player shouldn't be able to move it (like after two cards are selected). There is only one time when the player should be able to move it, when they're selecting a card. Luckily, we can deal with this by disallowing cursor movement based on what phase the game is in, which is determined by the jumptable value. The cursor movement logic already partially handles this, but it is incomplete. Let's fix it.
...
MemoryGame_InterpretJoypad_AnimateCursor:
ld a, [wJumptableIndex]
+ cp $3
+ jr c, .quit
+ cp $6
+ ret z
cp $7
jr nc, .quit
call JoyTextDelay
...
This will ensure the cursor does not move if we're in a state when the player cannot select a card.
2.4. Make Text Print Instantly
The other game corner games turn off text scrolling to keep the arcade-y feeling of the games. Let's do the same here.
_MemoryGame:
+ ld hl, wOptions
+ set NO_TEXT_SCROLL, [hl]
call .LoadGFXAndPals
call DelayFrame
.loop
call .JumptableLoop
jr nc, .loop
+ ld hl, wOptions
+ res NO_TEXT_SCROLL, [hl]
ret
3. Improving the Gameplay
We've taken care of the problems with untranslated strings and cleaned up the interface. Now it's time for some quality of life improvements.
Anything in this list is totally optional and it is up to you as a developer if you want to implement it.
3.1. Don't Hide Cards Until the Next Try
This is a minor complaint, but after you fail to find a match, the game hides the two cards and prints "Darn". We can make the game show the cards until the player presses "A" to proceed to the next try instead.
...
.no_match
+ ld hl, MemoryGameDarnText
+ call PrintText
+ call MemoryGame_PrintTries
+ call WaitPressAorB_BlinkCursor
+
xor a
ld [wMemoryGameLastCardPicked], a
ld a, [wMemoryGameCard1Location]
call MemoryGame_Card2Coord
call MemoryGame_PlaceCard
ld a, [wMemoryGameCard2Location]
call MemoryGame_Card2Coord
call MemoryGame_PlaceCard
-
- ld hl, MemoryGameDarnText
- call PrintText
- call MemoryGame_PrintTries
- call WaitPressAorB_BlinkCursor
ret
...
Now we see a bit longer what the cards are. They will be hidden again after the player proceeds.
3.2. Don't Deduct a Try For a Match
Five tries isn't a lot, and there are 45 cards on the board. Let's give the player more time to clear some cards! We'll do this by rewarding good gameplay and not using up a try when a match is found.
...
MemoryGame_CheckMatch:
ld hl, wMemoryGameCard1
ld a, [hli]
cp [hl]
jr nz, .no_match
+ ld hl, wMemoryGameNumberTriesRemaining
+ inc [hl]
ld hl, .VictoryText
call PrintText
...
.find_empty_slot
ld a, [hli]
...
inc [hl]
- inc [hl]
ld d, 0
hlcoord 0, 0
...
Since the player might now be able to make more than five matches (or in fact 10 matches), we'll make it so matched cards layer on top of each other, increasing the space at the top of the screen from 10 matches to 19. This is fine because it looks like the cards are simply lying on top of each other, with the bonus that it makes the matched cards more visually distinct from the board itself.
3.3. Add a "Game Over" Message
Let's let the player know specifically when the game has finished. Let's also remove an unnecessary "A press" required before revealing all the cards.
...
_MemoryGameDarnText::
text "Darn…"
done
+_MemoryGameGameOverText::
+ text "Game over!"
+ done
+
_StartMenuContestEndText::
...
...
.CheckTriesRemaining:
ld hl, wMemoryGameNumberTriesRemaining
ld a, [hl]
and a
jr nz, .next_try
+ ld hl, MemoryGameGameOverText
+ call PrintText
ld a, $7
ld [wJumptableIndex], a
ret
...
.RevealAll:
- ldh a, [hJoypadPressed]
- and PAD_A
- ret z
xor a
ld [wMemoryGameCounter], a
.RevelationLoop:
...
MemoryGameDarnText:
text_far _MemoryGameDarnText
text_end
+MemoryGameGameOverText:
+ text_far _MemoryGameGameOverText
+ text_end
+
MemoryGame_InitBoard:
...
3.4. Add Sounds
Cool, the game works now, but it's so incredibly quiet. The other Game Corner games have all kinds of neat noises and things that they make. Let's add some of our own here! These are completely independent from each other, so you can skip any of them.
3.4.1 Beginning Game, Board Set-Up
First, let's set up a sound for when the board gets constructed. We'll also take out a bit of unnecessary code while we're here (you can remove this whether you're adding the sound or not).
...
.ResetBoard:
- call UnusedCursor_InterpretJoypad_AnimateCursor
- jr nc, .proceed
- ld hl, wJumptableIndex
- set JUMPTABLE_EXIT_F, [hl]
- ret
-
-.proceed
+ ld de, SFX_SLOT_MACHINE_START
+ call PlaySFX
call MemoryGame_InitBoard
ld hl, wJumptableIndex
inc [hl]
...
3.4.2 Moving Cursor
Next we'll set up a little sound for moving the cursor from card to card.
...
MemoryGame_InterpretJoypad_AnimateCursor:
...
.pressed_left
...
add hl, bc
dec [hl]
- ret
+ jr .play_movement_sound
.pressed_right
...
add hl, bc
inc [hl]
- ret
+ jr .play_movement_sound
.pressed_up
...
add hl, bc
ld a, [hl]
sub 9
ld [hl], a
- ret
+ jr .play_movement_sound
.pressed_down
...
add hl, bc
ld a, [hl]
add 9
ld [hl], a
- ret
+ ; fallthrough
+
+.play_movement_sound
+ ld de, SFX_POKEBALLS_PLACED_ON_TABLE
+ call PlaySFX
+ ret
MemoryGameLZ:
...
3.4.3 Selecting a Card
Basically a sound for "clicking" on a card.
...
.PickCard1:
...
ld [wMemoryGameCardChoice], a
+ ld de, SFX_STOP_SLOT
+ call PlaySFX
ld hl, wJumptableIndex
inc [hl]
ret
.PickCard2:
...
ld [wMemoryGameCounter], a
+ ld de, SFX_STOP_SLOT
+ call PlaySFX
ld hl, wJumptableIndex
inc [hl]
.DelayPickAgain:
...
3.4.4 Match or No Match
Play a sound when a match was made, or when a match wasn't made.
MemoryGame_CheckMatch:
ld hl, wMemoryGameCard1
ld a, [hli]
cp [hl]
jr nz, .no_match
ld hl, wMemoryGameNumberTriesRemaining ; These two lines will only be here if you implemented
inc [hl] ; "Don't Deduct a Try For a Match"
ld hl, .VictoryText
call PrintText
call MemoryGame_PrintTries
+ ld de, SFX_3RD_PLACE
+ call PlaySFX
+ call WaitSFX
call WaitPressAorB_BlinkCursor
ld a, [wMemoryGameCard1Location]
...
.no_match
+ ld de, SFX_WRONG
+ call PlaySFX
ld hl, MemoryGameDarnText
call PrintText
...
3.4.5 Leaving the Game
The slot machines and card flip games both play a sad little tune when you quit. Let's add that here as well.
and a
ret
.quit
+ ld de, SFX_QUIT_SLOTS
+ call PlaySFX
+ call WaitSFX
scf
ret
.ExecuteJumptable:
4. Setting Up Actions
Now that the game is working, let's make it do something. There are several symbols that can appear on the cards, and there is a consistent amount of them that can appear on any given board. The number of any given card that appear on a board can be changed but that won't be covered here.
- 7
- 6
- 5
- 4
- 17
- 3
- 2
- 1
4.1. Take Action for a Match
Let's set up some code that will allow us to hook up some sort of reward system. We'll take out the generic "Yeah!" match text here because we'll be replacing it with more specific text later on.
...
MemoryGame_CheckMatch:
ld hl, wMemoryGameCard1
ld a, [hli]
cp [hl]
jr nz, .no_match
ld hl, wMemoryGameNumberTriesRemaining ; These lines are only here if you implemented
inc [hl] ; "Don't Deduct a Try For a Match" above
- ld hl, .VictoryText
- call PrintText
- call MemoryGame_PrintTries
- ld de, SFX_3RD_PLACE
- call PlaySFX ; These lines are only here if
- call WaitSFX ; you implemented the sounds above
- call WaitPressAorB_BlinkCursor
ld a, [wMemoryGameCard1Location]
...
...
.find_empty_slot
...
add hl, de
call MemoryGame_PlaceCard
+ call .HandleMatch
ret
.no_match
...
call MemoryGame_Card2Coord
call MemoryGame_PlaceCard
ret
+; Take action based on what was matched
+.HandleMatch
+ ld a, [wMemoryGameLastCardPicked]
+ cp 1 ; "Medkit"
+ jr nz, .not_medkit
+ ; Fill in action here
+ ret
+.not_medkit
+ cp 2 ; "Candy"
+ jr nz, .not_candy
+ ; Fill in action here
+ ret
+.not_candy
+ cp 3 ; "Clefairy doll"
+ jr nz, .not_pokedoll
+ ; Fill in action here
+ ret
+.not_pokedoll
+ cp 4 ; "Star"
+ jr nz, .not_star
+ ; Fill in action here
+ ret
+.not_star
+ cp 5 ; "Potion/Bottle"
+ jr nz, .not_potion
+ ; Fill in action here
+ ret
+.not_potion
+ cp 6 ; "Pokeball"
+ jr nz, .not_pokeball
+ ; Fill in action here
+ ret
+.not_pokeball
+ cp 7 ; "Superball"
+ ret nz ; The last icon only occurs once so we can't reward a match for it
+ ; Fill in action here
+ ret
.VictoryText:
...
Ok, now we've got our skeleton for what we're going to do. So let's make some stuff happen! Any one of these can be used or combined for different matches. For example, I like to reward extra tries when medkits are matched, nothing for potions, and coins for everything else.
4.1.1. Option 1: Rewarding Extra Tries
Pretty simple, we just increase wMemoryGameNumberTriesRemaining and print a message, and we can play a sound as well. Or, if you're feeling really mean, you can set number of tries to 0 to give an instant game over! Just make sure to use a different string.
...
.HandleMatch
ld a, [wMemoryGameLastCardPicked]
cp 1 ; Medkit
jr nz, .not_medkit
- ; Fill in action here
+ ; Add an extra try
+ ld hl, wMemoryGameNumberTriesRemaining
+ inc [hl]
+ ld hl, .ExtraTryText
+ call PrintText
+ ld de, SFX_2ND_PLACE
+ call PlaySFX
+ call WaitSFX
+ call WaitPressAorB_BlinkCursor
ret
.not_medkit
...
.VictoryText:
...
ret
+.ExtraTryText:
+ text_asm
+ push bc
+ hlcoord 2, 13
+ call MemoryGame_PlaceCard
+ ld hl, MemoryGameExtraTryText
+ pop bc
+ inc bc
+ inc bc
+ inc bc
+ ret
+
+MemoryGameExtraTryText:
+ text_far _MemoryGameExtraTryText
+ text_end
MemoryGameYeahText:
...
...
+_MemoryGameExtraTryText::
+ text " ! An extra"
+ line "try!"
+ done
...
4.1.2. Option 2: Rewarding Game Corner Coins
For this, we'll borrow some code from the Card Flip minigame. We'll use our own new string to indicate coins earned.
...
.not_pokeball
...
ret
+.Payout:
+ ld a, c
+ push bc
+ ld [wStringBuffer2], a
+ ld hl, .VictoryText
+ call PrintText
+ call CardFlip_PrintCoinBalance
+ ld de, SFX_3RD_PLACE
+ call PlaySFX
+ call WaitSFX
+ pop bc
+
+.loop
+ push bc
+ call .IsCoinCaseFull
+ jr c, .full
+ call .AddCoinPlaySFX
+
+.full
+ call CardFlip_PrintCoinBalance
+ ld c, 2
+ call DelayFrames
+ pop bc
+ dec c
+ jr nz, .loop
+ call WaitPressAorB_BlinkCursor
+ ret
+
+.AddCoinPlaySFX:
+ ld a, [wCoins]
+ ld h, a
+ ld a, [wCoins + 1]
+ ld l, a
+ inc hl
+ ld a, h
+ ld [wCoins], a
+ ld a, l
+ ld [wCoins + 1], a
+ ld de, SFX_PAY_DAY
+ call PlaySFX
+ ret
+
+.IsCoinCaseFull:
+ ld a, [wCoins]
+ cp HIGH(MAX_COINS)
+ jr c, .less
+ jr z, .check_low
+ jr .more
+
+.check_low
+ ld a, [wCoins + 1]
+ cp LOW(MAX_COINS)
+ jr c, .less
+
+.more
+ scf
+ ret
+
+.less
+ and a
+ ret
.VictoryText:
text_asm
...
...
_MemoryGameYeahText::
- text " , yeah!"
+ text " ! @"
+ text_decimal wStringBuffer2, 1, 2
+ text " Coin(s)!"
done
...
Then, for any one of the ; Fill in action here above, you can insert a piece of code like this.
.not_pokedoll
cp 4 ; "Star"
jr nz, .not_star
- ; Fill in action here
- ret
+ ; Reward 3 coins
+ ld c, 3
+ jr .Payout
.not_star
4.1.3 Reward Nothing
You may want to have a case where you reward nothing, but if you implemented "Don't Deduct a Try For a Match" above then the match still won't cost a try.
For rewarding nothing, the code we already have will work fine and no additional work is necessary. But you may also want to print a message to the player.
...
.not_star
cp 5 ; Bottle
jr nz, .not_potion
- ; Fill in action here
+ ld hl, .NoPrizeText
+ call PrintText
+ ld de, SFX_BUMP
+ call PlaySFX
+ call WaitSFX
+ call WaitPressAorB_BlinkCursor
ret
.not_potion
...
.VictoryText:
...
ret
+.NoPrizeText:
+ text_asm
+ push bc
+ hlcoord 2, 13
+ call MemoryGame_PlaceCard
+ ld hl, MemoryGameNoPrizeText
+ pop bc
+ inc bc
+ inc bc
+ inc bc
+ ret
+
MemoryGameExtraTryText:
text_far _MemoryGameExtraTryText
text_end
+MemoryGameNoPrizeText:
+ text_far _MemoryGameNoPrizeText
+ text_end
...
+_MemoryGameNoPrizeText::
+ text " ! No prize…"
+ done
...
4.2. Take Immediate Action for a Card
You may want to take immediate action when a player turns over a card, before even determining if a match has been made. For example, the only appears once on the board. If you wanted to do something with it aside from wasting a try, you may do that as well.
In this example, I'm giving an instant game over. But you can do whatever you want, you can even jump to .Payout like above if you want to give coins instead.
...
.PickCard1:
ld a, [wMemoryGameCardChoice]
...
call MemoryGame_Card2Coord
call MemoryGame_PlaceCard
+ ld a, [wMemoryGameLastCardPicked]
+ cp 8
+ jr z, .GameOverCard
xor a
ld [wMemoryGameCardChoice], a
...
.PickCard2:
ld a, [wMemoryGameCardChoice]
...
call MemoryGame_Card2Coord
call MemoryGame_PlaceCard
+ ld a, [wMemoryGameLastCardPicked]
+ cp 8
+ jr z, .GameOverCard
ld a, 30
ld [wMemoryGameCounter], a
...
.PickAgain:
call MemoryGame_CheckMatch
ld a, $3
ld [wJumptableIndex], a
ret
+.GameOverCard:
+ ld de, SFX_WRONG
+ call PlaySFX
+ ld a, 0
+ ld [wMemoryGameNumberTriesRemaining], a
+ ld a, $7
+ ld [wJumptableIndex], a
+ ld hl, GameOverCardText
+ call PrintText
+ ret
.RevealAll:
...
.NoPrizeText:
...
inc bc
inc bc
ret
+
+GameOverCardText:
+ text_asm
+ push bc
+ hlcoord 2, 13
+ call MemoryGame_PlaceCard
+ ld hl, MemoryGameGameOverCardText
+ pop bc
+ inc bc
+ inc bc
+ inc bc
+ ret
...
MemoryGameGameOverText:
text_far _MemoryGameGameOverText
text_end
+MemoryGameGameOverCardText:
+ text_far _MemoryGameGameOverCardText
+ text_end
...
...
+_MemoryGameGameOverCardText::
+ text " ! Game"
+ line "over…"
+ prompt
...