AII2048/src/AII2048.asm

1007 lines
31 KiB
NASM
Raw Permalink Blame History

.export CellTable, GameState
.exportzp RestartLatch, P0ScoreBCD, P1ScoreBCD
.import READ_CHAR, PRBYTE, DisplayBoard, DisplayState, InitScreen, VTAB, DisplayGameOverMsg, DisplayWonMsg
;
; 2048 2600
; =========
;
; A port of the 2048 game to the Atari 2600
;
; (C) 2014 Carlos Duarte do Nascimento (chesterbr)
; <cd@pobox.com | @chesterbr | http://chester.me>
;
; Latest version, contributors and general info:
; http://github.com/chesterbr/2048-2060
;
; Building
; ---------
;
; You can build with DASM (http://dasm-dillon.sourceforge.net/), e.g.:
;
; dasm 2048.asm -o2048.bin -f3
;
; However, you'll likely want to run graphics_gen.rb before. I've included
; my build script (build.sh), under the express condition that you don't
; mock my lame bash abilities :-P
; Cell Tables
; -----------
;
; The game store each player's tiles in a "cell table", which can contain one
; of these values:
;
; 0 = empty cell
; 1 = "2" tile
; 2 = "4" tile
; 3 = "8" tile
; 4 = "16" tile
; ...
; n = "2n" tile (or, if you prefer: log2k = "k" tile)
; ...
; 11 = "2048" tile
; 12 = "4096" tile
; 13 = "8192" tile
; (could go on, but try drawing a 5-digit 8x10 tile :-P )
; 128 ($FF) = sentinel tile (see below)
;
; In theory, we'd use 16 positions in memory for a 4x4 grid. Navigating
; left/right on the grid would mean subtracting/adding one position, and
; moving up/down would be done by subtracting 4 positions (that
; is, "cell table y offset" would be 4)
;
; However, we'd need to do complicated boundaries checking, so instead I
; surround the grid with "sentinel" or "wall" tiles. That would theoretically
; need 20 extra cells (bytes) to store the grid:
;
; first cell -> SSSSSS S = sentinel, . = data (a tile or empty cell)
; 7th cell -> S....S
; 13rd cell -> S....S
; 19th cell -> S....S
; 25th cell -> S....S
; SSSSSS <- last (36th) cell
;
; But we can save some space by removing th left-side sentinels, since the
; memory position before those will be a sentinel anyway (the previous line's
; right-side sentinel).
;
; We can also cut the first and last sentinel (no movement can reach those),
; ending with with this layout in memory (notice how you still hit a
; sentinel if you try to leave the board in any direction):
;
; first cell -> SSSSS S = sentinel, . = data (a tile or empty cell)
; 6th cell -> ....S
; 11th cell -> ....S
; 16th cell -> ....S
; 21st cell -> ....S
; SSSS <- last (29th) cell
;
; Only change from usual 4x4 is the cell table vertical y offset is now 5 (we
; add/subtract 5 to go down/up). The first data cell is still the first cell
; plus vertical y offset
;
;
; Grid Drawing
; ------------
;
; The grid itself will be drawn using the TIA playfield, and the tiles
; with player graphics. The Atari only allows two of those graphics per
; scanline (although they can be repeated up to 3 times by the hardware),
; and we have four tiles per row, meaning we have to trick it[2] by:
;
; - Load the graphic for tiles A and B: "A" and "B"
; - Ask TIA to repeat each player graphic: "A A" and "B B"
; - Overlap their horizontal positions: "A B A B"
; - Load graphic for tiles C and D when the "A B C D"
; TV beam is right halfway, that is, here: --------^
;
; First three steps can be done just once when we start drawing the grid
; (TIA remembers the position), but the fourth must be repeated for every
; scanline. Timing is crucial on third and fourth steps, but that's Atari
; programming for you!
;
; To translate the cell table into a visual grid, we have to calculate, for
; each data cell, where the bitmap for its value (tile or empty space) is
; stored. We use the scanlines between each row of cells to do this calculation,
; meaning we need 8 RAM positions (4 cells per row x 2 bytes per address).
;
; We use the full address instead of a memory page offset to take advantage
; of the "indirect indexed" 6502 addressing mode [1], but we load the
; graphics table at a "page aligned" location (i.e., a "$xx00" address),
; so we only need to update the least significant byte on the positions above.
;
;
; Shifting
; --------
;
; The original 2048 sets a "vector" structure that points two variables with
; the direction in which the tiles will shift (e.g., vector.x = -1, vector.y
; = 0 means we'll move left). It also processes them from the opposite side
; (e.g., start from leftmost if it's a right shift), making the first one
; stop before the edge, the second stop before the first, etc.
;
; It also marks each merged tile as so (removing the marks between shifts),
; so it can block multiple merges (i.e., the row " 4 4 8 16" does not go
; straight to "32" with a left, but first becomes "8 8 16", then "16 16", then
; "32". Similarly, "2 2 2 2" would first become "4 4", then "8". Finally,
; it stores the previous position for each tile, and lets the awesomeness
; of CSS move them all at once with ease.
;
; We'll translate this idea by having a single-byte "vector" which can be
; -1/+1 for left/right, and -5/+5 for up/down (each row is 4 bytes plus a
; sentinel tile, see above). Each tile will be pushed (by adding the vector
; value) until the next cell is non-empty and does not match its value.
;
; The vector signal also tells us where to start to ensure they all get to
; the end: negative (left/up) start processing from the first cell and
; positive (right/down) start from the last.
;
; Merged tiles are marked by setting bit 7 on their values, which will be
; easy to check in upcoming pushed blocks without needing extra memory.
; We'll only miss the animations, but we can't have everything.
;
;
; Timings
; -------
;
; Since the shift routine can have unpredictable timing (and I wanted some
; freedom to move routines between overscan and vertical blank), I decided
; to use RIOT timers instead of the traditional scanline count. It is not
; the usual route for games (as they tend to squeeze every scanline of
; processing), but for this project it worked fine.
;
; [1] http://skilldrick.github.io/easy6502/
; [2] http://www.slideshare.net/chesterbr/atari-2600programming
.org $2000
jmp Initialize
;;;;;;;;;;;;;;;;;
;; DATA TABLES ;;
;;;;;;;;;;;;;;;;;
; Tile and digit graphics go in the beginning of the cart to keep page-aligned
; (that is, the address' MSB never changes and we only calculate the LSB)
; Score values of each tile, in BCD
TileValuesBCD:
.byte $00,$04
.byte $00,$08
.byte $00,$16
.byte $00,$32
.byte $00,$64
.byte $01,$28
.byte $02,$56
.byte $05,$12
.byte $10,$24
.byte $20,$48
.byte $40,$96
.byte $81,$92
; Values that change if we are on PAL mode (TV TYPE switch "B<>W" position)
; Order: NTSC, PAL. (thanks @SvOlli)
VBlankTime64T:
.byte 44,74
OverscanTime64T:
.byte 35,65
;;;;;;;;;;;;;;;
;; CONSTANTS ;;
;;;;;;;;;;;;;;;
; Special cell values (see header)
CellEmpty = 0
Cell2 = 1
Cell4 = 2
Cell2048 = 11
CellSentinel = 127 ; Can't use bit 7 (will be wiped)
MergedMask = %10000000 ; Bit 7 is set to flag tiles as merged
ClearMergedMask = %01111111 ; We need to reset to get to original values
; The original 2048 puts 2s with 90% probability and 4s with 10%. Our random
; number range between 0-255, so we'll put a 4 if it is above 256 * 0.9 ? 230
ThresholdForTile4 = 230
; Values of GameState (it's a state machine!)
TitleScreen = 0 ; => AddingRandomTitle
AddingRandomTile = 1 ; => WaitingJoyRelease
WaitingJoyRelease = 2 ; => WaitingJoyPress
WaitingJoyPress = 3 ; => Shifting
Shifting = 4 ; => ShowingMerged OR WaitingJoyRelease
ShowingMerged = 5 ; => AddingRandomTile OR GameOverFX
GameOverFX = 6 ; => GameOver
GameOver = 7 ; => TitleScreen
; Values of GameMode
OnePlayerGame = 0
TwoPlayerGame = 1
; Some relative positions on the cell table
; Notice how we go to last data cell: Top-Left + 3 rows down + 3 columns right
; (FYI: add another row down and you'd have the last sentinel)
FirstDataCellOffset = 5
LastDataCellOffset = FirstDataCellOffset + (CellTableYOffset * 3) + 3
LastCellOffset = LastDataCellOffset + CellTableYOffset
CellTableYOffset = 5 ; How much we +/- to move up/down a line on the table
; Position of sentinels that might be picked as random tiles (for being the
; ones on the right "wall", see drawing on header), relative to the
; first data cell offset.
; We'll replace them with the last three, allowing our random nibble to
; effectively map a random data cell
Wall1 = 4
Wall2 = 9
Wall3 = 14
Wall1Repl = 16
Wall2Repl = 17
Wall3Repl = 18
ScoreColor = $28 ; Colors were chosen to get equal or equally nice
InactiveScoreColor = $04 ; on both PAL and NTSC, avoiding adjust branches
GridColor = $04
BackgroundColor = $00
TileHeight = 11 ; Tiles have 11 scanlines (and are in graphics.asm)
GridBottomHeight = 5 ; Doesn't count the ones we use calculating P1's score
JoyUp = %11100000 ; Masks to test SWCHA for joystick movement
JoyDown = %11010000 ; (we'll shift P1's bits into P0s on his turn, so
JoyLeft = %10110000 ; it's ok to use P0 values)
JoyRight = %01110000
JoyMask = %11110000
ColSwitchMask = %00001000 ; Mask to test SWCHB for TV TYPE switch
SelectResetMask = %00000011 ; Mask to test SWCHB for GAME SELECT/RESET switches
GameSelect = %00000001 ; Value for GAME SELECT pressed (after mask)
GameReset = %00000010 ; Value for GAME RESET pressed (after mask)
; Amount to add to move to a direciton in the cell grid, in two's complement
RightShiftVector = $01 ; 1
LeftShiftVector = $FF ; -1
DownShiftVector = $05 ; 5
UpShiftVector = $FB ; -5
TurnIndicatorFrames = 60 ; Special display for this # of frames if turn switches
MergeNoise = 4 ; Value of TurnNoise (AUDC0) if a merge happened
ShiftNoise = 1 ; Value of TurnNoise (AUDC0) if no merge happened
;;;;;;;;;
;; RAM ;;
;;;;;;;;;
; Some positions are shared between different coroutines
; (think of them as local variables)
RestartLatch = $81
CellTable = $90 ; 29 ($1D) bytes (16 cells + 13 sentinels)
CellCursor = $aD ; Table offset of the "current" cell on grid setup
; Frame count based RNG, used to add tiles and title screen rainbow
RandomNumber = $aE
; Counter to display "animated" tiles (merged / new)
AnimationCounter = $aF
; Added to the tile address to produce the merge animation
AnimationDelta = $b0
; 6-digit score is stored in BCD (each nibble = 1 digit => 3 bytes)
ScoreBCD = $b1
GameMode = $b4 ; One or Two players
DidMerge = $b5 ; Nonzero if a merge happened on the last move
; $A6-$A7 have different uses in various kernel routines:
TempVar1 = $b6 ; General use variable
TempVar2 = $b7 ; General use variable
LineCounter = $b6 ; Counts lines while drawing the score
TempDigitBmp = $b7 ; Stores intermediate part of 6-digit score
DidShift = $b6 ; True if a shift happened
GameState = $b8;
; Tile shift routine variables
ShiftVector = $b9 ; What to add to get to "next" tile in current direction
TilesLoopDirection = $bA ; +1 for left/up, -1 for right/down
OffsetBeingPushed = $bB ; Position in cell table of the tile being pushed
ShiftEndOffset = $bC ; Position in which we'll stop processing
CurrentValue = $bD ; Value of that tile
; Address of the graphic for for each digit (6x2 bytes)
; or tile (4x2 bytes) currently being drawn
DigitBmpPtr = $c0 ; 6 bytes
TileBmpPtr = $c0 ; 4 bytes (2 wasted)
; Colors of each tile on the current row (4 bytes)
RowTileColor = $cC
; Store each player score separatedly and copy
; from/to ScoreBCD as needed to display, add, etc.
; Note: P1 score will store (and show) the high-score in single-player games
P0ScoreBCD = $d0 ; 3 bytes
P1ScoreBCD = $d3 ; 3 bytes
ScoreBeingDrawn = $d6 ; 0 for P0 or 1 for P1
CurrentPlayer = $d7 ; 0 for P0 or 1 for P1
LastSWCHB = $d8 ; Avoid multiple detection of console switches
TurnIndicatorCounter = $d9 ; Controls the time spent changing player turn
GameOverEffectCounter = $dA ; Controls the time spent on game over effect
CurrentBGColor = $dB ; Ensures invisible score keeps invisible during
; game over "explosion"
DidMerge2048 = $dC ; 0 if no 2048 was reached; 11 (Cell2048) if we did
Party2048Counter= $dD ; 2048 effects counter (and ensures they play only once)
;;;;;;;;;;;;;;;
;; BOOTSTRAP ;;
;;;;;;;;;;;;;;;
Initialize: ; Cleanup routine from macro.h (by Andrew Davie/DASM)
sei
cld
ldx #0
txa
tay
sta RestartLatch
sta P0ScoreBCD
sta P0ScoreBCD+1
sta P0ScoreBCD+2
sta P1ScoreBCD
sta P1ScoreBCD+1
sta P1ScoreBCD+2
jsr InitScreen
;;;;;;;;;;;;;;;
;; TIA SETUP ;;
;;;;;;;;;;;;;;;
; lda #%00000001 ; Playfield (grid) in mirror (symmetrical) mode
; sta CTRLPF
;;;;;;;;;;;;;;;;;;;
;; PROGRAM SETUP ;;
;;;;;;;;;;;;;;;;;;;
; Pre-fill the graphic pointers' MSBs, so we only have to
; figure out the LSBs for each tile or digit
; lda #>Tiles ; MSB of tiles/digits page
; ldx #11 ; 12-byte table (6 digits), zero-based
;FillMsbLoop:
; sta TileBmpPtr,x
; dex ; Skip to the next MSB
; dex
; bpl FillMsbLoop
; Initialize the cell table with sentinels, then fill
; the interior with empty cells
ldx #LastCellOffset
lda #CellSentinel
InitCellTableLoop1:
sta CellTable,x
dex
bpl InitCellTableLoop1
;;;;;;;;;;;;;;;;;;
;; TITLE SCREEN ;;
;;;;;;;;;;;;;;;;;;
ShowTitleScreen:
lda #TitleScreen
sta GameState
; lda #BackgroundColor ; Hide the grid separator
; sta COLUPF
ldx #LastDataCellOffset ; Print using tiles
TitleScreenLoop:
lda TitleTiles-FirstDataCellOffset,x
sta CellTable,x
dex
cpx #FirstDataCellOffset-1
bne TitleScreenLoop
jsr DisplayBoard
jmp StartFrame
TitleTiles:
.byte $20, $48, $65, $02, CellSentinel
.byte $20, $48, $65, $02, CellSentinel
.byte $20, $48, $65, $02, CellSentinel
.byte $20, $48, $65, $02, CellSentinel
;;;;;;;;;;;;;;
;; NEW GAME ;;
;;;;;;;;;;;;;;
StartNewGame:
ldx #LastDataCellOffset ; Last non-sentinel cell offset
lda #CellEmpty
InitCellTableLoop2Outer:
ldy #4 ; We'll clean 4 cells at a time
InitCellTableLoop2Inner:
sta CellTable,x
dex
dey
bne InitCellTableLoop2Inner
dex ; skip 1 cell (side sentinel)
cpx #FirstDataCellOffset
bcs InitCellTableLoop2Outer ; and continue until we pass the top-left cell
; Reset score
lda #0
ldx #5 ; Reset both scores if on two-player game
ldy GameMode
cpy #TwoPlayerGame
beq LoopResetScore
ldx #2 ; Only reset P0 if single-player (P1=record)
LoopResetScore:
sta P0ScoreBCD,x
dex
bpl LoopResetScore
; Reset other variables
sta CurrentPlayer
sta CurrentBGColor
sta DidMerge2048
sta Party2048Counter
; Set up the screen
jsr InitScreen
; Start the game with a random tile
lda #AddingRandomTile
sta GameState
;;;;;;;;;;;;;;;;;
;; FRAME START ;;
;;;;;;;;;;;;;;;;;
StartFrame:
;;;;;;;;;;;;;;;;;;;;;
;; NEW RANDOM TILE ;;
;;;;;;;;;;;;;;;;;;;;;
lda GameState
cmp #AddingRandomTile
bne EndRandomTile ; No need for a random tile now
; Pick a random cell from a number from 0..15, mapping those that would
; hit sentinels on the right side ("walls") to the ones not covered by the range
lda RandomNumber
and #$0F
cmp #Wall1
bne NoWall1
lda #Wall1Repl
NoWall1:
cmp #Wall2
bne NoWall2
lda #Wall2Repl
NoWall2:
cmp #Wall3
bne CheckIfCellIsEmpty
lda #Wall3Repl
CheckIfCellIsEmpty:
tax ; Cell offset now in X
lda CellTable+FirstDataCellOffset,x
cmp #CellEmpty
bne EndRandomTile ; Tile not empty, let's try again next frame
PickTileType:
ldy RandomNumber
cpy #ThresholdForTile4
bcc PickTile2
lda #Cell4 ; >= threshold, A will be a "4" tile
jmp AddTileToCell
PickTile2:
lda #Cell2 ; < threshold, A will be a "2" tile
AddTileToCell:
sta CellTable+FirstDataCellOffset,x
lda #WaitingJoyRelease ; Tile added, player can play as soon as they
sta GameState ; release the joystick
jsr DisplayBoard
EndRandomTile:
inc RandomNumber ; Feed the random number generator
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SELECT, RESET AND P0 FIRE BUTTON ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
ldx GameMode ; Remember if we were on one or two player mode
; lda SWCHB ; We only want the switch presses once
; and #SelectResetMask ; (in particular GAME SELECT)
; cmp LastSWCHB
; beq NoSwitchChange
; sta LastSWCHB ; Store so we know if it's a repeat next time
; cmp #GameSelect ; GAME SELECT flips single/multiplayer...
; bne NoSelect
; lda GameMode
; eor #1
; sta GameMode
; jmp StartNewGame ; ...and restarts with no further game mode change
NoSelect:
; cmp #GameReset ; GAME RESET restarts the game at any time
; beq Restart
NoSwitchChange:
; lda INPT4
; bpl ButtonPressed ; P0 Fire button pressed?
; ldx #1 ; P1 fire button always starts two-player game
; lda INPT5 ; P1 fire button pressed?
; bmi NoRestart
ButtonPressed:
; lda GameState
; cmp #TitleScreen
; beq Restart ; Start game if title screen
; cmp #GameOver ; or game over
; bne NoRestart
Restart:
; stx GameMode
; jmp StartNewGame
lda RestartLatch
beq NoRestart
lda #$00
sta RestartLatch
jmp StartNewGame
NoRestart:
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; POST-SHIFT ACTION (MERGE ANIMATION, SCORE UPDATE, GAME OVER) ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
lda GameState
cmp #ShowingMerged
bne ResetAnimationCounter ; No animation: just keep the counter ready
lda AnimationCounter ; Steadly decrease the shift tone,
lsr ; giving it a "wall hit" feeling
lsr
sta AUDV0
dec AnimationCounter ; Decrease counter until animation ends
bne DoneCounterManagement
; Finished animation: update score and add new tile
lda #AddingRandomTile ; Animation done, let's add a tile...
sta GameState
ldx #FirstDataCellOffset ; and count points for merged cells...
lda #0
sta DidMerge ; and set this flag if there were any
sta DidMerge2048 ; and this to flag if any was a 2048
CountScoreLoop:
lda CellTable,x
cmp #MergedMask
bmi ClearMergedBit ; Not merged, just clear the bit
sta DidMerge ; Flag that we had (at least one) merge
and #ClearMergedMask ; and get the value without the merge bit
cmp #Cell2048
bne CountScore
sta DidMerge2048 ; If we merged a 2048, flag it
CountScore:
asl
tay ; Now Y = offset of tile values table
sed ; We'll work in BCD
lda CurrentPlayer
bne AddScoreToP1
AddScoreToP0:
clc
lda TileValuesBCD-3,y
adc P0ScoreBCD+2
sta P0ScoreBCD+2 ; score "low byte" += table LSB
lda TileValuesBCD-4,y
adc P0ScoreBCD+1
sta P0ScoreBCD+1 ; score "middle byte" += table MSB + carry
lda #0
adc P0ScoreBCD
sta P0ScoreBCD ; score "high byte" += carry
jmp DoneAddingScore
AddScoreToP1:
clc
lda TileValuesBCD-3,y
adc P1ScoreBCD+2
sta P1ScoreBCD+2 ; score "low byte" += table LSB
lda TileValuesBCD-4,y
adc P1ScoreBCD+1
sta P1ScoreBCD+1 ; score "middle byte" += table MSB + carry
lda #0
adc P1ScoreBCD
sta P1ScoreBCD ; score "high byte" += carry
DoneAddingScore:
cld
lda CellTable,x ; Restore original value
ClearMergedBit:
and #ClearMergedMask ; Clear bit and store
sta CellTable,x
inx
cpx #LastDataCellOffset+1
bne CountScoreLoop ; Go to the next, up to the last tile
; Change the turn to the other player, if appropriate
lda GameMode
cmp #TwoPlayerGame
bne DoneCounterManagement ; Single player always keeps their turn
lda DidMerge
bne DoneCounterManagement ; Did merge, player keeps his turn
lda CurrentPlayer
eor #1 ; Flip player (0<->1)
sta CurrentPlayer
lda #TurnIndicatorFrames ; Show it's his turn
sta TurnIndicatorCounter
ResetAnimationCounter:
DoneCounterManagement:
; Reached first 2048? Let's party!
lda DidMerge2048
beq NoParty ; No 2048
lda Party2048Counter
bne NoParty ; Already had a party
inc Party2048Counter ; Let's party!
jsr DisplayWonMsg
NoParty:
;;;;;;;;;;;;;;;;;;;;;;;;;
;; GAME OVER DETECTION ;;
;;;;;;;;;;;;;;;;;;;;;;;;;
lda GameState
cmp #WaitingJoyRelease
bne EndGameOverDetection
ldx #FirstDataCellOffset
FindAMoveLoop:
lda CellTable,x ; A = current cell value
cmp #CellEmpty
beq FoundAMove ; Empty cell => can move
inx
cmp #CellSentinel
beq FindAMoveLoopEnd ; Sentinel, check next cell
cmp CellTable,x ; X = offset of cell to the right
beq FoundAMove ; Cell matches right neighbour => can merge
inx
inx
inx
inx ; X = offset of cell to the bottom (1+4=5)
cmp CellTable,x
beq FoundAMove ; Cell matches bottom neighbour => can merge
dex
dex
dex
dex ; X = offset of next cell (1+4-4=1)
FindAMoveLoopEnd:
cpx #LastDataCellOffset+1
bne FindAMoveLoop ; Iterate all tiles
; If we get here, no move was found => game over
lda #GameOverFX ; Start the game over effects...
sta GameState
lda #127 ; with a centered...
sta AUDF0
lda #8 ; explosion-ish...
sta AUDC0
lda #15 ; loud sound
sta AUDV0
ldx #0
stx GameOverEffectCounter
; For two-player mode, find the winner and make it current (=highlighted)
; For one-player mode, just update the high score (P1 Score)
ldy GameMode
FindHigherScoreLoop: ; Iterate over score byte until we can
lda P0ScoreBCD,x ; define whether P0 or P1 is higher
cmp P1ScoreBCD,x
beq ContinueFindHigherScore ; Can't tell from this byte, keep looping
bcc P1Higher
P0Higher:
lda #0
cpy #TwoPlayerGame
beq SetTurnToWinner ; Two-player game: P0 wins, make it current.
ldx P0ScoreBCD ; One-player game: we have a new record,
stx P1ScoreBCD ; copy it to the P1 (high score)
ldx P0ScoreBCD+1
stx P1ScoreBCD+1
ldx P0ScoreBCD+2
stx P1ScoreBCD+2
jmp EndGameOverDetection
P1Higher:
cpy #OnePlayerGame
beq EndGameOverEffects ; One-player game: no new record, we're done
lda #1 ; Two-Player game: P1 wins, make it current.
jmp SetTurnToWinner
ContinueFindHigherScore:
inx
cpx #3
bne FindHigherScoreLoop
BothAreLosers:
lda #99 ; In a tie, no one will be bright
SetTurnToWinner:
sta CurrentPlayer
FoundAMove:
EndGameOverDetection:
;;;;;;;;;;;;;;;;;;;;;;;
;; GAME OVER EFFECTS ;;
;;;;;;;;;;;;;;;;;;;;;;;
lda GameState
cmp #GameOverFX
bne EndGameOverEffects
lda RandomNumber ; Flash the background
; sta COLUBK
sta CurrentBGColor
inc GameOverEffectCounter ; Keep on for ~2.3s (+/-0.2 for PAL/NTSC diff)
bpl EndGameOverEffects
lda #BackgroundColor ; Now game is *really* over, cut the effects
; sta COLUBK
sta CurrentBGColor
lda #0
sta AUDV0
lda #GameOver
sta GameState
jsr DisplayGameOverMsg
EndGameOverEffects:
;;;;;;;;;;;;;;;;;;;;;;;;;;
;; 2048 "PARTY" EFFECTS ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;
lda Party2048Counter
beq DonePartyCheck ; No party yet
bmi DonePartyCheck ; Party is over
lda #04 ; Party!
; sta AUDC1
lda Party2048Counter
; sta AUDF1
; sta COLUPF
lda #10
; sta AUDV1
inc Party2048Counter
bpl DonePartyCheck
lda #0 ; End of party
; sta AUDV1
lda GridColor
; sta COLUPF
DonePartyCheck:
;;;;;;;;;;;;;;;;;;;;
;; INPUT CHECKING ;;
;;;;;;;;;;;;;;;;;;;;
; Joystick
jsr READ_CHAR
VerifyGameStateForJoyCheck:
ldx GameState ; We only care for states in which we are waiting
cpx #WaitingJoyRelease ; for a joystick press or release
beq CheckJoyRelease
cpx #WaitingJoyPress
bne EndJoyCheck
; If the joystick is in one of these directions, trigger the shift by
; setting the ShiftVector and changing mode
CheckJoyUp:
cmp #$8b
bne CheckJoyDown
lda #UpShiftVector
jmp TriggerShift
CheckJoyDown:
cmp #$8a
bne CheckJoyLeft
lda #DownShiftVector
jmp TriggerShift
CheckJoyLeft:
cmp #$88
bne CheckJoyRight
lda #LeftShiftVector
jmp TriggerShift
CheckJoyRight:
cmp #$95
bne EndJoyCheck
lda #RightShiftVector
TriggerShift:
sta ShiftVector ; We'll need the direction vector on the shift
lda #Shifting
sta GameState
jmp EndJoyCheck
CheckJoyRelease:
lda tmp
bmi EndJoyCheck
lda #WaitingJoyPress ; Joystick released, can accept shifts again
sta GameState
EndJoyCheck:
;;;;;;;;;;;;;;;;;
;; SHIFT BOARD ;;
;;;;;;;;;;;;;;;;;
lda GameState
cmp #Shifting
beq StartShift ; Have to do this instead of bne EndShift
jmp EndShift ; because the routine is > 128 bytes!
; Outer loop will traverse the entire cell map in the *opposite* order of the
; movement, that is, from the beginning for right/up and from end for left/down,
; so they stack and merge as expected. Let's setup these parameters
StartShift:
lda #0 ; So far, no merges happened
sta DidMerge
SetDirection:
lda ShiftVector
bmi NegativeVector
PositiveVector:
ldx #LastDataCellOffset ; Start from the last cell
ldy #FirstDataCellOffset-1 ; Stop when we pass the first one
lda #$FF ; Go backwards (-1)
jmp SetShiftParams
NegativeVector:
ldx #FirstDataCellOffset ; Start from the first cell
ldy #LastDataCellOffset+1 ; Stop when we pass the last one
lda #$01 ; Go forward (+1)
SetShiftParams:
sty ShiftEndOffset
sta TilesLoopDirection
lda #WaitingJoyRelease ; We won't animate or add tiles unless some
sta GameState ; movement happens
; Notice that X will keep the offset of the cell being processed (main loop),
; and will advance until we have no more processable tile positions.
;
; Whenever we have a "good" tile on X (not empty or sentinel), we'll start
; pushing it on the vector direciton (inner/"push" loop), until we can't
; push anyomre.
CheckIfXIsPushable:
lda CellTable,x
cmp #CellSentinel
beq AdvanceToNext ; Skip empty cells
cmp #CellEmpty
beq AdvanceToNext ; Skip sentinels
stx OffsetBeingPushed
jmp StartPush ; This one is good, start pushing it
AdvanceToNext:
txa ; Move X to the next candidate offset
clc
adc TilesLoopDirection
tax
cpx ShiftEndOffset
beq TurnNoiseSetup ; Processed all tiles, shift is done!
jmp CheckIfXIsPushable ; Check the new candidate
TurnNoiseSetup:
lda GameState
cmp #WaitingJoyRelease
beq EndShift ; No shift => no noise
ldx #ShiftNoise
lda DidMerge
beq StartNoise ; Shift with no merge => shift noise
ldx #MergeNoise ; Shift & merge => merge noise
StartNoise:
stx AUDC0
lda #90
sta AUDF0
jmp EndShift
; Inner loop will push the tile currenlty picked by the outer loop towards
; the desired direction, until hitting an unmergeable tile
StartPush:
lda CellTable,x
sta CurrentValue ; Keep the current tile's value
stx OffsetBeingPushed ; Initialize inner loop counter
PushCurrentTile: ; Inner loop begins here
clc
lda OffsetBeingPushed
adc ShiftVector
tay
lda CellTable,y ; A <= value of next cell in the vector direction
cmp #CellEmpty
bne NotEmpty ; We won't move if the next cell is not empty
MoveCurrentToNext:
lda #ShowingMerged ; If we move at least once, we can show merged
sta GameState ; animations (if any)
lda CurrentValue
sta CellTable,y ; Set next cell to current value
lda #CellEmpty
ldy OffsetBeingPushed
sta CellTable,y ; Clear current cell
clc
lda OffsetBeingPushed
adc ShiftVector
sta OffsetBeingPushed ; - make next the one being pushed
jmp PushCurrentTile ; Keep pushing
NotEmpty:
cmp #MergedMask
bcs AdvanceToNext ; Can't merge if next cell has already been merged
cmp CurrentValue
bne AdvanceToNext ; Only merge if value matches
Merge:
inc CurrentValue ; Multiply by 2 in log (that is, add 1 to exponent)
lda #MergedMask
ora CurrentValue ; Add the "merged" bit (so it doesn't match others)
sta CurrentValue
sta DidMerge ; Signal the merge
jmp MoveCurrentToNext ; Move the multiplied cell to the target position
EndShift:
;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; REMAINDER OF OVERSCAN ;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;
WaitForOverscanEndLoop:
; lda INTIM ; Wait until the timer signals the actual end
; bne WaitForOverscanEndLoop ; of the overscan period
; sta WSYNC
jmp StartFrame
tmp: .byte 0
; The MIT License (MIT)
; Copyright (c) 2014 Carlos Duarte do Nascimento (Chester)
;
; Original 2048 game Copyright (c) 2014 Gabriele Cirulli
; Permission is hereby granted, free of charge, to any person obtaining a copy
; of this software and associated documentation files (the "Software"), to deal
; in the Software without restriction, including without limitation the rights
; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
; copies of the Software, and to permit persons to whom the Software is
; furnished to do so, subject to the following conditions:
; The above copyright notice and this permission notice shall be included in all
; copies or substantial portions of the Software.
; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
; SOFTWARE.
AUDV0 = $c030
AUDF0 = $c030
AUDC0 = $c030