├── .gitattributes ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets ├── cart-label-blocks-hydrant.png ├── cart-label-blocks.png ├── cart-label-fds-akfamilyhome.png ├── cart-label-fds-akfamilyhome2.png ├── cart-label-header.png ├── cart-label.png ├── logo.png ├── qr-code.png └── screens │ ├── anylevel.png │ ├── block.png │ ├── btype.png │ ├── checkerboard.png │ ├── controller.png │ ├── crash.png │ ├── crunch.png │ ├── dark.png │ ├── floor.png │ ├── gamehearts.png │ ├── garbage.png │ ├── harddrop.png │ ├── hearts.png │ ├── hz.png │ ├── invisible.png │ ├── legal.png │ ├── levelmenu.png │ ├── levelselect.png │ ├── linecap.png │ ├── lowstack.png │ ├── menu3.png │ ├── menu4.png │ ├── menu5.png │ ├── menu6.png │ ├── pace.png │ ├── ready.png │ ├── rocket.png │ ├── score-7digit.png │ ├── score-capped.png │ ├── score-classic.png │ ├── score-hidden.png │ ├── score-letters.png │ ├── score-m.png │ ├── setups.png │ ├── speedtest.png │ ├── stacking.png │ ├── tap.png │ ├── tapqty.png │ ├── transition.png │ └── tspins.png ├── build.js ├── src ├── audio.asm ├── boot.asm ├── charmap.asm ├── chr.asm ├── chr │ ├── game_tileset.png │ ├── rocket_tileset.png │ └── title_menu_tileset.png ├── constants.asm ├── data │ ├── bytebcd.asm │ ├── mult.asm │ └── orientation.asm ├── gamemode │ ├── bootscreen.asm │ ├── branch.asm │ ├── gametypemenu │ │ ├── linecap.asm │ │ └── menu.asm │ ├── levelmenu.asm │ ├── speedtest.asm │ └── waitscreen.asm ├── gamemodestate │ ├── branch.asm │ ├── checkforabss.asm │ ├── handlegameover.asm │ ├── initbackground.asm │ ├── initstate.asm │ ├── pause.asm │ ├── updatecounters.asm │ └── updateplayer1.asm ├── header.asm ├── highscores │ ├── data.asm │ ├── entry_screen.asm │ ├── render_menu.asm │ └── util.asm ├── io.asm ├── main.asm ├── modes │ ├── controllerinput.asm │ ├── crash.asm │ ├── crunch.asm │ ├── debug.asm │ ├── events.asm │ ├── floor.asm │ ├── garbage.asm │ ├── hz.asm │ ├── initchecker.asm │ ├── pace.asm │ ├── parity.asm │ ├── preset.asm │ ├── qtap.asm │ ├── saveslots.asm │ ├── tapqty.asm │ └── tspins.asm ├── nametables.asm ├── nametables │ ├── build.js │ ├── enter_high_score.js │ ├── game.js │ ├── game_type_menu.js │ ├── level_menu.js │ ├── nametables.js │ ├── rle.asm │ ├── rle.js │ └── rocket_legal.js ├── nmi │ ├── nmi.asm │ ├── pollcontroller.asm │ ├── pollkeyboard.asm │ ├── render.asm │ ├── render_hz.asm │ ├── render_input_log.asm │ ├── render_mode_congratulations_screen.asm │ ├── render_mode_level_menu.asm │ ├── render_mode_linecap.asm │ ├── render_mode_pause.asm │ ├── render_mode_play_and_demo.asm │ ├── render_mode_rocket.asm │ ├── render_mode_scroll.asm │ ├── render_mode_speed_test.asm │ ├── render_score.asm │ └── render_util.asm ├── palettes.asm ├── playstate │ ├── active.asm │ ├── branch.asm │ ├── completedrows.asm │ ├── gameover_rocket.asm │ ├── garbage.asm │ ├── lock.asm │ ├── preparenext.asm │ ├── spawnnext.asm │ ├── updatestats.asm │ └── util.asm ├── presets │ ├── presets.asm │ └── presets.js ├── ram.asm ├── reset.asm ├── sprites │ ├── bytesprite.asm │ ├── drawrect.asm │ ├── loadsprite.asm │ └── piece.asm ├── tetris.nes.cfg └── util │ ├── autodetect.asm │ ├── check_region.asm │ ├── core.asm │ ├── mapper.asm │ ├── math.asm │ ├── menuthrottle.asm │ ├── modetext.asm │ └── strings.asm ├── tests ├── Cargo.lock ├── Cargo.toml ├── README.md └── src │ ├── block.rs │ ├── constants.rs │ ├── crash.rs │ ├── crunch.rs │ ├── cycle_count.rs │ ├── drought.rs │ ├── floor.rs │ ├── garbage.rs │ ├── harddrop.rs │ ├── hz_display.rs │ ├── input.rs │ ├── labels.rs │ ├── main.rs │ ├── mapper.rs │ ├── nmi.rs │ ├── palettes.rs │ ├── patch.rs │ ├── playfield.rs │ ├── pushdown.rs │ ├── rng.rs │ ├── score.rs │ ├── sps.rs │ ├── toprow.rs │ ├── tspins.rs │ ├── util.rs │ └── video.rs └── tools ├── assemble ├── README.md ├── ca65.js ├── ca65.wasm ├── ld65.js └── ld65.wasm ├── cycles.lua ├── format.js ├── hzpalette.html ├── modes.awk ├── nametablepatch.js ├── pace.rb ├── patch ├── bps.js └── create.js └── png2chr ├── convert.js └── png.js /.gitattributes: -------------------------------------------------------------------------------- 1 | tools/png2chr/png.js linguist-generated 2 | tools/patch/bps.js linguist-generated 3 | tools/assemble/ca65.js linguist-generated 4 | tools/assemble/ld65.js linguist-generated 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.nes 2 | *.bps 3 | *.ips 4 | *.o 5 | *.chr 6 | *.pyc 7 | *.bin 8 | tetris.lst 9 | tetris.lbl 10 | tetris.map 11 | tetris.dbg 12 | release/* 13 | target 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 2 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 3 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 4 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 5 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 6 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 7 | SOFTWARE. 8 | -------------------------------------------------------------------------------- /assets/cart-label-blocks-hydrant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/cart-label-blocks-hydrant.png -------------------------------------------------------------------------------- /assets/cart-label-blocks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/cart-label-blocks.png -------------------------------------------------------------------------------- /assets/cart-label-fds-akfamilyhome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/cart-label-fds-akfamilyhome.png -------------------------------------------------------------------------------- /assets/cart-label-fds-akfamilyhome2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/cart-label-fds-akfamilyhome2.png -------------------------------------------------------------------------------- /assets/cart-label-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/cart-label-header.png -------------------------------------------------------------------------------- /assets/cart-label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/cart-label.png -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/logo.png -------------------------------------------------------------------------------- /assets/qr-code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/qr-code.png -------------------------------------------------------------------------------- /assets/screens/anylevel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/anylevel.png -------------------------------------------------------------------------------- /assets/screens/block.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/block.png -------------------------------------------------------------------------------- /assets/screens/btype.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/btype.png -------------------------------------------------------------------------------- /assets/screens/checkerboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/checkerboard.png -------------------------------------------------------------------------------- /assets/screens/controller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/controller.png -------------------------------------------------------------------------------- /assets/screens/crash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/crash.png -------------------------------------------------------------------------------- /assets/screens/crunch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/crunch.png -------------------------------------------------------------------------------- /assets/screens/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/dark.png -------------------------------------------------------------------------------- /assets/screens/floor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/floor.png -------------------------------------------------------------------------------- /assets/screens/gamehearts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/gamehearts.png -------------------------------------------------------------------------------- /assets/screens/garbage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/garbage.png -------------------------------------------------------------------------------- /assets/screens/harddrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/harddrop.png -------------------------------------------------------------------------------- /assets/screens/hearts.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/hearts.png -------------------------------------------------------------------------------- /assets/screens/hz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/hz.png -------------------------------------------------------------------------------- /assets/screens/invisible.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/invisible.png -------------------------------------------------------------------------------- /assets/screens/legal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/legal.png -------------------------------------------------------------------------------- /assets/screens/levelmenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/levelmenu.png -------------------------------------------------------------------------------- /assets/screens/levelselect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/levelselect.png -------------------------------------------------------------------------------- /assets/screens/linecap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/linecap.png -------------------------------------------------------------------------------- /assets/screens/lowstack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/lowstack.png -------------------------------------------------------------------------------- /assets/screens/menu3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/menu3.png -------------------------------------------------------------------------------- /assets/screens/menu4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/menu4.png -------------------------------------------------------------------------------- /assets/screens/menu5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/menu5.png -------------------------------------------------------------------------------- /assets/screens/menu6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/menu6.png -------------------------------------------------------------------------------- /assets/screens/pace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/pace.png -------------------------------------------------------------------------------- /assets/screens/ready.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/ready.png -------------------------------------------------------------------------------- /assets/screens/rocket.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/rocket.png -------------------------------------------------------------------------------- /assets/screens/score-7digit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/score-7digit.png -------------------------------------------------------------------------------- /assets/screens/score-capped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/score-capped.png -------------------------------------------------------------------------------- /assets/screens/score-classic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/score-classic.png -------------------------------------------------------------------------------- /assets/screens/score-hidden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/score-hidden.png -------------------------------------------------------------------------------- /assets/screens/score-letters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/score-letters.png -------------------------------------------------------------------------------- /assets/screens/score-m.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/score-m.png -------------------------------------------------------------------------------- /assets/screens/setups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/setups.png -------------------------------------------------------------------------------- /assets/screens/speedtest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/speedtest.png -------------------------------------------------------------------------------- /assets/screens/stacking.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/stacking.png -------------------------------------------------------------------------------- /assets/screens/tap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/tap.png -------------------------------------------------------------------------------- /assets/screens/tapqty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/tapqty.png -------------------------------------------------------------------------------- /assets/screens/transition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/transition.png -------------------------------------------------------------------------------- /assets/screens/tspins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/assets/screens/tspins.png -------------------------------------------------------------------------------- /src/boot.asm: -------------------------------------------------------------------------------- 1 | ; $0000 through $06FF cleared during vblank wait 2 | lda initMagic 3 | cmp #$54 4 | bne @coldBoot 5 | lda initMagic+1 6 | cmp #$2D 7 | bne @coldBoot 8 | lda initMagic+2 9 | cmp #$47 10 | bne @coldBoot 11 | lda initMagic+3 12 | cmp #$59 13 | bne @coldBoot 14 | lda initMagic+4 15 | cmp #$4D 16 | bne @coldBoot 17 | jmp @continueWarmBootInit 18 | 19 | @coldBoot: 20 | ; zero out config memory 21 | lda #$0 22 | ldx #$A0 23 | @loop: 24 | dex 25 | sta menuRAM, x 26 | ; cpx #0 ; dex sets z flag 27 | bne @loop 28 | 29 | ; default pace to A 30 | lda #$A 31 | sta paceModifier 32 | 33 | lda #$10 34 | sta dasModifier 35 | 36 | lda #INITIAL_LINECAP_LEVEL 37 | sta linecapLevel 38 | lda #INITIAL_LINECAP_LINES 39 | sta linecapLines 40 | lda #INITIAL_LINECAP_LINES_1 41 | sta linecapLines+1 42 | 43 | jsr resetScores 44 | 45 | .if SAVE_HIGHSCORES 46 | jsr detectSRAM 47 | beq @noSRAM 48 | jsr checkSavedInit 49 | jsr copyScoresFromSRAM 50 | @noSRAM: 51 | .endif 52 | 53 | lda #$54 54 | sta initMagic 55 | lda #$2D 56 | sta initMagic+1 57 | lda #$47 58 | sta initMagic+2 59 | lda #$59 60 | sta initMagic+3 61 | lda #$4D 62 | sta initMagic+4 63 | @continueWarmBootInit: 64 | ldx #$89 65 | stx rng_seed 66 | dex 67 | stx rng_seed+1 68 | ldy #$00 69 | sty PPUSCROLL 70 | ldy #$00 71 | sty PPUSCROLL 72 | lda #$90 73 | sta currentPpuCtrl 74 | sta PPUCTRL 75 | lda #$06 76 | sta PPUMASK 77 | jsr LE006 78 | jsr updateAudio2 79 | jsr updateAudioWaitForNmiAndDisablePpuRendering 80 | jsr disableNmi 81 | jsr drawBlackBGPalette 82 | ; instead of clearing vram like the original, blank out the palette 83 | lda #$EF 84 | ldx #$04 85 | ldy #$04 ; used to be 5, but we dont need to clear 2p playfield 86 | jsr memset_page 87 | jsr waitForVBlankAndEnableNmi 88 | jsr updateAudioWaitForNmiAndResetOamStaging 89 | jsr updateAudioWaitForNmiAndEnablePpuRendering 90 | jsr updateAudioWaitForNmiAndResetOamStaging 91 | lda #$00 92 | sta gameModeState 93 | sta gameMode 94 | lda #$00 95 | sta frameCounter+1 96 | -------------------------------------------------------------------------------- /src/charmap.asm: -------------------------------------------------------------------------------- 1 | .charmap '0', $00 2 | .charmap '1', $01 3 | .charmap '2', $02 4 | .charmap '3', $03 5 | .charmap '4', $04 6 | .charmap '5', $05 7 | .charmap '6', $06 8 | .charmap '7', $07 9 | .charmap '8', $08 10 | .charmap '9', $09 11 | .charmap 'A', $0a 12 | .charmap 'B', $0b 13 | .charmap 'C', $0c 14 | .charmap 'D', $0d 15 | .charmap 'E', $0e 16 | .charmap 'F', $0f 17 | .charmap 'G', $10 18 | .charmap 'H', $11 19 | .charmap 'I', $12 20 | .charmap 'J', $13 21 | .charmap 'K', $14 22 | .charmap 'L', $15 23 | .charmap 'M', $16 24 | .charmap 'N', $17 25 | .charmap 'O', $18 26 | .charmap 'P', $19 27 | .charmap 'Q', $1a 28 | .charmap 'R', $1b 29 | .charmap 'S', $1c 30 | .charmap 'T', $1d 31 | .charmap 'U', $1e 32 | .charmap 'V', $1f 33 | .charmap 'W', $20 34 | .charmap 'X', $21 35 | .charmap 'Y', $22 36 | .charmap 'Z', $23 37 | .charmap '-', $24 38 | .charmap '.', $25 39 | .charmap '>', $27 40 | .charmap '!', $52 41 | .charmap '?', $55 42 | .charmap '^', $29 43 | .charmap '(', $2a 44 | .charmap ')', $2b 45 | .charmap ' ', $ff 46 | -------------------------------------------------------------------------------- /src/chr.asm: -------------------------------------------------------------------------------- 1 | .segment "CHR" 2 | ; CHRBankSet0: 3 | .incbin "chr/title_menu_tileset.chr" 4 | .incbin "chr/game_tileset.chr" 5 | ; CHRBankSet1: 6 | .if INES_MAPPER <> 0 ; exclude for NROM 7 | .incbin "chr/rocket_tileset.chr" 8 | .endif 9 | -------------------------------------------------------------------------------- /src/chr/game_tileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/src/chr/game_tileset.png -------------------------------------------------------------------------------- /src/chr/rocket_tileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/src/chr/rocket_tileset.png -------------------------------------------------------------------------------- /src/chr/title_menu_tileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/src/chr/title_menu_tileset.png -------------------------------------------------------------------------------- /src/constants.asm: -------------------------------------------------------------------------------- 1 | .ifndef INES_MAPPER ; is set via ca65 flags 2 | INES_MAPPER := 1000 ; 0 (NROM), 1 (MMC1), 3 (CNROM), 4 (MMC3), 5 (MMC5), and 1000 (autodetect 1/3) 3 | .endif 4 | 5 | .ifndef SAVE_HIGHSCORES 6 | SAVE_HIGHSCORES := 1 7 | .endif 8 | 9 | .ifndef AUTO_WIN 10 | ; faster aeppoz + press select to end game 11 | AUTO_WIN := 0 12 | .endif 13 | 14 | .ifndef KEYBOARD 15 | KEYBOARD := 0 16 | .endif 17 | 18 | .ifndef CNROM_OVERRIDE 19 | CNROM_OVERRIDE := 0 20 | .endif 21 | 22 | NO_MUSIC := 1 23 | 24 | ; dev flags 25 | NO_SCORING := 0 ; breaks pace 26 | NO_SFX := 0 27 | NO_MENU := 0 28 | ALWAYS_CURTAIN := 0 29 | QUAL_BOOT := 0 30 | SWAP_DUTY_CYCLES := 0 ; counters the duty cycle swap present in some clone consoles 31 | 32 | INITIAL_CUSTOM_LEVEL := 29 33 | INITIAL_LINECAP_LEVEL := 39 34 | INITIAL_LINECAP_LINES := $30 ; bcd 35 | INITIAL_LINECAP_LINES_1 := 3 ; hex (lol) 36 | BTYPE_START_LINES := $25 ; bcd 37 | MENU_HIGHLIGHT_COLOR := $12 ; $12 in gym, $16 in original 38 | BLOCK_TILES := $7B 39 | EMPTY_TILE := $EF 40 | LOW_STACK_LINE := $DF 41 | TETRIMINO_X_HIDE := $EF 42 | 43 | PAUSE_SPRITE_X := $74 44 | PAUSE_SPRITE_Y := $58 45 | ; jazzthief-style 46 | ; PAUSE_SPRITE_X := $C4 47 | ; PAUSE_SPRITE_Y := $16 48 | 49 | BUTTON_DOWN := $4 50 | BUTTON_UP := $8 51 | BUTTON_RIGHT := $1 52 | BUTTON_LEFT := $2 53 | BUTTON_B := $40 54 | BUTTON_A := $80 55 | BUTTON_SELECT := $20 56 | BUTTON_START := $10 57 | BUTTON_DPAD := BUTTON_UP | BUTTON_DOWN | BUTTON_LEFT | BUTTON_RIGHT 58 | 59 | RENDER_LINES = $01 60 | RENDER_LEVEL = $02 61 | RENDER_SCORE = $04 62 | RENDER_DEBUG = $08 63 | RENDER_HZ = $10 64 | RENDER_STATS = $40 65 | RENDER_HIGH_SCORE_LETTER = $80 66 | 67 | .enum 68 | MODE_TETRIS 69 | MODE_TSPINS 70 | MODE_SEED 71 | MODE_PARITY 72 | MODE_PACE 73 | MODE_PRESETS 74 | MODE_TYPEB 75 | MODE_FLOOR 76 | MODE_CRUNCH 77 | MODE_TAP 78 | MODE_TRANSITION 79 | MODE_MARATHON 80 | MODE_TAPQTY 81 | MODE_CHECKERBOARD 82 | MODE_GARBAGE 83 | MODE_DROUGHT 84 | MODE_DAS 85 | MODE_LOWSTACK 86 | MODE_KILLX2 87 | MODE_INVISIBLE 88 | MODE_HARDDROP 89 | MODE_SPEED_TEST 90 | MODE_SCORE_DISPLAY 91 | MODE_CRASH 92 | MODE_STRICT 93 | MODE_HZ_DISPLAY 94 | MODE_INPUT_DISPLAY 95 | MODE_DISABLE_FLASH 96 | MODE_DISABLE_PAUSE 97 | MODE_DARK 98 | MODE_GOOFY 99 | MODE_DEBUG 100 | MODE_LINECAP 101 | MODE_DASONLY 102 | MODE_QUAL 103 | MODE_PAL 104 | .endenum 105 | 106 | MODE_QUANTITY = MODE_PAL + 1 107 | MODE_GAME_QUANTITY = MODE_HARDDROP + 1 108 | 109 | SCORING_CLASSIC := 0 ; for scoringModifier 110 | SCORING_LETTERS := 1 111 | SCORING_SEVENDIGIT := 2 112 | SCORING_FLOAT := 3 113 | SCORING_SCORECAP := 4 114 | SCORING_HIDDEN := 5 115 | 116 | LINECAP_KILLX2 := 1 117 | LINECAP_FLOOR := 2 118 | LINECAP_INVISIBLE := 3 119 | LINECAP_HALT := 4 120 | 121 | CRASH_OFF := 0 122 | CRASH_SHOW := 1 123 | CRASH_TOPOUT := 2 124 | CRASH_CRASH := 3 125 | 126 | LINECAP_WHEN_STRING_OFFSET := $10 127 | LINECAP_HOW_STRING_OFFSET := $12 128 | 129 | MENU_SPRITE_Y_BASE := $46 130 | MENU_MAX_Y_SCROLL := $A0 131 | MENU_TOP_MARGIN_SCROLL := 7 ; in blocks 132 | 133 | ; menuConfigSizeLookup 134 | ; menu ram is defined at menuRAM in ./ram.asm 135 | .macro MENUSIZES 136 | .byte $0 ; MODE_TETRIS 137 | .byte $0 ; MODE_TSPINS 138 | .byte $0 ; MODE_SEED 139 | .byte $0 ; MODE_PARITY 140 | .byte $F ; MODE_PACE 141 | .byte $7 ; MODE_PRESETS 142 | .byte $8 ; MODE_TYPEB 143 | .byte $C ; MODE_FLOOR 144 | .byte $F ; MODE_CRUNCH 145 | .byte $20 ; MODE_TAP 146 | .byte $10 ; MODE_TRANSITION 147 | .byte $2 ; MODE_MARATHON 148 | .byte $1F ; MODE_TAPQTY 149 | .byte $8 ; MODE_CHECKERBOARD 150 | .byte $4 ; MODE_GARBAGE 151 | .byte $12 ; MODE_DROUGHT 152 | .byte $10 ; MODE_DAS 153 | .byte $12 ; MODE_LOWSTACK 154 | .byte $0 ; MODE_KILLX2 155 | .byte $0 ; MODE_INVISIBLE 156 | .byte $0 ; MODE_HARDDROP 157 | .byte $0 ; MODE_SPEED_TEST 158 | .byte $5 ; MODE_SCORE_DISPLAY 159 | .byte $3 ; MODE_CRASH 160 | .byte $1 ; MODE_STRICT 161 | .byte $1 ; MODE_HZ_DISPLAY 162 | .byte $1 ; MODE_INPUT_DISPLAY 163 | .byte $1 ; MODE_DISABLE_FLASH 164 | .byte $1 ; MODE_DISABLE_PAUSE 165 | .byte $5 ; MODE_DARK 166 | .byte $1 ; MODE_GOOFY 167 | .byte $1 ; MODE_DEBUG 168 | .byte $1 ; MODE_LINECAP 169 | .byte $1 ; MODE_DASONLY 170 | .byte $1 ; MODE_QUAL 171 | .byte $1 ; MODE_PAL 172 | .endmacro 173 | 174 | .macro MODENAMES 175 | .byte "TETRIS" 176 | .byte "TSPINS" 177 | .byte " SEED " 178 | .byte "STACKN" 179 | .byte " PACE " 180 | .byte "SETUPS" 181 | .byte "B-TYPE" 182 | .byte "FLOOR " 183 | .byte "CRUNCH" 184 | .byte "QCKTAP" 185 | .byte "TRNSTN" 186 | .byte "MARTHN" 187 | .byte "TAPQTY" 188 | .byte "CKRBRD" 189 | .byte "GARBGE" 190 | .byte "LOBARS" 191 | .byte "DASDLY" 192 | .byte "LOWSTK" 193 | .byte "KILLX2" 194 | .byte "INVZBL" 195 | .byte "HRDDRP" 196 | .endmacro 197 | -------------------------------------------------------------------------------- /src/data/bytebcd.asm: -------------------------------------------------------------------------------- 1 | levelDisplayTable: ; original goes to 29 2 | byteToBcdTable: ; original goes to 49 3 | .byte $00,$01,$02,$03,$04,$05,$06,$07 4 | .byte $08,$09,$10,$11,$12,$13,$14,$15 5 | .byte $16,$17,$18,$19,$20,$21,$22,$23 6 | .byte $24,$25,$26,$27,$28,$29,$30,$31 7 | .byte $32,$33,$34,$35,$36,$37,$38,$39 8 | .byte $40,$41,$42,$43,$44,$45,$46,$47 9 | .byte $48,$49 10 | ; 50 extra bytes is shorter than a conversion routine (and super fast) 11 | ; (used in renderByteBCD) 12 | .byte $50,$51,$52,$53,$54,$55,$56,$57,$58,$59,$60,$61,$62,$63,$64,$65,$66,$67,$68,$69,$70,$71,$72,$73,$74,$75,$76,$77,$78,$79,$80,$81,$82,$83,$84,$85,$86,$87,$88,$89,$90,$91,$92,$93,$94,$95,$96,$97,$98,$99 13 | -------------------------------------------------------------------------------- /src/data/mult.asm: -------------------------------------------------------------------------------- 1 | multBy10Table: 2 | .byte $00,$0A,$14,$1E,$28,$32,$3C,$46 3 | .byte $50,$5A,$64,$6E,$78,$82,$8C,$96 4 | .byte $A0,$AA,$B4,$BE 5 | multBy32Table: 6 | .byte 0,32,64,96,128,160,192,224 7 | multBy100Table: 8 | .byte $0, $64, $c8, $2c, $90 9 | .byte $f4, $58, $bc, $20, $84 10 | -------------------------------------------------------------------------------- /src/data/orientation.asm: -------------------------------------------------------------------------------- 1 | orientationTable: 2 | .byte $00,$7B,$FF,$00,$7B,$00,$00,$7B 3 | .byte $01,$FF,$7B,$00,$FF,$7B,$00,$00 4 | .byte $7B,$00,$00,$7B,$01,$01,$7B,$00 5 | .byte $00,$7B,$FF,$00,$7B,$00,$00,$7B 6 | .byte $01,$01,$7B,$00,$FF,$7B,$00,$00 7 | .byte $7B,$FF,$00,$7B,$00,$01,$7B,$00 8 | .byte $FF,$7D,$00,$00,$7D,$00,$01,$7D 9 | .byte $FF,$01,$7D,$00,$FF,$7D,$FF,$00 10 | .byte $7D,$FF,$00,$7D,$00,$00,$7D,$01 11 | .byte $FF,$7D,$00,$FF,$7D,$01,$00,$7D 12 | .byte $00,$01,$7D,$00,$00,$7D,$FF,$00 13 | .byte $7D,$00,$00,$7D,$01,$01,$7D,$01 14 | .byte $00,$7C,$FF,$00,$7C,$00,$01,$7C 15 | .byte $00,$01,$7C,$01,$FF,$7C,$01,$00 16 | .byte $7C,$00,$00,$7C,$01,$01,$7C,$00 17 | .byte $00,$7B,$FF,$00,$7B,$00,$01,$7B 18 | .byte $FF,$01,$7B,$00,$00,$7D,$00,$00 19 | .byte $7D,$01,$01,$7D,$FF,$01,$7D,$00 20 | .byte $FF,$7D,$00,$00,$7D,$00,$00,$7D 21 | .byte $01,$01,$7D,$01,$FF,$7C,$00,$00 22 | .byte $7C,$00,$01,$7C,$00,$01,$7C,$01 23 | .byte $00,$7C,$FF,$00,$7C,$00,$00,$7C 24 | .byte $01,$01,$7C,$FF,$FF,$7C,$FF,$FF 25 | .byte $7C,$00,$00,$7C,$00,$01,$7C,$00 26 | .byte $FF,$7C,$01,$00,$7C,$FF,$00,$7C 27 | .byte $00,$00,$7C,$01,$FE,$7B,$00,$FF 28 | .byte $7B,$00,$00,$7B,$00,$01,$7B,$00 29 | .byte $00,$7B,$FE,$00,$7B,$FF,$00,$7B 30 | .byte $00,$00,$7B,$01,$00,$FF,$00,$00 31 | .byte $FF,$00,$00,$FF,$00,$00,$FF,$00 32 | 33 | tetriminoTypeFromOrientation: 34 | .byte $00,$00,$00,$00,$01,$01,$01,$01 35 | .byte $02,$02,$03,$04,$04,$05,$05,$05 36 | .byte $05,$06,$06 37 | spawnTable: 38 | .byte $02,$07,$08,$0A,$0B,$0E,$12 39 | .byte $02 40 | -------------------------------------------------------------------------------- /src/gamemode/bootscreen.asm: -------------------------------------------------------------------------------- 1 | gameMode_bootScreen: ; boot 2 | ; ABSS goes to gameTypeMenu instead of here 3 | 4 | ; reset cursors 5 | lda #$0 6 | sta practiseType 7 | sta menuSeedCursorIndex 8 | 9 | ; levelMenu stuff 10 | sta levelControlMode 11 | lda #INITIAL_CUSTOM_LEVEL 12 | sta customLevel 13 | 14 | ; detect region 15 | jsr updateAudioAndWaitForNmi 16 | jsr checkRegion 17 | 18 | .if !QUAL_BOOT 19 | ; check if qualMode is already set 20 | lda qualFlag 21 | bne @qualBoot 22 | ; hold select to start in qual mode 23 | lda heldButtons_player1 24 | and #BUTTON_SELECT 25 | beq @nonQualBoot 26 | .endif 27 | @qualBoot: 28 | lda #1 29 | sta gameMode 30 | lda #1 31 | sta qualFlag 32 | jmp gameMode_waitScreen 33 | 34 | @nonQualBoot: 35 | ; set start level to 8/18 36 | lda #$8 37 | sta classicLevel 38 | lda #2 39 | sta gameMode 40 | rts 41 | -------------------------------------------------------------------------------- /src/gamemode/branch.asm: -------------------------------------------------------------------------------- 1 | branchOnGameMode: 2 | lda gameMode 3 | jsr switch_s_plus_2a 4 | .addr gameMode_bootScreen 5 | .addr gameMode_waitScreen 6 | .addr gameMode_gameTypeMenu 7 | .addr gameMode_levelMenu 8 | .addr gameMode_playAndEndingHighScore_jmp 9 | .addr gameMode_playAndEndingHighScore_jmp ; use to be demo 10 | .addr gameMode_playAndEndingHighScore_jmp ; used to be startDemo 11 | .addr gameMode_speedTest 12 | 13 | .include "bootscreen.asm" 14 | .include "waitscreen.asm" 15 | .include "gametypemenu/menu.asm" 16 | .include "levelmenu.asm" 17 | 18 | gameMode_playAndEndingHighScore_jmp: 19 | jsr branchOnGameModeState 20 | rts 21 | 22 | .include "speedtest.asm" 23 | -------------------------------------------------------------------------------- /src/gamemode/speedtest.asm: -------------------------------------------------------------------------------- 1 | gameMode_speedTest: 2 | lda #$6 3 | sta renderMode 4 | ; reset some stuff for input log rendering 5 | lda #$EF 6 | sta inputLogCounter 7 | lda #$1 8 | sta hzFrameCounter+1 9 | 10 | jsr hzStart 11 | jsr updateAudioWaitForNmiAndDisablePpuRendering 12 | jsr disableNmi 13 | jsr clearNametable 14 | jsr bulkCopyToPpu 15 | .addr speedtest_nametable_patch 16 | jsr bulkCopyToPpu 17 | .addr game_palette 18 | ; patch color 19 | lda #$3f 20 | sta PPUADDR 21 | lda #$b 22 | sta PPUADDR 23 | lda #$30 24 | sta PPUDATA 25 | lda #NMIEnable|BGPattern1|SpritePattern1 26 | sta currentPpuCtrl 27 | .if INES_MAPPER <> 0 28 | lda #CHRBankSet0 29 | jsr changeCHRBanks 30 | .endif 31 | 32 | jsr waitForVBlankAndEnableNmi 33 | jsr updateAudioWaitForNmiAndResetOamStaging 34 | jsr updateAudioWaitForNmiAndEnablePpuRendering 35 | jsr updateAudioWaitForNmiAndResetOamStaging 36 | 37 | @loop: 38 | lda heldButtons_player1 39 | cmp #BUTTON_A+BUTTON_B+BUTTON_START+BUTTON_SELECT 40 | beq @back 41 | 42 | lda #$50 43 | sta tmp3 44 | jsr controllerInputDisplayX 45 | jsr speedTestControl 46 | 47 | jsr updateAudioWaitForNmiAndResetOamStaging 48 | jmp @loop 49 | 50 | @back: 51 | lda #$02 52 | sta soundEffectSlot1Init 53 | sta gameMode 54 | rts 55 | 56 | speedTestControl: 57 | ; add sfx 58 | lda heldButtons_player1 59 | and #BUTTON_LEFT+BUTTON_RIGHT+BUTTON_B+BUTTON_A 60 | beq @noupdate 61 | lda #RENDER_HZ 62 | sta renderFlags 63 | lda newlyPressedButtons_player1 64 | and #BUTTON_LEFT+BUTTON_RIGHT 65 | beq @noupdate 66 | lda #$1 67 | sta soundEffectSlot1Init 68 | @noupdate: 69 | ; use normal controls 70 | jsr hzControl 71 | rts 72 | -------------------------------------------------------------------------------- /src/gamemode/waitscreen.asm: -------------------------------------------------------------------------------- 1 | gameMode_waitScreen: 2 | lda #0 3 | sta screenStage 4 | waitScreenLoad: 5 | lda #$0 6 | sta renderMode 7 | jsr updateAudioWaitForNmiAndDisablePpuRendering 8 | jsr disableNmi 9 | lda #NMIEnable 10 | sta currentPpuCtrl 11 | .if INES_MAPPER <> 0 12 | ; NROM (and possibly FDS in the future) won't load the 2nd bankset 13 | ; and will instead use the title/menu chrset letters. This won't be noticeable 14 | ; unless a graphic is added 15 | lda #CHRBankSet1 16 | jsr changeCHRBanks 17 | .endif 18 | jsr bulkCopyToPpu 19 | .addr wait_palette 20 | jsr copyRleNametableToPpu 21 | .addr legal_nametable 22 | 23 | lda screenStage 24 | cmp #2 25 | bne @justLegal 26 | jsr bulkCopyToPpu 27 | .addr title_nametable_patch 28 | @justLegal: 29 | 30 | jsr waitForVBlankAndEnableNmi 31 | jsr updateAudioWaitForNmiAndResetOamStaging 32 | jsr updateAudioWaitForNmiAndEnablePpuRendering 33 | jsr updateAudioWaitForNmiAndResetOamStaging 34 | 35 | ; if title, skip wait 36 | lda screenStage 37 | cmp #2 38 | beq waitLoopCheckStart 39 | 40 | lda #$FF 41 | ldx palFlag 42 | ; cpx #0 ; ldx sets z flag 43 | beq @notPAL 44 | lda #$CC 45 | @notPAL: 46 | sta sleepCounter 47 | @loop: 48 | ; if second wait, skip render loop 49 | lda screenStage 50 | cmp #1 51 | beq waitLoopCheckStart 52 | 53 | jsr updateAudioWaitForNmiAndResetOamStaging 54 | lda #$1A 55 | sta spriteXOffset 56 | lda #$20 57 | sta spriteYOffset 58 | lda #sleepCounter 59 | sta byteSpriteAddr 60 | lda #0 61 | sta byteSpriteAddr+1 62 | sta byteSpriteTile 63 | lda #1 64 | sta byteSpriteLen 65 | jsr byteSprite 66 | lda sleepCounter 67 | bne @loop 68 | inc screenStage 69 | jmp @justLegal 70 | 71 | waitLoopCheckStart: 72 | lda screenStage 73 | cmp #1 74 | bne @title 75 | lda sleepCounter 76 | beq waitLoopNext 77 | @title: 78 | lda newlyPressedButtons_player1 79 | cmp #BUTTON_START 80 | beq waitLoopNext 81 | jsr updateAudioWaitForNmiAndResetOamStaging 82 | jmp waitLoopCheckStart 83 | waitLoopNext: 84 | ldx #$02 85 | lda screenStage 86 | cmp #2 87 | beq waitLoopContinue 88 | stx soundEffectSlot1Init 89 | inc screenStage 90 | jmp waitScreenLoad 91 | waitLoopContinue: 92 | stx soundEffectSlot1Init 93 | inc gameMode 94 | rts 95 | -------------------------------------------------------------------------------- /src/gamemodestate/branch.asm: -------------------------------------------------------------------------------- 1 | ; the return value of this routine dictates if we should wait for nmi or not right after 2 | 3 | branchOnGameModeState: 4 | lda gameModeState 5 | jsr switch_s_plus_2a 6 | .addr gameModeState_initGameBackground ; gms: 1 acc: 0 - ne 7 | .addr gameModeState_initGameState ; gms: 2 acc: 4/0 - ne 8 | .addr gameModeState_updateCountersAndNonPlayerState ; gms: 3 acc: 0/1 - ne 9 | .addr gameModeState_handleGameOver ; gms: 4 acc: eq (set to $9) if gameOver, $1 otherwise (ne) 10 | .addr gameModeState_updatePlayer1 ; gms: 5 acc: $FF - ne 11 | .addr gameModeState_next ; gms: 6 acc: $1 ne 12 | .addr gameModeState_checkForResetKeyCombo ; gms: 7 acc: 0 or heldButtons - eq if holding down, left and right 13 | .addr gameModeState_handlePause ; gms: 8 acc: 0/3 - ne 14 | .addr gameModeState_vblankThenRunState2 ; gms: 2 acc eq (set to $2) 15 | 16 | gameModeState_next: ; used to be updatePlayer2 17 | inc gameModeState 18 | lda #$1 ; acc should not be equal 19 | rts 20 | 21 | gameModeState_vblankThenRunState2: 22 | lda #$02 23 | sta gameModeState 24 | rts 25 | 26 | .include "initbackground.asm" 27 | .include "initstate.asm" 28 | .include "handlegameover.asm" 29 | .include "updatecounters.asm" 30 | .include "updateplayer1.asm" 31 | .include "checkforabss.asm" 32 | .include "pause.asm" 33 | -------------------------------------------------------------------------------- /src/gamemodestate/checkforabss.asm: -------------------------------------------------------------------------------- 1 | ; A+B+Select+Start 2 | gameModeState_checkForResetKeyCombo: 3 | lda heldButtons_player1 4 | cmp #BUTTON_A+BUTTON_B+BUTTON_START+BUTTON_SELECT 5 | beq @reset 6 | inc gameModeState 7 | ; acc has to be heldButtons_player1 here 8 | rts 9 | 10 | @reset: jsr updateAudio2 11 | lda #$2 ; straight to menu screen 12 | sta gameMode 13 | lda qualFlag 14 | beq @skipLegal 15 | dec gameMode ; gameMode_waitScreen 16 | @skipLegal: 17 | rts 18 | -------------------------------------------------------------------------------- /src/gamemodestate/handlegameover.asm: -------------------------------------------------------------------------------- 1 | gameModeState_handleGameOver: 2 | .if AUTO_WIN 3 | lda newlyPressedButtons_player1 4 | and #BUTTON_SELECT 5 | beq @continue 6 | lda #$0A ; playState_checkStartGameOver 7 | sta playState 8 | jmp @ret 9 | @continue: 10 | .endif 11 | lda #$05 12 | sta generalCounter2 13 | lda playState 14 | ; cmp #$00 ; lda sets z flag 15 | beq @gameOver 16 | jmp @ret 17 | @gameOver: 18 | lda #$03 19 | sta renderMode 20 | jsr handleHighScoreIfNecessary 21 | lda #$01 22 | sta playState 23 | lda #$EF 24 | ldx #$04 25 | ldy #$04 ; used to be 5, but we dont need to clear 2p playfield 26 | jsr memset_page 27 | lda #$00 28 | sta vramRow 29 | lda #$01 30 | sta playState 31 | jsr updateAudioWaitForNmiAndResetOamStaging 32 | ldx #3 ; levelMenu 33 | lda practiseType 34 | cmp #MODE_KILLX2 35 | bne @notGameTypeMenu 36 | dex 37 | @notGameTypeMenu: 38 | stx gameMode 39 | rts 40 | 41 | @ret: inc gameModeState ; 4 42 | lda #$1 ; acc should not be equal (always $1 in original game) 43 | rts 44 | -------------------------------------------------------------------------------- /src/gamemodestate/pause.asm: -------------------------------------------------------------------------------- 1 | gameModeState_handlePause: 2 | lda renderMode 3 | cmp #$03 4 | bne @ret 5 | 6 | lda newlyPressedButtons_player1 7 | and #$10 8 | beq @ret 9 | 10 | @startPressed: 11 | ; do nothing if curtain is being lowered 12 | lda disablePauseFlag 13 | bne @ret 14 | lda playState 15 | cmp #$0A 16 | beq @ret 17 | jsr pause 18 | 19 | @ret: inc gameModeState ; 8 20 | lda #$0 ; acc must not be equal 21 | rts 22 | 23 | pause: 24 | lda #$05 25 | sta musicStagingNoiseHi 26 | 27 | lda qualFlag 28 | beq @pauseSetupNotClassic 29 | 30 | @pauseSetupClassic: 31 | lda #$16 32 | sta PPUMASK 33 | @pauseSetupNotClassic: 34 | lda #$04 ; render_mode_pause 35 | sta renderMode 36 | 37 | @pauseSetupPart2: 38 | jsr updateAudioWaitForNmiAndResetOamStaging 39 | 40 | @pauseLoop: 41 | lda qualFlag 42 | beq @pauseLoopNotClassic 43 | 44 | @pauseLoopClassic: 45 | lda #$70 46 | sta spriteXOffset 47 | lda #$77 48 | sta spriteYOffset 49 | jmp @pauseLoopCommon 50 | 51 | @pauseLoopNotClassic: 52 | lda #PAUSE_SPRITE_X 53 | sta spriteXOffset 54 | lda #PAUSE_SPRITE_Y 55 | sta spriteYOffset 56 | 57 | @pauseLoopCommon: 58 | clc 59 | lda #$A 60 | adc debugFlag 61 | sta spriteIndexInOamContentLookup 62 | jsr stringSprite 63 | 64 | ; block tool hud - X/Y/Piece 65 | lda debugFlag 66 | beq @noDebugHUD 67 | lda #$70 68 | sta spriteXOffset 69 | lda #$60 70 | sta spriteYOffset 71 | lda #tetriminoX 72 | sta byteSpriteAddr 73 | lda #0 74 | sta byteSpriteAddr+1 75 | lda #0 76 | sta byteSpriteTile 77 | lda #3 78 | sta byteSpriteLen 79 | jsr byteSprite 80 | @noDebugHUD: 81 | 82 | lda qualFlag 83 | bne @pauseCheckStart 84 | 85 | jsr practiseGameHUD 86 | jsr debugMode 87 | ; debugMode calls stageSpriteForNextPiece, stageSpriteForCurrentPiece 88 | 89 | @pauseCheckStart: 90 | lda newlyPressedButtons_player1 91 | cmp #$10 92 | beq @resume 93 | jsr updateAudioWaitForNmiAndResetOamStaging 94 | jmp @pauseLoop 95 | 96 | @resume: 97 | lda #$1E 98 | sta PPUMASK 99 | lda #$00 100 | sta musicStagingNoiseHi 101 | sta vramRow 102 | lda #$03 103 | sta renderMode 104 | rts 105 | -------------------------------------------------------------------------------- /src/gamemodestate/updatecounters.asm: -------------------------------------------------------------------------------- 1 | gameModeState_updateCountersAndNonPlayerState: 2 | ; CHR bank used to be reset to 0 here 3 | lda #$00 4 | sta oamStagingLength 5 | inc fallTimer 6 | ; next code makes acc behave as normal 7 | ; (dont edit unless you know what you're doing) 8 | lda newlyPressedButtons_player1 9 | and #BUTTON_SELECT 10 | beq @ret 11 | lda hideNextPiece 12 | eor #$01 13 | sta hideNextPiece 14 | @ret: inc gameModeState ; 3 15 | rts 16 | -------------------------------------------------------------------------------- /src/gamemodestate/updateplayer1.asm: -------------------------------------------------------------------------------- 1 | gameModeState_updatePlayer1: 2 | lda #$04 3 | sta playfieldAddr+1 4 | ; copy controller from mirror 5 | lda newlyPressedButtons_player1 6 | sta newlyPressedButtons 7 | lda heldButtons_player1 8 | sta heldButtons 9 | 10 | jsr checkDebugGameplay 11 | jsr practiseAdvanceGame 12 | jsr practiseGameHUD 13 | jsr branchOnPlayStatePlayer1 14 | jsr stageSpriteForCurrentPiece 15 | jsr stageSpriteForNextPiece 16 | 17 | inc gameModeState ; 5 18 | lda #$FF ; acc from stateSpriteForNextPiece 19 | rts 20 | -------------------------------------------------------------------------------- /src/header.asm: -------------------------------------------------------------------------------- 1 | ; 2 | ; iNES header 3 | ; 4 | 5 | ; This iNES header is from Brad Smith (rainwarrior) 6 | ; https://github.com/bbbradsmith/NES-ca65-example 7 | 8 | .segment "HEADER" 9 | 10 | .include "constants.asm" ; for INES_HEADER 11 | 12 | INES_MIRROR = 0 ; 0 = horizontal mirroring, 1 = vertical mirroring (ignored in MMC1) 13 | INES_SRAM = 1 ; 1 = battery backed SRAM at $6000-7FFF 14 | 15 | ; Override INES_MAPPER for mode 1000 (auto detect) 16 | .if INES_MAPPER = 1000 17 | .if CNROM_OVERRIDE 18 | _INES_MAPPER = 3 ; Test CNROM on Emulator/Flashcart 19 | .else 20 | _INES_MAPPER = 1 ; MMC1 for Emulator/Flashcart 21 | .endif 22 | .else 23 | _INES_MAPPER = INES_MAPPER ; use actual INES_MAPPER otherwise 24 | .endif 25 | 26 | .byte 'N', 'E', 'S', $1A ; ID 27 | .byte $02 ; 16k PRG chunk count 28 | .byte $02 ; 8k CHR chunk count 29 | .byte INES_MIRROR | (INES_SRAM << 1) | ((_INES_MAPPER & $f) << 4) 30 | .byte (_INES_MAPPER & %11110000) 31 | .byte $0, $0, $0, $0, $0, $0, $0, $0 ; padding 32 | -------------------------------------------------------------------------------- /src/highscores/data.asm: -------------------------------------------------------------------------------- 1 | highScorePpuAddrTable: 2 | .dbyt $2284,$22C4,$2304 3 | highScoreCharToTile: 4 | .byte $FF,$0A,$0B,$0C,$0D,$0E,$0F,$10 5 | .byte $11,$12,$13,$14,$15,$16,$17,$18 6 | .byte $19,$1A,$1B,$1C,$1D,$1E,$1F,$20 7 | .byte $21,$22,$23,$00,$01,$02,$03,$04 8 | .byte $05,$06,$07,$08,$09,$25,$4F,$5E 9 | .byte $5F,$6E,$6F,$52,$55,$24 10 | highScoreCharSize := $2E 11 | -------------------------------------------------------------------------------- /src/highscores/render_menu.asm: -------------------------------------------------------------------------------- 1 | showHighScores: 2 | ldy #0 3 | lda #0 4 | sta generalCounter2 5 | @copyEntry: 6 | lda generalCounter2 7 | asl a 8 | tax 9 | lda highScorePpuAddrTable,x 10 | sta PPUADDR 11 | inx 12 | lda highScorePpuAddrTable,x 13 | sta PPUADDR 14 | 15 | ; name 16 | ldx #highScoreNameLength 17 | @copyChar: 18 | lda highscores,y 19 | sty generalCounter 20 | tay 21 | lda highScoreCharToTile,y 22 | ldy generalCounter 23 | sta PPUDATA 24 | iny 25 | dex 26 | bne @copyChar 27 | 28 | lda #$FF 29 | sta PPUDATA 30 | 31 | lda #0 ; 8 digit flag 32 | sta tmpZ 33 | 34 | ; score 35 | lda highscores,y 36 | cmp #$A 37 | bmi @scoreHighWrite 38 | jsr twoDigsToPPU 39 | lda #1 40 | sta tmpZ 41 | jmp @scoreEnd 42 | @scoreHighWrite: 43 | sta PPUDATA 44 | @scoreEnd: 45 | iny 46 | lda highscores,y 47 | jsr twoDigsToPPU 48 | iny 49 | lda highscores,y 50 | jsr twoDigsToPPU 51 | iny 52 | lda highscores,y 53 | jsr twoDigsToPPU 54 | iny 55 | 56 | lda #$FF 57 | sta PPUDATA 58 | 59 | ; lines 60 | lda highscores,y 61 | sta PPUDATA 62 | iny 63 | lda highscores,y 64 | jsr twoDigsToPPU 65 | iny 66 | 67 | lda #$FF 68 | sta PPUDATA 69 | 70 | ; levels 71 | lda tmpZ 72 | beq @normalLevel 73 | lda highscores,y 74 | cmp #100 75 | bpl @normalLevel 76 | tax 77 | lda byteToBcdTable, x 78 | jsr twoDigsToPPU 79 | jmp @levelContinue 80 | @normalLevel: 81 | lda highscores,y ; startlevel 82 | jsr renderByteBCD 83 | @levelContinue: 84 | iny 85 | 86 | ; update PPUADDR for start level 87 | lda generalCounter2 88 | asl a 89 | tax 90 | lda highScorePpuAddrTable,x 91 | sta PPUADDR 92 | inx 93 | lda highScorePpuAddrTable,x 94 | adc #$35 95 | sta PPUADDR 96 | 97 | ; level 98 | lda highscores,y 99 | jsr renderByteBCD 100 | iny 101 | 102 | inc generalCounter2 103 | lda generalCounter2 104 | cmp #highScoreQuantity 105 | beq showHighScores_ret 106 | jmp @copyEntry 107 | 108 | showHighScores_ret: rts 109 | -------------------------------------------------------------------------------- /src/highscores/util.asm: -------------------------------------------------------------------------------- 1 | resetScores: 2 | ldx #$0 3 | lda #$0 4 | @initHighScoreTable: 5 | cpx #highScoreLength * highScoreQuantity 6 | beq @continue 7 | sta highscores,x 8 | inx 9 | jmp @initHighScoreTable 10 | @continue: 11 | rts 12 | 13 | .if SAVE_HIGHSCORES 14 | detectSRAM: 15 | lda #$37 16 | sta SRAM_hsMagic 17 | lda #$64 18 | sta SRAM_hsMagic+1 19 | lda SRAM_hsMagic 20 | cmp #$37 21 | bne @noSRAM 22 | lda SRAM_hsMagic+1 23 | cmp #$64 24 | bne @noSRAM 25 | lda #1 26 | rts 27 | @noSRAM: 28 | lda #0 29 | rts 30 | 31 | checkSavedInit: 32 | lda SRAM_hsMagic+2 33 | cmp #$4B 34 | bne resetSavedScores 35 | lda SRAM_hsMagic+3 36 | cmp #$D2 37 | bne resetSavedScores 38 | rts 39 | 40 | resetSavedScores: 41 | lda #$4B 42 | sta SRAM_hsMagic+2 43 | lda #$D2 44 | sta SRAM_hsMagic+3 45 | 46 | ldx #$0 47 | lda #$0 48 | @copyLoop: 49 | cpx #highScoreLength * highScoreQuantity 50 | beq @continue 51 | sta SRAM_highscores,x 52 | inx 53 | jmp @copyLoop 54 | @continue: 55 | rts 56 | 57 | copyScoresFromSRAM: 58 | ldx #$0 59 | @copyLoop: 60 | cpx #highScoreLength * highScoreQuantity 61 | beq @continue 62 | lda SRAM_highscores,x 63 | sta highscores,x 64 | inx 65 | jmp @copyLoop 66 | @continue: 67 | rts 68 | 69 | copyScoresToSRAM: 70 | ldx #$0 71 | @copyLoop: 72 | cpx #highScoreLength * highScoreQuantity 73 | beq @continue 74 | lda highscores,x 75 | sta SRAM_highscores,x 76 | inx 77 | jmp @copyLoop 78 | @continue: 79 | rts 80 | 81 | .endif 82 | -------------------------------------------------------------------------------- /src/io.asm: -------------------------------------------------------------------------------- 1 | SRAM := $6000 ; 8kb 2 | SRAM_states := SRAM 3 | SRAM_hsMagic := SRAM+$A00 4 | SRAM_highscores := SRAM_hsMagic+$4 5 | 6 | PPUCTRL := $2000 7 | PPUMASK := $2001 8 | PPUSTATUS := $2002 9 | OAMADDR := $2003 10 | OAMDATA := $2004 11 | PPUSCROLL := $2005 12 | PPUADDR := $2006 13 | PPUDATA := $2007 14 | SQ1_VOL := $4000 15 | SQ1_SWEEP := $4001 16 | SQ1_LO := $4002 17 | SQ1_HI := $4003 18 | SQ2_VOL := $4004 19 | SQ2_SWEEP := $4005 20 | SQ2_LO := $4006 21 | SQ2_HI := $4007 22 | TRI_LINEAR := $4008 23 | TRI_LO := $400A 24 | TRI_HI := $400B 25 | NOISE_VOL := $400C 26 | NOISE_LO := $400E 27 | NOISE_HI := $400F 28 | DMC_FREQ := $4010 29 | DMC_RAW := $4011 30 | DMC_START := $4012 ; start << 6 + $C000 31 | DMC_LEN := $4013 ; len << 4 + 1 32 | OAMDMA := $4014 33 | SND_CHN := $4015 34 | JOY1 := $4016 35 | JOY2_APUFC := $4017 ; read: bits 0-4 joy data lines (bit 0 being normal controller), bits 6-7 are FC inhibit and mode 36 | 37 | ; Used by Family Basic Keyboard 38 | .if KEYBOARD 39 | KB_INIT := $05 40 | KB_COL_0 := $04 41 | KB_COL_1 := $06 42 | KB_MASK := $1E 43 | .endif 44 | 45 | MMC1_Control := $8000 46 | MMC1_CHR0 := $BFFF 47 | MMC1_CHR1 := $DFFF 48 | MMC1_PRG := $FFFF 49 | 50 | MMC3_BANK_SELECT := $8000 51 | MMC3_BANK_DATA := $8001 52 | MMC3_MIRRORING := $A000 53 | MMC3_PRG_RAM := $A001 54 | 55 | ; https://www.nesdev.org/wiki/MMC5#Configuration 56 | MMC5_PRG_MODE := $5100 57 | MMC5_CHR_MODE := $5101 58 | MMC5_RAM_PROTECT1 := $5102 59 | MMC5_RAM_PROTECT2 := $5103 60 | MMC5_NT_MAPPING := $5105 ; $50 horizontal, $44 vertical, $00 single 61 | MMC5_CHR_BANK0 := $5123 ; 4kb page index 62 | MMC5_CHR_BANK1 := $5127 63 | 64 | .macro RESET_MMC1 65 | .if INES_MAPPER = 1 .or INES_MAPPER = 1000 66 | : inc :- ; increments inc ($aa), writing a negative value to prg 67 | ; https://www.nesdev.org/wiki/MMC1#Reset 68 | .endif 69 | .endmacro 70 | 71 | NMIEnable = $80 72 | BGPattern1 = $10 73 | SpritePattern1 = $08 74 | 75 | CHRBankSet0 = $00 76 | CHRBankSet1 = $02 77 | -------------------------------------------------------------------------------- /src/main.asm: -------------------------------------------------------------------------------- 1 | ; _____ _ _ _____ __ __ _____ 2 | ; |_ _|___| |_ ___|_|___| __| | | | 3 | ; | | | -_| _| _| |_ -| | |_ _| | | | 4 | ; |_| |___|_| |_| |_|___|_____| |_| |_|_|_| 5 | ; 6 | ; TetrisGYM - A Tetris Practise ROM 7 | 8 | .include "charmap.asm" 9 | .include "constants.asm" 10 | .include "io.asm" 11 | .include "ram.asm" 12 | .include "chr.asm" 13 | 14 | .setcpu "6502" 15 | .feature force_range 16 | 17 | .segment "PRG_chunk1": absolute 18 | 19 | ; region code at start of page to keep cycle count consistent 20 | .include "util/check_region.asm" 21 | .include "audio.asm" 22 | 23 | initRam: 24 | 25 | .include "boot.asm" 26 | 27 | mainLoop: 28 | jsr branchOnGameMode 29 | cmp gameModeState 30 | bne @continue 31 | jsr updateAudioWaitForNmiAndResetOamStaging 32 | @continue: 33 | jmp mainLoop 34 | 35 | .include "nmi/nmi.asm" 36 | .include "nmi/render.asm" 37 | .include "nmi/pollcontroller.asm" 38 | .if KEYBOARD 39 | .include "nmi/pollkeyboard.asm" 40 | .endif 41 | 42 | .include "gamemode/branch.asm" 43 | ; -> playAndEnding 44 | .include "gamemodestate/branch.asm" 45 | ; -> updatePlayer1 46 | .include "playstate/branch.asm" 47 | 48 | .include "highscores/data.asm" 49 | .include "highscores/util.asm" 50 | .include "highscores/render_menu.asm" 51 | .include "highscores/entry_screen.asm" 52 | 53 | .include "util/core.asm" 54 | .include "util/strings.asm" 55 | .include "util/math.asm" 56 | .include "util/menuthrottle.asm" 57 | .include "util/modetext.asm" 58 | .include "util/mapper.asm" 59 | .if INES_MAPPER = 1000 60 | .include "util/autodetect.asm" 61 | .endif 62 | 63 | .include "sprites/bytesprite.asm" 64 | .include "sprites/drawrect.asm" 65 | .include "sprites/loadsprite.asm" 66 | .include "sprites/piece.asm" 67 | 68 | .include "data/bytebcd.asm" 69 | .include "data/orientation.asm" 70 | .include "data/mult.asm" 71 | 72 | .include "palettes.asm" 73 | .include "nametables.asm" 74 | .include "presets/presets.asm" 75 | 76 | ; the modes/ folder contains large supplimentary routines used elsewhere 77 | ; the full mode code can be found by globally searching `MODE_` for each 78 | 79 | .include "modes/hz.asm" 80 | .include "modes/pace.asm" 81 | .include "modes/debug.asm" 82 | .include "modes/saveslots.asm" 83 | .include "modes/crash.asm" 84 | .include "modes/events.asm" 85 | .include "modes/controllerinput.asm" 86 | .include "modes/tapqty.asm" 87 | .include "modes/initchecker.asm" 88 | .include "modes/tspins.asm" 89 | .include "modes/parity.asm" 90 | .include "modes/preset.asm" 91 | .include "modes/floor.asm" 92 | .include "modes/crunch.asm" 93 | .include "modes/qtap.asm" 94 | .include "modes/garbage.asm" 95 | 96 | .code 97 | 98 | .segment "PRG_chunk3": absolute 99 | 100 | .include "reset.asm" 101 | 102 | .code 103 | 104 | .segment "VECTORS": absolute 105 | 106 | .addr nmi 107 | .addr reset 108 | .addr irq 109 | 110 | .code 111 | -------------------------------------------------------------------------------- /src/modes/controllerinput.asm: -------------------------------------------------------------------------------- 1 | controllerInputTiles: 2 | ; .byte "RLDUSSBA" 3 | .byte $D0, $D1, $D2, $D3 4 | .byte $D4, $D4, $D5, $D5 5 | controllerInputX: 6 | .byte $9, $0, $5, $5 7 | .byte $1D, $14, $28, $31 8 | controllerInputY: 9 | .byte $0, $0, $5, $FB 10 | .byte $0, $0, $FF, $FF 11 | 12 | controllerInputDisplay: ; called in events, speedtest 13 | lda #0 14 | sta tmp3 15 | controllerInputDisplayX: 16 | lda heldButtons_player1 17 | sta tmp1 18 | ldy #0 19 | @inputLoop: 20 | lda tmp1 21 | and #1 22 | beq @inputContinue 23 | ldx oamStagingLength 24 | clc 25 | lda controllerInputY, y 26 | adc #$4C 27 | sta oamStaging, x 28 | inx 29 | lda controllerInputTiles, y 30 | sta oamStaging, x 31 | inx 32 | lda #$01 33 | sta oamStaging, x 34 | inx 35 | lda controllerInputX, y 36 | clc 37 | adc #$13 38 | adc tmp3 39 | sta oamStaging, x 40 | inx 41 | ; increase OAM index 42 | lda #$04 43 | clc 44 | adc oamStagingLength 45 | sta oamStagingLength 46 | @inputContinue: 47 | lda tmp1 48 | ror 49 | sta tmp1 50 | iny 51 | cpy #8 52 | bmi @inputLoop 53 | rts 54 | -------------------------------------------------------------------------------- /src/modes/crunch.asm: -------------------------------------------------------------------------------- 1 | ; crunch start variants: 2 | ; 0 - 0 left 0 right 3 | ; 1 - 0 left 1 right 4 | ; 2 - 0 left 2 right 5 | ; 3 - 0 left 3 right 6 | ; 4 - 1 left 0 right 7 | ; 5 - 1 left 1 right 8 | ; 6 - 1 left 2 right 9 | ; 7 - 1 left 3 right 10 | ; 8 - 2 left 0 right 11 | ; 9 - 2 left 1 right 12 | ; A - 2 left 2 right 13 | ; B - 2 left 3 right 14 | ; C - 3 left 0 right 15 | ; D - 3 left 1 right 16 | ; E - 3 left 2 right 17 | ; F - 3 left 3 right 18 | 19 | ; clobbers generalCounter3 & generalCounter4 (defined in playstate/util.asm) 20 | 21 | advanceGameCrunch: 22 | ; initialize playfield row 19 to 0 23 | ldx #$13 24 | @nextRow: 25 | lda multBy10Table,x 26 | sta playfieldAddr ; restored to 0 at end of loop 27 | jsr advanceSides 28 | dex 29 | bpl @nextRow 30 | inx ; x is FF, increase to store 0 in vramRow 31 | stx vramRow 32 | crunchReturn: 33 | rts 34 | 35 | advanceSides: 36 | ; called in playState_checkForCompletedRows and in advanceGameCrunch 37 | ; draws to row defined in playfieldAddr, which defaults to 0 38 | jsr unpackCrunchModifier 39 | 40 | lda #BLOCK_TILES 41 | 42 | ldy #$0 43 | @leftLoop: 44 | dec crunchLeftColumns 45 | bmi @initRight 46 | sta (playfieldAddr),y 47 | iny 48 | bpl @leftLoop ; unconditional 49 | 50 | @initRight: 51 | ldy #$9 52 | @rightLoop: 53 | dec crunchRightColumns 54 | bmi crunchReturn 55 | sta (playfieldAddr),y 56 | dey 57 | bpl @rightLoop ; unconditional 58 | 59 | 60 | unpackCrunchModifier: 61 | lda crunchModifier 62 | lsr 63 | lsr 64 | sta crunchLeftColumns ; generalCounter3 65 | lda crunchModifier 66 | and #$03 67 | sta crunchRightColumns ; generalCounter4 68 | rts 69 | -------------------------------------------------------------------------------- /src/modes/events.asm: -------------------------------------------------------------------------------- 1 | practiseInitGameState: 2 | lda practiseType 3 | cmp #MODE_CHECKERBOARD 4 | bne @skipChecker 5 | jsr initChecker 6 | @skipChecker: 7 | jsr practiseEachPiece 8 | cmp #MODE_FLOOR 9 | bne @skipFloor 10 | jmp advanceGameFloor 11 | @skipFloor: 12 | lda practiseType 13 | cmp #MODE_CRUNCH 14 | bne @skipCrunch 15 | jsr advanceGameCrunch 16 | @skipCrunch: 17 | rts 18 | 19 | practisePrepareNext: 20 | lda practiseType 21 | cmp #MODE_PACE 22 | bne @skipPace 23 | jmp prepareNextPace 24 | @skipPace: 25 | cmp #MODE_GARBAGE 26 | bne @skipGarbo 27 | jmp prepareNextGarbage 28 | @skipGarbo: 29 | cmp #MODE_PARITY 30 | bne @skipParity 31 | jmp prepareNextParity 32 | @skipParity: 33 | jsr practiseEachPiece 34 | rts 35 | 36 | practiseAdvanceGame: 37 | lda practiseType 38 | cmp #MODE_TSPINS 39 | bne @skipTSpins 40 | jmp advanceGameTSpins 41 | @skipTSpins: 42 | rts 43 | 44 | practiseEachPiece: ; only used in this file 45 | cmp #MODE_TAPQTY 46 | bne @skipTapQuantity 47 | jsr prepareNextTapQuantity 48 | @skipTapQuantity: 49 | cmp #MODE_TAP 50 | bne @skipTap 51 | jmp advanceGameTap 52 | @skipTap: 53 | cmp #MODE_PRESETS 54 | bne @skipPresets 55 | jmp advanceGamePreset 56 | @skipPresets: 57 | rts 58 | 59 | practiseGameHUD: 60 | lda inputDisplayFlag 61 | beq @noInput 62 | jsr controllerInputDisplay 63 | @noInput: 64 | 65 | lda practiseType 66 | cmp #MODE_PACE 67 | bne @skipPace 68 | jsr gameHUDPace 69 | @skipPace: 70 | 71 | lda practiseType 72 | cmp #MODE_TAPQTY 73 | bne @skipTapQuantity 74 | 75 | ldy #0 76 | ldx oamStagingLength 77 | @drawQTY: 78 | ; taps 79 | tya 80 | asl 81 | asl 82 | asl 83 | adc #$34 84 | sta tmpY 85 | sta oamStaging, x 86 | inx 87 | lda tqtyCurrent, y 88 | cmp #5 89 | bmi @right0 90 | sbc #5 91 | jmp @left0 92 | @right0: 93 | lda #6 94 | sbc tqtyCurrent, y 95 | @left0: 96 | sta oamStaging, x 97 | inx 98 | lda #$02 99 | sta oamStaging, x 100 | inx 101 | lda #$64 102 | sta oamStaging, x 103 | inx 104 | 105 | ; direction 106 | lda tmpY 107 | sta oamStaging, x 108 | inx 109 | 110 | lda tqtyCurrent, y 111 | cmp #6 112 | bmi @right 113 | lda #$D6 114 | jmp @left 115 | @right: 116 | lda #$D7 117 | @left: 118 | sta oamStaging, x 119 | inx 120 | lda #$02 121 | sta oamStaging, x 122 | inx 123 | lda #$6E 124 | sta oamStaging, x 125 | inx 126 | 127 | ; $D6 / D7 for direction 128 | ; increase OAM index 129 | lda #$08 130 | clc 131 | adc oamStagingLength 132 | sta oamStagingLength 133 | iny 134 | cpy #2 135 | bmi @drawQTY 136 | 137 | @skipTapQuantity: 138 | rts 139 | -------------------------------------------------------------------------------- /src/modes/floor.asm: -------------------------------------------------------------------------------- 1 | advanceGameFloor: 2 | lda currentFloor 3 | drawFloor: 4 | ; get correct offset 5 | sta tmp1 6 | lda #$D 7 | sec 8 | sbc tmp1 9 | tax 10 | ; x10 11 | lda multBy10Table, x 12 | tax 13 | ; draw block tiles+3 ($7E) 14 | lda #BLOCK_TILES+3 15 | @loop: 16 | sta playfield+$46,X 17 | inx 18 | cpx #$82 19 | bmi @loop 20 | @skip: 21 | rts 22 | -------------------------------------------------------------------------------- /src/modes/garbage.asm: -------------------------------------------------------------------------------- 1 | prepareNextGarbage: 2 | lda garbageModifier 3 | jsr switch_s_plus_2a 4 | .addr garbageAlwaysTetrisReady 5 | .addr garbageNormal 6 | .addr garbageSmart 7 | .addr garbageHard 8 | .addr garbageTypeC ; infinite dig 9 | 10 | garbageTypeC: 11 | jsr findTopBulky 12 | adc #$20 ; offset from starting position 13 | @loop: 14 | sta tmp3 15 | 16 | jsr random10 17 | adc tmp3 18 | tax 19 | jsr swapMino 20 | txa 21 | 22 | sta tmp3 23 | cmp #$c0 24 | bcc @loop 25 | rts 26 | 27 | findTopBulky: 28 | lda #$0 29 | @loop: 30 | sta tmp3 ; line 31 | 32 | tax 33 | lda #0 34 | sta tmp2 ; line block qty 35 | ldy #9 36 | @loopLine: 37 | lda playfield, x 38 | cmp #EMPTY_TILE 39 | beq @noBlock 40 | inc tmp2 41 | @noBlock: 42 | inx 43 | dey 44 | bne @loopLine 45 | lda tmp2 46 | cmp #4 ; requirement 47 | bpl @done 48 | 49 | lda tmp3 50 | adc #$A 51 | cmp #$b8 52 | bcc @loop 53 | @done: 54 | txa 55 | rts 56 | 57 | swapMino: 58 | ldy #EMPTY_TILE 59 | lda playfield, x 60 | cmp #EMPTY_TILE 61 | bne @full 62 | ldy #BLOCK_TILES+3 63 | @full: 64 | tya 65 | sta playfield, x 66 | rts 67 | 68 | garbageNormal: 69 | jsr randomHole 70 | jsr randomGarbage 71 | rts 72 | 73 | garbageSmart: 74 | jsr smartHole 75 | jsr randomGarbage 76 | rts 77 | 78 | findTop: 79 | ldx #$0 80 | @loop: 81 | lda playfield, x 82 | cmp #EMPTY_TILE 83 | bne @done 84 | inx 85 | cpx #$b8 86 | bcc @loop 87 | @done: 88 | rts 89 | 90 | randomGarbage: 91 | jsr findTop 92 | cpx #130 93 | bcc @done 94 | 95 | lda garbageDelay 96 | ; cmp #0 ; lda sets z flag 97 | bne @delay 98 | 99 | jsr random10 100 | and #3 101 | sta pendingGarbage 102 | jsr random10 103 | and #$7 104 | adc #$2+1 105 | sta garbageDelay 106 | @delay: 107 | dec garbageDelay 108 | @done: 109 | rts 110 | 111 | garbageHard: 112 | jsr findTop 113 | cpx #100 114 | bcc @nothing 115 | 116 | lda spawnCount 117 | and #1 118 | bne @nothing 119 | jsr randomHole 120 | inc pendingGarbage 121 | @nothing: 122 | rts 123 | 124 | smartHole: 125 | ldx #199 126 | @loop: 127 | lda playfield, x 128 | cmp #EMPTY_TILE 129 | beq @done 130 | dex 131 | cpx #190 132 | bcs @loop 133 | @done: 134 | txa 135 | sbc #190 136 | sta garbageHole 137 | rts 138 | 139 | randomHole: 140 | jsr random10 141 | sta garbageHole 142 | rts 143 | 144 | garbageAlwaysTetrisReady: 145 | ; right well 146 | lda #9 147 | sta garbageHole 148 | 149 | lda #0 150 | sta tmp1 ; garbage to add 151 | 152 | ldx #190 153 | jsr checkTetrisReady 154 | ldx #180 155 | jsr checkTetrisReady 156 | ldx #170 157 | jsr checkTetrisReady 158 | ldx #160 159 | jsr checkTetrisReady 160 | 161 | lda tmp1 162 | sta pendingGarbage 163 | rts 164 | 165 | checkTetrisReady: 166 | ldy #9 167 | @loop: 168 | lda playfield, x 169 | cmp #EMPTY_TILE 170 | bne @filled 171 | inc tmp1 ; add garbage 172 | ldy #1 173 | @filled: 174 | inx 175 | dey 176 | bne @loop 177 | rts 178 | -------------------------------------------------------------------------------- /src/modes/initchecker.asm: -------------------------------------------------------------------------------- 1 | initChecker: 2 | CHECKERBOARD_TILE := BLOCK_TILES 3 | CHECKERBOARD_FLIP := CHECKERBOARD_TILE ^ EMPTY_TILE 4 | lda #0 5 | sta vramRow 6 | ldx checkerModifier 7 | lda typeBBlankInitCountByHeightTable, x 8 | tax 9 | cpx #$C8 ; edge case for height 0 10 | bne @notZero 11 | ldx #$BE 12 | @notZero: 13 | lda frameCounter 14 | and #1 15 | beq @checkerStartA 16 | lda #CHECKERBOARD_TILE 17 | bne @checkerStart 18 | @checkerStartA: 19 | lda #EMPTY_TILE 20 | @checkerStart: 21 | ; hydrantdude found the short way to do this 22 | ldy #$B 23 | @loop: 24 | dey 25 | bne @notA 26 | eor #CHECKERBOARD_FLIP 27 | ldy #$A 28 | @notA: sta playfield, x 29 | eor #CHECKERBOARD_FLIP 30 | inx 31 | cpx #$C8 32 | bcc @loop 33 | rts 34 | -------------------------------------------------------------------------------- /src/modes/parity.asm: -------------------------------------------------------------------------------- 1 | ; stacking mode was called 'parity mode' initially 2 | 3 | prepareNextParity: 4 | ; stacking highlights 5 | 6 | ; 1 red 1+ white 7 | ; skip the first one 8 | ; 1 gap inbetween make the others red 9 | ; gap between wall and stack (left only) 10 | ; overhangs 11 | 12 | ldx #$7C 13 | lda levelNumber 14 | cmp #19 15 | bne @altColor 16 | inx 17 | @altColor: 18 | stx parityColor 19 | 20 | ; change everything to 7B 21 | ldx #$C8 22 | lda #$7B 23 | @loop: 24 | ldy playfield, x 25 | cpy #EMPTY_TILE 26 | beq @empty 27 | sta playfield, x 28 | @empty: 29 | dex 30 | bne @loop 31 | 32 | ; mark things with parityColor 33 | 34 | lda #190 35 | sta parityIndex 36 | @runLine: 37 | jsr highlightParity 38 | lda parityIndex 39 | sec 40 | sbc #10 41 | sta parityIndex 42 | cmp #30 43 | bcs @runLine 44 | rts 45 | 46 | highlightParity: 47 | jsr highlightOrphans 48 | jsr highlightGaps 49 | rts 50 | 51 | highlightGaps: 52 | ldx parityIndex 53 | 54 | highlistGapsLeft: 55 | ; check first gap 56 | lda playfield, x 57 | cmp #EMPTY_TILE 58 | bne @startGapEnd 59 | lda playfield+1, x 60 | cmp #EMPTY_TILE 61 | beq @startGapEnd 62 | lda parityColor 63 | sta playfield+1, x 64 | @startGapEnd: 65 | 66 | highlightGapsOverhang: 67 | ldy #10 68 | 69 | @checkHang: 70 | lda playfield, x 71 | cmp #EMPTY_TILE 72 | bne @checkGroup 73 | lda playfield-10, x 74 | cmp #EMPTY_TILE 75 | beq @checkGroup 76 | 77 | ; draw in red 78 | lda parityColor 79 | sta playfield-10, x 80 | 81 | @checkGroup: 82 | cpy #3 ; you want the first 8 83 | bmi @groupNext 84 | ; horizontal 85 | lda playfield, x 86 | cmp #EMPTY_TILE 87 | beq @groupNext 88 | lda playfield+1, x 89 | cmp #EMPTY_TILE 90 | bne @groupNext 91 | lda playfield+2, x 92 | cmp #EMPTY_TILE 93 | beq @groupNext 94 | 95 | ; draw in red 96 | lda parityColor 97 | sta playfield, x 98 | sta playfield+2, x 99 | 100 | @groupNext: 101 | inx 102 | dey 103 | bne @checkHang 104 | 105 | rts 106 | 107 | highlightOrphans: 108 | ldx parityIndex 109 | ; reset stuff 110 | lda #0 111 | sta parityCount 112 | ldy #10 113 | 114 | @checkString: 115 | lda playfield, x 116 | cmp #EMPTY_TILE 117 | beq @stringEmpty 118 | inc parityCount 119 | jmp @stringNext 120 | @stringEmpty: 121 | lda parityCount 122 | cmp #1 123 | bne @resetCount 124 | ; dont highlight the first one 125 | cpy #9 126 | beq @resetCount 127 | ; last is skipped anyway 128 | lda parityColor 129 | sta playfield-1, x 130 | 131 | @resetCount: 132 | lda #0 133 | sta parityCount 134 | jmp @stringNext 135 | 136 | @stringNext: 137 | inx 138 | dey 139 | bne @checkString 140 | rts 141 | -------------------------------------------------------------------------------- /src/modes/preset.asm: -------------------------------------------------------------------------------- 1 | advanceGamePreset: 2 | jsr clearPlayfield 3 | ; render layout 4 | ldx #0 5 | stx generalCounter 6 | @drawNext: 7 | ; get layout offset 8 | ldy presetModifier 9 | lda presets, y 10 | 11 | ; add index 12 | adc generalCounter 13 | 14 | ; load byte from layout 15 | tax 16 | ldy presets, x 17 | 18 | ; check if finished 19 | cpy #$FF 20 | beq @skip 21 | 22 | ; draw from y 23 | lda #$7B 24 | sta $0400, y 25 | 26 | ; loop 27 | inc generalCounter 28 | jmp @drawNext 29 | @skip: 30 | rts 31 | -------------------------------------------------------------------------------- /src/modes/qtap.asm: -------------------------------------------------------------------------------- 1 | advanceGameTap: 2 | jsr clearPlayfield 3 | ldx tapModifier 4 | ; cpx #0 ; ldx sets z flag 5 | beq @skip ; skip if zero 6 | ldy #$BF ; left side 7 | cpx #$11 8 | bmi @loop 9 | ldy #$C6 ; right side 10 | txa 11 | sbc #$10 12 | tax 13 | 14 | @loop: 15 | lda #$7B 16 | sta $400, y 17 | ; add 10 to y 18 | tya 19 | sec ;important 20 | sbc #$A 21 | tay 22 | dex 23 | bne @loop 24 | @skip: 25 | rts 26 | -------------------------------------------------------------------------------- /src/modes/saveslots.asm: -------------------------------------------------------------------------------- 1 | SLOT_SIZE := $100 ; ~$CC used, the rest free 2 | 3 | saveslots: 4 | .word SRAM 5 | .word SRAM+SLOT_SIZE 6 | .word SRAM+(SLOT_SIZE*2) 7 | .word SRAM+(SLOT_SIZE*3) 8 | .word SRAM+(SLOT_SIZE*4) 9 | .word SRAM+(SLOT_SIZE*5) 10 | .word SRAM+(SLOT_SIZE*6) 11 | .word SRAM+(SLOT_SIZE*7) 12 | .word SRAM+(SLOT_SIZE*8) 13 | .word SRAM+(SLOT_SIZE*9) 14 | 15 | getSlotPointer: 16 | lda saveStateSlot 17 | asl 18 | tax 19 | lda saveslots,x 20 | sta pointerAddr 21 | lda saveslots+1,x 22 | sta pointerAddr+1 23 | rts 24 | 25 | saveState: 26 | jsr getSlotPointer 27 | 28 | ldy #0 29 | @copy: 30 | lda playfield,y 31 | sta (pointerAddr), y 32 | iny 33 | cpy #$c8 34 | bcc @copy 35 | 36 | lda tetriminoX 37 | sta (pointerAddr), y 38 | iny 39 | lda tetriminoY 40 | sta (pointerAddr), y 41 | iny 42 | lda currentPiece 43 | sta (pointerAddr), y 44 | iny 45 | lda nextPiece 46 | sta (pointerAddr), y 47 | 48 | ; level/lines/score 49 | ; iny 50 | ; lda levelNumber 51 | ; jsr saveSlot 52 | ; iny 53 | ; lda lines 54 | ; jsr saveSlot 55 | ; iny 56 | ; lda score 57 | ; jsr saveSlot 58 | ; iny 59 | ; lda score+1 60 | ; jsr saveSlot 61 | ; iny 62 | ; lda score+2 63 | ; jsr saveSlot 64 | 65 | 66 | lda #$17 67 | sta saveStateSpriteType 68 | lda #$20 69 | sta saveStateSpriteDelay 70 | rts 71 | 72 | loadState: 73 | jsr getSlotPointer 74 | 75 | ldy #0 76 | @copy: 77 | lda (pointerAddr), y 78 | sta playfield,y 79 | iny 80 | cpy #$c8 81 | bcc @copy 82 | 83 | lda (pointerAddr), y 84 | sta tetriminoX 85 | iny 86 | lda (pointerAddr), y 87 | sta tetriminoY 88 | iny 89 | lda (pointerAddr), y 90 | sta currentPiece 91 | iny 92 | lda (pointerAddr), y 93 | sta nextPiece 94 | 95 | ; level/lines/score 96 | ; iny 97 | ; jsr loadSlot 98 | ; sta levelNumber 99 | ; iny 100 | ; jsr loadSlot 101 | ; sta lines 102 | ; iny 103 | ; jsr loadSlot 104 | ; sta score 105 | ; iny 106 | ; jsr loadSlot 107 | ; sta score+1 108 | ; iny 109 | ; jsr loadSlot 110 | ; sta score+2 111 | ; ; mark for update 112 | ; lda #7 113 | ; sta renderFlags 114 | 115 | lda #$18 116 | sta saveStateSpriteType 117 | lda #$20 118 | sta saveStateSpriteDelay 119 | @done: 120 | rts 121 | -------------------------------------------------------------------------------- /src/modes/tapqty.asm: -------------------------------------------------------------------------------- 1 | prepareNextTapQuantity: 2 | ; patched also in in @updatePlayfieldComplete 3 | @checkEqual: 4 | lda tqtyNext 5 | cmp tqtyCurrent 6 | bne @notEqual 7 | jsr random10 8 | sta tqtyNext 9 | jmp @checkEqual 10 | @notEqual: 11 | 12 | ; playfield 13 | sec 14 | lda tapqtyModifier 15 | and #$F 16 | tax 17 | ; cpx #0 ; tax sets z flag 18 | bne @notZero 19 | ldx #4 ; default to four 20 | @notZero: 21 | lda multBy10Table, x 22 | sta tmp1 23 | lda #$c8 24 | sbc tmp1 25 | sta tmp1 ; starting offset 26 | 27 | ldx #0 28 | @drawLoop: 29 | lda #BLOCK_TILES 30 | cpx tmp1 31 | bcs @saveMino 32 | lda #EMPTY_TILE 33 | @saveMino: 34 | sta playfield, x 35 | inx 36 | cpx #$c8 37 | bcc @drawLoop 38 | 39 | ; wells 40 | clc 41 | lda tmp1 42 | tax 43 | @nextLoop: 44 | txa 45 | adc tqtyCurrent 46 | tay 47 | lda #EMPTY_TILE 48 | sta playfield, y 49 | 50 | txa 51 | adc tqtyNext 52 | tay 53 | lda #BLOCK_TILES+1 54 | sta playfield, y 55 | 56 | txa 57 | adc #10 58 | tax 59 | cpx #$c8 60 | bcc @nextLoop 61 | rts 62 | -------------------------------------------------------------------------------- /src/modes/tspins.asm: -------------------------------------------------------------------------------- 1 | advanceGameTSpins: 2 | ; track the tspin quantity on the first tspin attempt 3 | lda tspinQuantity 4 | bne @qtyEnd 5 | lda tetriminoX 6 | cmp #$EF 7 | beq @qtyEnd 8 | lda statsByType 9 | sta tspinQuantity 10 | @qtyEnd: 11 | ; reset score if tspinQuantity doesnt match 12 | lda score 13 | bne @scrub 14 | lda score+1 15 | bne @scrub 16 | lda score+2 17 | bne @scrub 18 | jmp @continue 19 | @scrub: 20 | lda tspinQuantity 21 | beq @continue 22 | cmp statsByType 23 | beq @continue 24 | 25 | jsr clearPoints 26 | 27 | lda renderFlags 28 | ora #RENDER_SCORE 29 | sta renderFlags 30 | @continue: 31 | 32 | advanceGameTSpins_actual: 33 | ; see if the sprite has reached the right position 34 | lda #8 35 | sbc tspinX 36 | cmp tetriminoX 37 | bne @notSuccessful 38 | lda #18 39 | sbc tspinY 40 | cmp tetriminoY 41 | bne @notSuccessful 42 | ; check the orientation 43 | lda currentPiece 44 | cmp #2 45 | bne @notSuccessful 46 | 47 | ; set successful tspin vars 48 | lda #$3 49 | sta playState 50 | lda #0 51 | sta tspinX 52 | sta vramRow ; shorter to do it here than in rendering 53 | 54 | ; add score 55 | lda #$2 56 | sta completedLines 57 | jsr addPointsRaw 58 | 59 | ; TODO: copy score to top 60 | lda #$20 61 | sta spawnDelay 62 | lda #TETRIMINO_X_HIDE 63 | sta tetriminoX 64 | 65 | @notSuccessful: 66 | ; check if a tspin is setup 67 | lda tspinX 68 | ; cmp #0 ; lda sets z flag 69 | bne renderTSpin 70 | 71 | generateNewTSpin: 72 | ldx #rng_seed 73 | jsr generateNextPseudorandomNumber 74 | lda rng_seed 75 | tax 76 | ; lower nybble 77 | and #$7 78 | sta tspinX 79 | ; high nybbleish 80 | txa 81 | ror 82 | ror 83 | ror 84 | ror 85 | and #3 86 | sta tspinY 87 | ; some other bit 88 | txa 89 | and #1 90 | sta tspinType 91 | 92 | lda #0 93 | sta tspinQuantity 94 | 95 | renderTSpin: 96 | jsr clearPlayfield 97 | 98 | lda tspinY 99 | clc 100 | adc #3 101 | jsr drawFloor 102 | 103 | ; get tspin offset 104 | ldx tspinY 105 | lda multBy10Table, x 106 | sta tmp1 107 | 108 | lda #$FF 109 | sbc tspinX ; sub X 110 | sbc tmp1 ; sub Y 111 | tax 112 | ; draw tspin 113 | lda #EMPTY_TILE 114 | sta $03bc, x 115 | sta $03bd, x 116 | sta $03be, x 117 | sta $03c7, x 118 | sta $03b3, x 119 | ldy tspinType 120 | ; cpy #0 ; ldy sets z flag 121 | bne @noInc 122 | inx 123 | inx 124 | @noInc: 125 | sta $03b2, x 126 | 127 | rts 128 | -------------------------------------------------------------------------------- /src/nametables.asm: -------------------------------------------------------------------------------- 1 | game_type_menu_nametable: ; RLE 2 | .incbin "nametables/game_type_menu_nametable_practise.bin" 3 | game_type_menu_nametable_extra: ; RLE 4 | .incbin "nametables/game_type_menu_nametable_extra.bin" 5 | level_menu_nametable: ; RLE 6 | .incbin "nametables/level_menu_nametable_practise.bin" 7 | game_nametable: ; RLE 8 | .incbin "nametables/game_nametable_practise.bin" 9 | enter_high_score_nametable: ; RLE 10 | .incbin "nametables/enter_high_score_nametable_practise.bin" 11 | rocket_nametable: ; RLE 12 | .incbin "nametables/rocket_nametable.bin" 13 | legal_nametable: ; RLE 14 | .incbin "nametables/legal_nametable.bin" 15 | title_nametable_patch: ; stripe 16 | .byte $21, $69, $5, $1D, $12, $1D, $15, $E 17 | .byte $FF 18 | rocket_nametable_patch: ; stripe 19 | .byte $20, $83, 5, $19, $1B, $E, $1c, $1c 20 | .byte $20, $A3, 5, $1c, $1d, $a, $1b, $1d 21 | .byte $FF 22 | 23 | speedtest_nametable_patch: 24 | ; tiles 25 | .byte $21, $A3, $6, 0, 0, $ED, 0, 0, $EC 26 | .byte $22, $23, $3, 'T', 'A', 'P' 27 | .byte $22, $A3, $3, 'D', 'I', 'R' 28 | .byte $22, $28, $1, 0 29 | ; attrs 30 | .byte $23, $e2, $1, 0 31 | .byte $23, $ea, $1, 0 32 | .byte $23, $d8, $43, $55 33 | .byte $FF 34 | 35 | 36 | .include "nametables/rle.asm" 37 | -------------------------------------------------------------------------------- /src/nametables/build.js: -------------------------------------------------------------------------------- 1 | require('./game'); 2 | require('./enter_high_score'); 3 | require('./game_type_menu'); 4 | require('./level_menu'); 5 | require('./rocket_legal'); 6 | -------------------------------------------------------------------------------- /src/nametables/enter_high_score.js: -------------------------------------------------------------------------------- 1 | const { 2 | writeRLE, 3 | blankNT, 4 | drawTiles, 5 | drawAttrs, 6 | flatLookup, 7 | } = require('./nametables'); 8 | 9 | 10 | const buffer = blankNT(); 11 | 12 | let lookup = flatLookup(` 13 | 0123456789ABCDEF 14 | GHIJKLMNOPQRSTUV 15 | WXYZ-,˙>rtyfhvbn 16 | ########qweadzxc 17 | ############jkl/ 18 | ui!###g@######() 19 | ############^$#. 20 | ################ 21 | ################ 22 | ################ 23 | ################ 24 | ################ 25 | ################ 26 | ################ 27 | ################ 28 | ############### 29 | `); 30 | 31 | drawTiles(buffer, lookup, ` 32 | ╲▂╢╢╢▀░▀╃▃╢╲╠╡▂▐▃╲▐▁▃▂╢▂╢╲╢▀▁▁▃╲ 33 | ╲qwwwwwwwwwwwwwwwwwwwwwwwwwwwwe□ 34 | ▂a d╢ 35 | ▀a d□ 36 | ╢a d╢ 37 | ╂a d□ 38 | ▂a GOOD GAME d▀ 39 | ╢a d╡ 40 | ╀a d╱ 41 | ▂a d▀ 42 | ╠a YOU ARE A d▃ 43 | ╰a d▃ 44 | ╠a TETRIS MASTER. d╢ 45 | ╰a d╀ 46 | ▀a d▂ 47 | ╢a PLEASE ENTER YOUR NAME d╢ 48 | ╀a d╲ 49 | ▂a rtttttttttttttttttttttttty d╲ 50 | ▃a fNAME SCORE LNS LVh d▂ 51 | ▀a jbbbbbbbbbbbbbbbbbbbbbbbbl d▀ 52 | ▀a f h d▃ 53 | ▐a f h d▀ 54 | ╲a f h d▃ 55 | ▂a f h d▀ 56 | ▃a f h d▀ 57 | ▀a f h d╡ 58 | ▀a vbbbbbbbbbbbbbbbbbbbbbbbbn d╱ 59 | ▁a d╢ 60 | ╠zxxxxxxxxxxxxxxxxxxxxxxxxxxxxc□ 61 | ╰╱╰╱▐▃▀▁□╲■▁▃▂╀▃▂▀╂╀▃▀░▀╂╲╢╰╱╲▂╢ 62 | `); 63 | 64 | drawAttrs(buffer, [` 65 | 2222222222222222 66 | 2222222222222222 67 | 2222222222222222 68 | 2222233333322222 69 | 2222222222222222 70 | 2222222222222222 71 | 2222222222222222 72 | 2222222222222222 73 | `,` 74 | 2000000000000002 75 | 2000000000000002 76 | 2000000000000002 77 | 2000000000000002 78 | 2000000000000002 79 | 2000000000000002 80 | 2222222222222222 81 | 2222222222222222 82 | `]); 83 | 84 | writeRLE( 85 | __dirname + '/enter_high_score_nametable_practise.bin', 86 | buffer, 87 | ); 88 | -------------------------------------------------------------------------------- /src/nametables/game.js: -------------------------------------------------------------------------------- 1 | const { 2 | writeRLE, 3 | blankNT, 4 | drawTiles, 5 | drawAttrs, 6 | flatLookup, 7 | printNT, 8 | } = require('./nametables'); 9 | 10 | const buffer = blankNT(); 11 | 12 | const lookup = flatLookup(` 13 | 0123456789ABCDEF 14 | GHIJKLMNOPQRSTUV 15 | WXYZ-#!######.## 16 | ########qweadzxc 17 | ################ 18 | ################ 19 | ################ 20 | ################ 21 | ################ 22 | ################ 23 | ################ 24 | ################ 25 | ################ 26 | ################ 27 | ################ 28 | ############### 29 | `); 30 | 31 | drawTiles(buffer, lookup, ` 32 | ################################ 33 | ################################ 34 | ###########ɴȴȴȴȴȴȴȴȴȴȴɵɴȴȴȴȴȴȴɵ# 35 | ##ɴȴȴȴȴȴȴɵ#ȵ#LINES-000ȶȵ######ȶ# 36 | ##ȵ######ȶ#ɶȷȷȷȷȷȷȷȷȷȷɷȵTOP###ȶ# 37 | ##ɶȷȷȷȷȷȷɷ#############ȵ######ȶ# 38 | #######################ȵ######ȶ# 39 | #######################ȵSCORE#ȶ# 40 | #ɴȴȴȴȴȴȴȴȴɵ############ȵ000000ȶ# 41 | #ȵ########ȶ############ȵ######ȶ# 42 | #ȵ########ȶ############ɶȷȷȷȷȷȷɷ# 43 | #ȵ########ȶ##################### 44 | #ȵ####000#ȶ##################### 45 | #ȵ########ȶ#############NEXT#### 46 | #ȵ####000#ȶ##################### 47 | #ȵ########ȶ##################### 48 | #ȵ####000#ȶ##################### 49 | #ȵ########ȶ##################### 50 | #ȵ####000#ȶ##################### 51 | #ȵ########ȶ############ɴȴȴȴȴȴɵ## 52 | #ȵ####000#ȶ############ȵLEVELȶ## 53 | #ȵ########ȶ############ȵ#####ȶ## 54 | #ȵ####000#ȶ############ɶȷȷȷȷȷɷ## 55 | #ȵ########ȶ##################### 56 | #ȵ####000#ȶ##################### 57 | #ȵ########ȶ##################### 58 | #ɶȷȷȷȷȷȷȷȷɷ##################### 59 | ################################ 60 | ################################ 61 | ################################ 62 | `); 63 | 64 | drawTiles(buffer, lookup, ` 65 | ʞʟʠʠʒʝʞʜʕʞʟʠʜʙʜʙʠʡʟʜʓʡʐʑʟʡʜʕʞʒʞʟ 66 | ʒʙʡʠʡʟʜʓʡʒʙʡʜʝʝʞʡʜʚʞʘʞʖʗʘʝʞʡʜʙʜʛ 67 | ʡʒʞʡʜʚʞʘʞʡʟ####################ʡ 68 | ʟʠ########ʠ####################ʟ 69 | ʠʡ########ʠ####################ʠ 70 | ʖʞ########ʡɰȰȰȰȰȰȰȰȰȰȰɱ########ʠ 71 | ʜʝʝʞʟʐʑʟʜʝʓȱ##########Ȳ########ʡ 72 | ʝʞʜʝʙʖʗʘʝʞʡȱ##########Ȳ########ʟ 73 | ʓ##########ȱ##########Ȳ########ʘ 74 | ʠ#ɩɪɫɬɭɮɯɟ#ȱ##########Ȳ########ʟ 75 | ʡ##########ȱ##########Ȳ########ʠ 76 | ʞ##ɀɁɂ#####ȱ##########Ȳʜʝʝʞʜʝʓʜʙ 77 | ʞ##ɐɑɒ#####ȱ##########ȲɰȰȰȰȰɱʡʟʜ 78 | ʞ##ɉɊɋ#####ȱ##########Ȳȱ####Ȳʒʙʜ 79 | ʟ##əɚɛ#####ȱ##########Ȳȱ####Ȳʡʒʞ 80 | ʠ##Ɇɇ######ȱ##########Ȳȱ####Ȳʜʙʐ 81 | ʠ##ɖɗɘ#####ȱ##########Ȳȱ####Ȳʒʞʖ 82 | ʡ##ɠɡ######ȱ##########Ȳȱ####Ȳʠʜʓ 83 | ʑ##ɢɣ######ȱ##########Ȳɲȳȳȳȳɳʡʟʠ 84 | ʗ##ɃɄɅ#####ȱ##########Ȳ#######ʠʡ 85 | ʞ##ɓɔɕ#####ȱ##########Ȳ#######ʠʜ 86 | ʞ##ɌɍɎ#####ȱ##########Ȳ#######ʡʟ 87 | ʟ##ɜɝɞ#####ȱ##########Ȳ#######ʜʛ 88 | ʙ##########ȱ##########Ȳʟʜʕʞʟʒʝʞʡ 89 | ʓ##ɤɥɦ#####ȱ##########Ȳʔʞʡʜʛʡʟʜʝ 90 | ʠ##########ȱ##########Ȳʡʜʝʓʡʒʙʒʞ 91 | ʡ##########ɲȳȳȳȳȳȳȳȳȳȳɳʟʐʑʡʟʡʜʙʟ 92 | ʟʟʜʕʞʒʝʞʟʜʓʜʓʟʒʞʒʝʞʜʝʝʞʠʖʗʜʛʟʜʝʙ 93 | ʠʘʓʡʟʡʟʒʙʟʘʞʠʠʠʟʡʟʐʑʒʞʟʖʞʜʓʡʠʜʝʝ 94 | ʖʞʡʜʚʞʠʡʜʚʞʟʡʠʡʠʒʙʖʗʠʟʘʝʞʟʠʜʙʒʞʟ 95 | `); 96 | 97 | drawAttrs(buffer, [` 98 | 3333333333333333 99 | 3333333333333333 100 | 3333333333333333 101 | 3333332222233333 102 | 3333332222233333 103 | 3220032222233333 104 | 3220032222233333 105 | 3220032222233333 106 | `, ` 107 | 3220032222233333 108 | 3220032222233333 109 | 3220032222233333 110 | 3220032222233333 111 | 3220032222233333 112 | 3333333333333333 113 | 3333333333333333 114 | 0000000000000000 115 | `]); 116 | 117 | writeRLE( 118 | __dirname + '/game_nametable_practise.bin', 119 | buffer, 120 | ); 121 | -------------------------------------------------------------------------------- /src/nametables/level_menu.js: -------------------------------------------------------------------------------- 1 | const { 2 | blankNT, 3 | writeRLE, 4 | drawTiles, 5 | drawAttrs, 6 | flatLookup, 7 | } = require('./nametables'); 8 | 9 | const buffer = blankNT(); 10 | 11 | let lookup = flatLookup(` 12 | 0123456789ABCDEF 13 | GHIJKLMNOPQRSTUV 14 | WXYZ-,˙>rtyfhvbn 15 | ########qweadzxc 16 | ####ĄąĆćĈĉĊċjkl/ 17 | ui!###g@ß#####() 18 | ###########æ^$#. 19 | ################ 20 | ################ 21 | ################ 22 | ################ 23 | ################ 24 | ################ 25 | ################ 26 | ################ 27 | ############### 28 | `); 29 | 30 | drawTiles(buffer, lookup, ` 31 | ɠɡɠɡɢʂɢʐʃɢɢɢʂʀʑʀʑɰɱɢʂʀʁʁʃʐʃɢɢʀʁʑ 32 | ɰqwwwwwwwwwwwwwwwwwwwwwwwwwwwweʂ 33 | ʀa dɡ 34 | ʀa qwwwwwwe dɱ 35 | ɢa a d dʃ 36 | ɲa zxxxxxxc dʑ 37 | ʠa dɲ 38 | ʐa ĄąąąąąĆ dʂ 39 | ʂa ćLEVELĈ dʑ 40 | ɠa ĉĊĊĊĊĊċ dʂ 41 | ɰa rtututututy dʑ 42 | ʀa f0f1f2f3f4h rttty dʂ 43 | ɢa jbkbkbkbkbl f h dʀ 44 | ɲa f5f6f7f8f9h vbbbn dʃ 45 | ɲa vbibibibibn dʃ 46 | ʂa dʃ 47 | ɢa dʑ 48 | ʠa rtttttttttttttttttttttttty dɲ 49 | ɢa fNAME SCORE LNS LVh dʂ 50 | ╂a jbbbbbbbbbbbbbbbbbbbbbbbbl dʑ 51 | ʂa f h dʂ 52 | ʀa f h dʑ 53 | ɠa f h dʂ 54 | ɰa f h dʀ 55 | ɢa f h dʃ 56 | ɲa f h dʃ 57 | ɲa vbbbbbbbbbbbbbbbbbbbbbbbbn dɡ 58 | ʂa dɱ 59 | ɢzxxxxxxxxxxxxxxxxxxxxxxxxxxxxcɢ 60 | ɲʀʡʀ╁ʃɢʂʀ╃ʃʀʡɢɲɢɰɱʂʀʁʡɢʂɠɡʠʃʠʃʀ╁ 61 | `); 62 | 63 | drawAttrs(buffer, [` 64 | 2222222222222222 65 | 2222222222222222 66 | 2222222222222222 67 | 2222222222222222 68 | 2222222222222222 69 | 2223333333333222 70 | 2223333333333222 71 | 2223333333333222 72 | `,` 73 | 2000000000000002 74 | 2000000000000002 75 | 2000000000000002 76 | 2000000000000002 77 | 2000000000000002 78 | 2000000000000002 79 | 2222222222222222 80 | 2222222222222222 81 | `]); 82 | 83 | writeRLE( 84 | __dirname + '/level_menu_nametable_practise.bin', 85 | buffer, 86 | ); 87 | -------------------------------------------------------------------------------- /src/nametables/nametables.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require('fs'); 2 | const { konamiComp } = require('./rle'); 3 | 4 | function strip(_array) { 5 | const array = [..._array]; 6 | const stripped = []; 7 | while (array.length) { 8 | const next = array.splice(0, 35); 9 | stripped.push(...next.slice(3)); 10 | } 11 | return stripped; 12 | } 13 | 14 | function readStripe(filename) { 15 | const bin = readFileSync(filename); 16 | return strip(bin); 17 | } 18 | 19 | function writeRLE(filename, buffer) { 20 | const compressed = Buffer.from(konamiComp(Array.from(buffer))); 21 | // console.log(`${filename.split('/').pop()} ${buffer.length} -> ${compressed.length}`); 22 | writeFileSync(filename, compressed); 23 | } 24 | 25 | const STR_OFFSET = 512; 26 | 27 | function printNT(buffer, lookup) { 28 | const chars = [...buffer].map(value => { 29 | const char = lookup[value]; 30 | 31 | if (char === '#') { 32 | return String.fromCharCode(value + STR_OFFSET); 33 | } 34 | 35 | return char; 36 | }); 37 | 38 | console.log(chars.join('').match(/.{32}/g).join('\n')); 39 | } 40 | 41 | function blankNT() { 42 | return Array.from({ length: 1024 }, () => 0xFF); 43 | } 44 | 45 | function drawTiles(buffer, lookup, tiles) { 46 | [...tiles.trim().split('\n').join('')].forEach((d, i) => { 47 | if (d !== '#') { 48 | const charCode = d.charCodeAt(0); 49 | if (charCode > STR_OFFSET) { 50 | buffer[i] = charCode - STR_OFFSET; 51 | } else { 52 | buffer[i] = lookup.indexOf(d); 53 | } 54 | } 55 | }); 56 | } 57 | 58 | function drawRect(buffer, x, y, w, h, offset) { 59 | x += 3; 60 | const pixel = x+ (y*32); 61 | for (let i=0;w>i;i++) { 62 | for (let j=0;h>j;j++) { 63 | buffer[pixel + i + (j * 32)] = offset+i + (j * 0x10); 64 | } 65 | } 66 | } 67 | 68 | function drawAttrs(buffer, attrs) { 69 | const palettes = p => p.trim().match(/.+\n.+$/gm) 70 | .flatMap(line=>{ 71 | const [t,b]=line.split('\n'); 72 | return t.trim().match(r=/../g).map((d,i)=>d+b.trim().match(r)[i]) 73 | }) 74 | .map(d=>+('0b'+[...d].reverse().map(d=>(+d).toString(2).padStart(2,0)).join``)); 75 | 76 | [ 77 | [30 * 32, palettes(attrs[0])], 78 | [31 * 32, palettes(attrs[1])], 79 | ].forEach(([index, attributes]) => { 80 | attributes.forEach((byte, i) => { buffer[i+index] = byte; }); 81 | }); 82 | } 83 | 84 | function flatLookup(lookup) { 85 | return lookup.trim().split('\n').map(d=>d.padEnd(16).slice(0, 16)).join(''); 86 | } 87 | 88 | module.exports = { 89 | strip, 90 | readStripe, 91 | writeRLE, 92 | printNT, 93 | blankNT, 94 | drawTiles, 95 | drawRect, 96 | drawAttrs, 97 | flatLookup, 98 | }; 99 | -------------------------------------------------------------------------------- /src/nametables/rle.asm: -------------------------------------------------------------------------------- 1 | ; Konami RLE decompressor 2 | 3 | ; MIT License 4 | 5 | ; Copyright (c) 2019 Eric Anderson 6 | 7 | ; Permission is hereby granted, free of charge, to any person obtaining a copy 8 | ; of this software and associated documentation files (the "Software"), to deal 9 | ; in the Software without restriction, including without limitation the rights 10 | ; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | ; copies of the Software, and to permit persons to whom the Software is 12 | ; furnished to do so, subject to the following conditions: 13 | 14 | ; The above copyright notice and this permission notice shall be included in all 15 | ; copies or substantial portions of the Software. 16 | 17 | ; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | ; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | ; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | ; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | ; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | ; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | ; SOFTWARE. 24 | ; 25 | ; Encoding: 26 | ; $00 Unsupported (useless) 27 | ; <= $80 Repeat next byte n times 28 | ; $FF End 29 | ; > $80 Next (n-128) bytes are literal 30 | 31 | addrLo := $0000 32 | addrHi := addrLo+1 33 | addrOff := addrLo+2 34 | 35 | copyRleNametableToPpu: 36 | lda #$20 37 | sta addrOff 38 | copyRleNametableToPpuOffset: 39 | jsr copyAddrAtReturnAddressToTmp_incrReturnAddrBy2 40 | ldx PPUSTATUS 41 | lda addrOff 42 | sta PPUADDR 43 | lda #$00 44 | sta PPUADDR 45 | jmp rleDecodeToPpu 46 | 47 | ; Decodes Konami RLE-encoded stream with address stored at $0000. 48 | ; 49 | ; Does not support 0-length runs; control byte $00 is not supported 50 | rleDecodeToPpu: 51 | ; y is current input offset 52 | ; x is chunk length remaining 53 | ldy #$00 54 | 55 | @processChunk: 56 | lda (addrLo),y 57 | cmp #$81 58 | bmi @runLength 59 | cmp #$FF 60 | beq @ret 61 | and #$7F 62 | 63 | ; literalLength 64 | tax 65 | @literalLoop: 66 | iny 67 | lda (addrLo),y 68 | sta PPUDATA 69 | dex 70 | bne @literalLoop 71 | beq @preventYOverflow 72 | 73 | @runLength: 74 | tax 75 | iny 76 | lda (addrLo),y 77 | @runLengthLoop: 78 | sta PPUDATA 79 | dex 80 | bne @runLengthLoop 81 | 82 | @preventYOverflow: 83 | ; The largest input chunk size is literal with a length of 126, which 84 | ; is 127 bytes of input. We make sure adding 127 to y does not 85 | ; overflow. This allows us to ignore y overflow during loops. 86 | iny 87 | bpl @processChunk 88 | ; y is at risk of overflowing next chunk 89 | tya 90 | ldy #$00 91 | clc 92 | adc addrLo 93 | sta addrLo 94 | bcc @processChunk 95 | inc addrHi 96 | jmp @processChunk 97 | 98 | @ret: 99 | rts 100 | -------------------------------------------------------------------------------- /src/nametables/rle.js: -------------------------------------------------------------------------------- 1 | function konamiComp(buffer) { 2 | const compressed = []; 3 | 4 | for (let i = 0; i < buffer.length;) { 5 | const byte = buffer[i]; 6 | 7 | // count extra dupes 8 | let peek = 0; 9 | for (;byte ==buffer[i+1+peek];peek++); 10 | const count = Math.min(peek + 1, 0x80); 11 | 12 | if (peek > 0) { 13 | compressed.push([count, byte]); 14 | i+= count; 15 | } else { 16 | // we have already peeked the next byte and know it's not a double 17 | // so start checking from there 18 | const start = i + 1; 19 | const nextDouble = buffer.slice(start, start + 0x7F) 20 | .findIndex((d,i,a)=>d==a[i+1]); 21 | 22 | const count = Math.min(nextDouble === -1 23 | ? buffer.length - i 24 | : nextDouble + 1, 0x7F); 25 | 26 | compressed.push([0x80 + count, buffer.slice(i, count + i)]); 27 | i += count; 28 | } 29 | } 30 | 31 | compressed.push(0xFF); 32 | 33 | return compressed.flat(Infinity); 34 | } 35 | 36 | function strip(_array) { 37 | const array = [..._array]; 38 | const stripped = []; 39 | while (array.length) { 40 | const next = array.splice(0, 35); 41 | stripped.push(...next.slice(3)); 42 | } 43 | return stripped; 44 | } 45 | 46 | module.exports = function (buffer) { 47 | const array = Array.from(buffer); 48 | const compressed = konamiComp(strip(array)); 49 | console.log(`compressed ${buffer.length} -> ${compressed.length}`); 50 | return Buffer.from(compressed); 51 | }; 52 | 53 | Object.assign(module.exports, { konamiComp }); 54 | -------------------------------------------------------------------------------- /src/nametables/rocket_legal.js: -------------------------------------------------------------------------------- 1 | const { 2 | writeRLE, 3 | blankNT, 4 | drawTiles, 5 | flatLookup, 6 | drawAttrs, 7 | } = require('./nametables'); 8 | 9 | const legal = blankNT(); 10 | const rocket = blankNT(); 11 | 12 | const lookup = flatLookup(` 13 | 0123456789ABCDEF 14 | GHIJKLMNOPQRSTUV 15 | WXYZ-.˙>qweadzxc 16 | ################ 17 | ################ 18 | ################ 19 | ################ 20 | ################ 21 | ################ 22 | ################ 23 | ################ 24 | ################ 25 | ################ 26 | ################ 27 | ################ 28 | ############### 29 | `); 30 | 31 | drawTiles(rocket, lookup, ` 32 | ################################ 33 | ################################ 34 | ################################ 35 | ################################ 36 | ################################ 37 | ################################ 38 | ############ ########## 39 | ############ ROCKET ##SCORE### 40 | ############ SCREEN ########## 41 | ############ ########## 42 | ############ ########## 43 | ############ ##LINES### 44 | ############ ########## 45 | ############ ########## 46 | ############ ########## 47 | ############ ##LEVEL### 48 | ############ ########## 49 | ############ ########## 50 | ############ ########## 51 | ############ ##START### 52 | ############ ########## 53 | ############ ########## 54 | ############ ########## 55 | ############ ########## 56 | ############ ########## 57 | ############ ########## 58 | ################################ 59 | ################################ 60 | ################################ 61 | ################################ 62 | `); 63 | 64 | // drawRect(rocket, -2, 11, 9, 6, 0x30); 65 | 66 | drawTiles(legal, lookup, ` 67 | ################################ 68 | ################################ 69 | ################################ 70 | ################################ 71 | ################################ 72 | ################################ 73 | ################################ 74 | ################################ 75 | ################################ 76 | ################################ 77 | ################################ 78 | #########LEGAL SCREEN########## 79 | ################################ 80 | ################################ 81 | ################################ 82 | ################################ 83 | ################################ 84 | ################################ 85 | ################################ 86 | ################################ 87 | ################################ 88 | ################################ 89 | ################################ 90 | ######SOMETHING TO DO WITH###### 91 | ########ALEXEY PAZHITNOV######## 92 | ################################ 93 | ################################ 94 | ################################ 95 | ################################ 96 | ################################ 97 | `); 98 | 99 | drawAttrs(rocket, [` 100 | 1111111111111111 101 | 1111111111111111 102 | 1111111111111111 103 | 1111111111111111 104 | 1111111111111111 105 | 1111111111111111 106 | 1111111111111111 107 | 1111111111111111 108 | `,` 109 | 1111111111111111 110 | 1111111111111111 111 | 1111111111111111 112 | 1111111111111111 113 | 1111111111111111 114 | 1111111111111111 115 | 1111111111111111 116 | 1111111111111111 117 | `]); 118 | 119 | drawAttrs(legal, [` 120 | 0000000000000000 121 | 0000000000000000 122 | 0000000000000000 123 | 0000000000000000 124 | 0000000000000000 125 | 0000000000000000 126 | 0000000000000000 127 | 0000000000000000 128 | `,` 129 | 0000000000000000 130 | 0000000000000000 131 | 0000000000000000 132 | 0000000000000000 133 | 1111111111111111 134 | 1111111111111111 135 | 1111111111111111 136 | 1111111111111111 137 | `]); 138 | 139 | writeRLE( 140 | __dirname + '/rocket_nametable.bin', 141 | rocket, 142 | ); 143 | 144 | writeRLE( 145 | __dirname + '/legal_nametable.bin', 146 | legal, 147 | ); 148 | -------------------------------------------------------------------------------- /src/nmi/nmi.asm: -------------------------------------------------------------------------------- 1 | nmi: pha 2 | txa 3 | pha 4 | tya 5 | pha 6 | lda #$00 7 | sta oamStagingLength 8 | jsr render 9 | lda currentPpuCtrl 10 | sta PPUCTRL 11 | dec sleepCounter 12 | lda sleepCounter 13 | cmp #$FF 14 | bne @jumpOverIncrement 15 | inc sleepCounter 16 | @jumpOverIncrement: 17 | jsr copyOamStagingToOam 18 | lda frameCounter 19 | clc 20 | adc #$01 21 | sta frameCounter 22 | lda #$00 23 | adc frameCounter+1 24 | sta frameCounter+1 25 | ldx #rng_seed 26 | jsr generateNextPseudorandomNumber 27 | jsr copyCurrentScrollAndCtrlToPPU 28 | jsr pollControllerButtons 29 | lda #$00 30 | sta lagState ; clear flag after lag frame achieved 31 | .if KEYBOARD 32 | ; Read Family BASIC Keyboard 33 | jsr pollKeyboard 34 | .endif 35 | lda #$01 36 | sta verticalBlankingInterval 37 | pla 38 | tay 39 | tsx 40 | lda stack+4,x 41 | sta nmiReturnAddr 42 | pla 43 | tax 44 | pla 45 | irq: rti 46 | -------------------------------------------------------------------------------- /src/nmi/pollcontroller.asm: -------------------------------------------------------------------------------- 1 | pollControllerButtons: 2 | ; demo stuff used to live here 3 | jmp pollController 4 | 5 | pollController_actualRead: 6 | ldx joy1Location 7 | inx 8 | stx JOY1 9 | dex 10 | stx JOY1 11 | ldx #$08 12 | @readNextBit: 13 | lda JOY1 14 | lsr a 15 | rol newlyPressedButtons_player1 16 | lsr a 17 | rol tmp1 18 | lda JOY2_APUFC 19 | lsr a 20 | rol newlyPressedButtons_player2 21 | lsr a 22 | rol tmp2 23 | dex 24 | bne @readNextBit 25 | rts 26 | 27 | addExpansionPortInputAsControllerInput: 28 | lda tmp1 29 | ora newlyPressedButtons_player1 30 | sta newlyPressedButtons_player1 31 | lda tmp2 32 | ora newlyPressedButtons_player2 33 | sta newlyPressedButtons_player2 34 | rts 35 | 36 | jsr pollController_actualRead 37 | beq diffOldAndNewButtons 38 | pollController: 39 | jsr pollController_actualRead 40 | jsr addExpansionPortInputAsControllerInput 41 | lda newlyPressedButtons_player1 42 | sta generalCounter2 43 | lda newlyPressedButtons_player2 44 | sta generalCounter3 45 | jsr pollController_actualRead 46 | jsr addExpansionPortInputAsControllerInput 47 | lda newlyPressedButtons_player1 48 | and generalCounter2 49 | sta newlyPressedButtons_player1 50 | lda newlyPressedButtons_player2 51 | and generalCounter3 52 | sta newlyPressedButtons_player2 53 | 54 | lda goofyFlag 55 | beq @noGoofy 56 | lda newlyPressedButtons_player1 57 | asl 58 | and #$AA 59 | sta tmp3 60 | lda newlyPressedButtons_player1 61 | and #$AA 62 | lsr 63 | ora tmp3 64 | sta newlyPressedButtons_player1 65 | @noGoofy: 66 | 67 | diffOldAndNewButtons: 68 | ldx #$01 69 | @diffForPlayer: 70 | lda newlyPressedButtons_player1,x 71 | tay 72 | eor heldButtons_player1,x 73 | and newlyPressedButtons_player1,x 74 | sta newlyPressedButtons_player1,x 75 | sty heldButtons_player1,x 76 | dex 77 | bpl @diffForPlayer 78 | rts 79 | -------------------------------------------------------------------------------- /src/nmi/render.asm: -------------------------------------------------------------------------------- 1 | render: lda renderMode 2 | jsr switch_s_plus_2a 3 | .addr render_mode_static 4 | .addr render_mode_scroll 5 | .addr render_mode_congratulations_screen 6 | .addr render_mode_play_and_demo 7 | .addr render_mode_pause 8 | .addr render_mode_rocket 9 | .addr render_mode_speed_test 10 | .addr render_mode_level_menu 11 | .addr render_mode_linecap_menu 12 | 13 | .include "render_mode_level_menu.asm" ; no rts / jmp 14 | 15 | render_mode_static: 16 | lda currentPpuCtrl 17 | and #$FC 18 | sta currentPpuCtrl 19 | jsr resetScroll 20 | rts 21 | 22 | .include "render_mode_linecap.asm" 23 | .include "render_mode_pause.asm" 24 | .include "render_mode_congratulations_screen.asm" 25 | .include "render_mode_rocket.asm" 26 | .include "render_mode_scroll.asm" 27 | .include "render_mode_speed_test.asm" 28 | .include "render_mode_play_and_demo.asm" 29 | 30 | .include "render_hz.asm" 31 | .include "render_input_log.asm" 32 | .include "render_score.asm" 33 | .include "render_util.asm" 34 | -------------------------------------------------------------------------------- /src/nmi/render_hz.asm: -------------------------------------------------------------------------------- 1 | renderHz: 2 | ; only set at game start and when player is controlling a piece 3 | ; during which, no other tile updates are happening 4 | ; this is pretty expensive and uses up $8 PPU tile writes and 1 palette write 5 | 6 | ; delay 7 | 8 | lda #$22 9 | sta PPUADDR 10 | lda #$67 11 | sta PPUADDR 12 | ldx #$24 ; minus sign 13 | lda hzSpawnDelay 14 | and #$80 15 | bne @isNegative 16 | ldx #$FF ; blank tile 17 | @isNegative: 18 | stx PPUDATA 19 | lda hzSpawnDelay 20 | and #$7F ; clear sign flag 21 | sta PPUDATA 22 | 23 | renderHzSpeedTest: 24 | 25 | ; palette 26 | 27 | lda #$3F 28 | sta PPUADDR 29 | lda #$07 30 | sta PPUADDR 31 | lda hzPalette 32 | sta PPUDATA 33 | 34 | ; hz 35 | 36 | lda #$21 37 | sta PPUADDR 38 | lda #$A3 39 | sta PPUADDR 40 | lda hzResult 41 | jsr twoDigsToPPU 42 | lda #$21 43 | sta PPUADDR 44 | lda #$A6 45 | sta PPUADDR 46 | lda hzResult+1 47 | jsr twoDigsToPPU 48 | 49 | ; taps 50 | 51 | lda #$22 52 | sta PPUADDR 53 | lda #$28 54 | sta PPUADDR 55 | lda hzTapCounter 56 | ; and #$f 57 | sta PPUDATA 58 | 59 | ; direction 60 | 61 | lda #$22 62 | sta PPUADDR 63 | lda #$A8 64 | sta PPUADDR 65 | lda hzTapDirection 66 | clc 67 | adc #$D6 68 | sta PPUDATA 69 | rts 70 | -------------------------------------------------------------------------------- /src/nmi/render_input_log.asm: -------------------------------------------------------------------------------- 1 | getInputAddr: 2 | clc 3 | lda inputLogCounter 4 | and #$18 5 | lsr 6 | lsr 7 | lsr 8 | adc #$20 9 | and #$23 10 | tay ; high 11 | 12 | clc 13 | ldx inputLogCounter 14 | txa 15 | and #$7 16 | tax 17 | lda multBy32Table, x 18 | clc 19 | adc #$19 20 | tax ; low 21 | rts 22 | 23 | renderHzInputRows: 24 | ; if hzFrameCounter == 0, reset rows 25 | lda hzFrameCounter+1 26 | bne @checkLimit 27 | lda hzFrameCounter 28 | bne @checkLimit 29 | 30 | lda #2 31 | sta inputLogCounter 32 | 33 | ; enable vertical drawing 34 | lda PPUCTRL 35 | ora #%100 36 | sta PPUCTRL 37 | 38 | jsr getInputAddr 39 | tya 40 | sta PPUADDR 41 | txa 42 | sta PPUADDR 43 | 44 | jsr clearInputLine 45 | 46 | jsr getInputAddr 47 | inx 48 | tya 49 | sta PPUADDR 50 | txa 51 | sta PPUADDR 52 | 53 | jsr clearInputLine 54 | 55 | lda PPUCTRL 56 | and #%11111011 57 | sta PPUCTRL 58 | 59 | 60 | @checkLimit: 61 | lda inputLogCounter 62 | cmp #28 63 | bcc @tickCounter 64 | rts 65 | @tickCounter: 66 | jsr getInputAddr 67 | tya 68 | sta PPUADDR 69 | txa 70 | sta PPUADDR 71 | 72 | lda heldButtons_player1 73 | ora newlyPressedButtons_player1 74 | sta tmpZ 75 | and #BUTTON_RIGHT|BUTTON_LEFT 76 | tax 77 | lda inputLogTiles, x 78 | sta PPUDATA 79 | 80 | ; print A/B 81 | lda tmpZ 82 | and #BUTTON_A|BUTTON_B 83 | lsr 84 | lsr 85 | lsr 86 | lsr 87 | lsr 88 | lsr 89 | tax 90 | lda inputLogTiles+3, x 91 | sta PPUDATA 92 | 93 | inc inputLogCounter 94 | rts 95 | 96 | clearInputLine: 97 | lda #$FF 98 | ldx #26 99 | @clearRow: 100 | sta PPUDATA 101 | dex 102 | bne @clearRow 103 | rts 104 | 105 | inputLogTiles: 106 | .byte $24, $1B, $15 107 | .byte $24, $B, $A 108 | -------------------------------------------------------------------------------- /src/nmi/render_mode_congratulations_screen.asm: -------------------------------------------------------------------------------- 1 | render_mode_congratulations_screen: 2 | lda renderFlags 3 | and #RENDER_HIGH_SCORE_LETTER 4 | beq @ret 5 | lda highScoreEntryRawPos 6 | asl a 7 | tax 8 | lda highScorePpuAddrTable,x 9 | sta PPUADDR 10 | lda highScoreEntryRawPos 11 | asl a 12 | tax 13 | inx 14 | lda highScorePpuAddrTable,x 15 | sta generalCounter 16 | clc 17 | adc highScoreEntryNameOffsetForLetter 18 | sta PPUADDR 19 | ldx highScoreEntryCurrentLetter 20 | lda highScoreCharToTile,x 21 | sta PPUDATA 22 | lda #0 23 | sta renderFlags 24 | @ret: 25 | jsr resetScroll 26 | rts 27 | -------------------------------------------------------------------------------- /src/nmi/render_mode_level_menu.asm: -------------------------------------------------------------------------------- 1 | render_mode_level_menu: 2 | lda renderFlags 3 | and #RENDER_LINES 4 | beq @noCustomLevel 5 | lda #$21 6 | sta PPUADDR 7 | lda #$95 8 | sta PPUADDR 9 | lda customLevel 10 | jsr renderByteBCD 11 | lda #0 12 | sta renderFlags 13 | @noCustomLevel: 14 | -------------------------------------------------------------------------------- /src/nmi/render_mode_linecap.asm: -------------------------------------------------------------------------------- 1 | render_mode_linecap_menu: 2 | lda renderFlags 3 | and #RENDER_LINES 4 | beq @static 5 | ; render level / lines 6 | lda #0 7 | sta renderFlags 8 | lda #$21 9 | sta PPUADDR 10 | lda #$F3 11 | sta PPUADDR 12 | jsr render_linecap_level_lines 13 | 14 | @static: 15 | jmp render_mode_static 16 | 17 | render_linecap_level_lines: 18 | lda linecapWhen 19 | bne @linecapLines 20 | lda linecapLevel 21 | jsr renderByteBCD 22 | jmp render_mode_static 23 | 24 | @linecapLines: 25 | lda linecapLines+1 26 | sta PPUDATA 27 | lda linecapLines 28 | jsr twoDigsToPPU 29 | rts 30 | -------------------------------------------------------------------------------- /src/nmi/render_mode_pause.asm: -------------------------------------------------------------------------------- 1 | render_mode_pause: 2 | lda renderFlags 3 | and #RENDER_DEBUG 4 | beq @skipSaveSlotPatch 5 | jsr saveSlotNametablePatch 6 | lda renderFlags 7 | and #~RENDER_DEBUG 8 | sta renderFlags 9 | @skipSaveSlotPatch: 10 | lda playState 11 | cmp #$04 12 | beq @done 13 | jsr render_playfield 14 | @done: 15 | jsr resetScroll 16 | rts 17 | -------------------------------------------------------------------------------- /src/nmi/render_mode_rocket.asm: -------------------------------------------------------------------------------- 1 | render_mode_rocket: 2 | lda screenStage 3 | bne @stage1 4 | lda #$20 5 | sta PPUADDR 6 | lda #$83 7 | sta PPUADDR 8 | lda endingSleepCounter 9 | sta PPUDATA 10 | lda endingSleepCounter+1 11 | jsr twoDigsToPPU 12 | jmp @rocketEnd 13 | @stage1: 14 | cmp #1 15 | bne @stage2 16 | inc screenStage 17 | jsr bulkCopyToPpu 18 | .addr rocket_nametable_patch 19 | @stage2: 20 | @rocketEnd: 21 | jsr resetScroll 22 | rts 23 | -------------------------------------------------------------------------------- /src/nmi/render_mode_scroll.asm: -------------------------------------------------------------------------------- 1 | render_mode_scroll: 2 | ; handle scroll 3 | lda currentPpuCtrl 4 | and #$FC 5 | sta currentPpuCtrl 6 | lda #0 7 | sta ppuScrollX 8 | 9 | jsr calc_menuScrollY 10 | cmp menuScrollY 11 | beq @endscroll 12 | ; not equal 13 | cmp menuScrollY 14 | bcc @lessThan 15 | 16 | inc menuScrollY 17 | 18 | jmp @endscroll 19 | @lessThan: 20 | dec menuScrollY 21 | @endscroll: 22 | 23 | lda menuScrollY 24 | cmp #MENU_MAX_Y_SCROLL 25 | bcc @uncapped 26 | lda #MENU_MAX_Y_SCROLL 27 | sta menuScrollY 28 | @uncapped: 29 | 30 | sta ppuScrollY 31 | rts 32 | 33 | calc_menuScrollY: 34 | lda practiseType 35 | cmp #MENU_TOP_MARGIN_SCROLL 36 | bcs @underflow 37 | lda #MENU_TOP_MARGIN_SCROLL+1 38 | @underflow: 39 | sbc #MENU_TOP_MARGIN_SCROLL 40 | asl 41 | asl 42 | asl 43 | rts 44 | -------------------------------------------------------------------------------- /src/nmi/render_mode_speed_test.asm: -------------------------------------------------------------------------------- 1 | render_mode_speed_test: 2 | jsr renderHzInputRows 3 | lda renderFlags 4 | beq @noUpdate 5 | jsr renderHzSpeedTest 6 | lda #0 7 | sta renderFlags 8 | @noUpdate: 9 | lda #$B0 10 | sta ppuScrollX 11 | lda #$0 12 | sta ppuScrollY 13 | rts 14 | -------------------------------------------------------------------------------- /src/nmi/render_util.asm: -------------------------------------------------------------------------------- 1 | renderByteBCDNoPad: 2 | ldx #1 3 | jmp renderByteBCDStart 4 | renderByteBCD: 5 | ldx #$0 6 | renderByteBCDStart: 7 | sta tmpZ 8 | cmp #200 9 | bcc @maybe100 10 | lda #2 11 | sta PPUDATA 12 | lda tmpZ 13 | sbc #200 14 | jmp @byte 15 | @maybe100: 16 | cmp #100 17 | bcc @not100 18 | lda #1 19 | sta PPUDATA 20 | lda tmpZ 21 | sbc #100 22 | jmp @byte 23 | @not100: 24 | ; cpx #0 25 | txa ; either branch clobbers accumulator. txa sets z, saves 1 byte. 26 | bne @main 27 | lda #$EF 28 | sta PPUDATA 29 | @main: 30 | lda tmpZ 31 | @byte: 32 | tax 33 | lda byteToBcdTable, x 34 | 35 | twoDigsToPPU: 36 | sta generalCounter 37 | and #$F0 38 | lsr a 39 | lsr a 40 | lsr a 41 | lsr a 42 | sta PPUDATA 43 | lda generalCounter 44 | and #$0F 45 | sta PPUDATA 46 | rts 47 | 48 | render_playfield: 49 | lda #$04 50 | sta playfieldAddr+1 51 | jsr copyPlayfieldRowToVRAM 52 | jsr copyPlayfieldRowToVRAM 53 | jsr copyPlayfieldRowToVRAM 54 | jsr copyPlayfieldRowToVRAM 55 | lda practiseType 56 | cmp #MODE_LOWSTACK 57 | bne @ret 58 | jmp copyLowStackRowToVram 59 | @ret: rts 60 | 61 | vramPlayfieldRows: 62 | .word $20CC,$20EC,$210C,$212C 63 | .word $214C,$216C,$218C,$21AC 64 | .word $21CC,$21EC,$220C,$222C 65 | .word $224C,$226C,$228C,$22AC 66 | .word $22CC,$22EC,$230C,$232C 67 | 68 | copyLowStackRowToVram: 69 | sec 70 | lda #19 71 | sbc lowStackRowModifier 72 | asl 73 | tax 74 | lda vramPlayfieldRows+1,x 75 | sta PPUADDR 76 | lda vramPlayfieldRows,x 77 | sta PPUADDR 78 | ldx #$0A 79 | lda #LOW_STACK_LINE 80 | @drawLine: 81 | sta PPUDATA 82 | dex 83 | bne @drawLine 84 | rts 85 | 86 | copyPlayfieldRowToVRAM: 87 | ldx vramRow 88 | cpx #$15 89 | bpl @ret 90 | lda multBy10Table,x 91 | tay 92 | txa 93 | asl a 94 | tax 95 | inx 96 | lda vramPlayfieldRows,x 97 | sta PPUADDR 98 | dex 99 | 100 | lda vramPlayfieldRows,x 101 | sta PPUADDR 102 | @copyRow: 103 | ldx #$0A 104 | lda invisibleFlag 105 | bne @copyRowInvisible 106 | @copyByte: 107 | lda (playfieldAddr),y 108 | sta PPUDATA 109 | iny 110 | dex 111 | bne @copyByte 112 | @rowCopied: 113 | inc vramRow 114 | lda vramRow 115 | cmp #$14 116 | bmi @ret 117 | lda #$20 118 | sta vramRow 119 | @ret: rts 120 | 121 | @copyRowInvisible: 122 | lda #EMPTY_TILE 123 | @copyByteInvisible: 124 | sta PPUDATA 125 | dex 126 | bne @copyByteInvisible 127 | jmp @rowCopied 128 | -------------------------------------------------------------------------------- /src/palettes.asm: -------------------------------------------------------------------------------- 1 | ; ppu hi, ppu lo 2 | ; length 3 | ; palette data 4 | ; $FF 5 | 6 | game_palette: 7 | .byte $3F,$00 8 | .byte $20 9 | .byte $0F,$30,$12,$16 ; bg 10 | .byte $0F,$20,$12,$00 11 | .byte $0F,$2C,$16,$29 12 | .byte $0F,$3C,$00,$30 13 | .byte $0F,$16,$2A,$22 ; sprite 14 | .byte $0F,$10,$16,$2D 15 | .byte $0F,$2C,$16,$29 16 | .byte $0F,$3C,$00,$30 17 | .byte $FF 18 | title_palette: 19 | .byte $3F,$00 20 | .byte $14 21 | .byte $0F,$3C,$38,$00 ; bg 22 | .byte $0F,$17,$27,$37 23 | .byte $0F,$30,MENU_HIGHLIGHT_COLOR,$00 24 | .byte $0F,$22,$2A,$28 25 | .byte $0F,$30,$29,$27 ; sprite 26 | .byte $FF 27 | menu_palette: 28 | .byte $3F,$00 29 | .byte $16 30 | .byte $0F,$30,$38,$26 ; bg 31 | .byte $0F,$17,$27,$37 32 | .byte $0F,$30,MENU_HIGHLIGHT_COLOR,$00 33 | .byte $0F,$16,$2A,$28 34 | .byte $0F,$16,$26,$27 ; sprite 35 | .byte $0F,$2A 36 | .byte $FF 37 | rocket_palette: 38 | .byte $3F,$11 39 | .byte $07 40 | .if INES_MAPPER = 0 ; sprite 41 | .byte $2D,$30,$27 ; Ufo colors 42 | .else 43 | .byte $16,$2A,$28 ; Cathedral colors 44 | .endif 45 | .byte $0F,$37,$18,$38 46 | .byte $3F,$00 47 | .byte $08 48 | .byte $0F,$3C,$38,$00 ; bg 49 | .byte $0F,$20,$12,$15 50 | .byte $FF 51 | wait_palette: 52 | .byte $3F,$11 53 | .byte $01 54 | .byte $30 ; sprite 55 | .byte $3F,$00 56 | .byte $08 57 | .byte $0F,$30,$38,$26 ; bg 58 | .byte $0F,$17,$27,$37 59 | .byte $FF 60 | -------------------------------------------------------------------------------- /src/playstate/branch.asm: -------------------------------------------------------------------------------- 1 | branchOnPlayStatePlayer1: 2 | lda playState 3 | jsr switch_s_plus_2a 4 | .addr playState_unassignOrientationId 5 | .addr playState_playerControlsActiveTetrimino 6 | .addr playState_lockTetrimino 7 | .addr playState_checkForCompletedRows 8 | .addr playState_noop 9 | .addr playState_updateLinesAndStatistics 10 | .addr playState_prepareNext ; used to be bTypeGoalCheck 11 | .addr playState_receiveGarbage 12 | .addr playState_spawnNextTetrimino 13 | .addr playState_noop 14 | .addr playState_checkStartGameOver 15 | .addr playState_incrementPlayState 16 | 17 | playState_unassignOrientationId: 18 | lda #$13 19 | sta currentPiece 20 | rts 21 | 22 | playState_incrementPlayState: 23 | inc playState 24 | playState_noop: 25 | rts 26 | 27 | .include "active.asm" 28 | .include "lock.asm" 29 | .include "completedrows.asm" 30 | .include "updatestats.asm" 31 | .include "preparenext.asm" 32 | .include "garbage.asm" 33 | .include "spawnnext.asm" 34 | .include "gameover_rocket.asm" 35 | 36 | .include "util.asm" 37 | -------------------------------------------------------------------------------- /src/playstate/completedrows.asm: -------------------------------------------------------------------------------- 1 | activeFloorMode := generalCounter5 2 | 3 | playState_checkForCompletedRows: 4 | lda vramRow 5 | cmp #$20 6 | bpl @updatePlayfieldComplete 7 | jmp playState_checkForCompletedRows_return 8 | 9 | @updatePlayfieldComplete: 10 | 11 | lda tetriminoY 12 | sec 13 | sbc #$02 14 | bpl @yInRange 15 | lda #$00 16 | @yInRange: 17 | clc 18 | adc lineIndex 19 | sta generalCounter2 20 | asl a 21 | sta generalCounter 22 | asl a 23 | asl a 24 | clc 25 | adc generalCounter 26 | sta generalCounter 27 | tay 28 | lda #$00 29 | sta activeFloorMode ; Don't draw floor unless active 30 | ldx #$0A 31 | @checkIfRowComplete: 32 | .if AUTO_WIN 33 | jmp @rowIsComplete 34 | .endif 35 | lda practiseType 36 | cmp #MODE_TSPINS 37 | beq @rowNotComplete 38 | 39 | ; lda practiseType ; accumulator is still practiseType 40 | cmp #MODE_FLOOR 41 | beq @floorCheck 42 | lda linecapState 43 | cmp #LINECAP_FLOOR 44 | beq @fullRowBurningCheck 45 | bne @normalRow 46 | 47 | @floorCheck: 48 | lda currentFloor 49 | beq @rowNotComplete 50 | 51 | @fullRowBurningCheck: 52 | inc activeFloorMode ; Floor is active 53 | lda #$13 54 | sec 55 | sbc generalCounter2 ; contains current row being checked 56 | cmp currentFloor 57 | bcc @rowNotComplete ; ignore floor rows 58 | @normalRow: 59 | 60 | @checkIfRowCompleteLoopStart: 61 | lda (playfieldAddr),y 62 | cmp #EMPTY_TILE 63 | beq @rowNotComplete 64 | iny 65 | dex 66 | bne @checkIfRowCompleteLoopStart 67 | 68 | @rowIsComplete: 69 | ; sound effect $A to slot 1 used to live here 70 | inc completedLines 71 | ldx lineIndex 72 | lda generalCounter2 73 | sta completedRow,x 74 | ldy generalCounter 75 | dey 76 | @movePlayfieldDownOneRow: 77 | lda (playfieldAddr),y 78 | ldx #$0A 79 | stx playfieldAddr 80 | sta (playfieldAddr),y 81 | lda #$00 82 | sta playfieldAddr 83 | dey 84 | cpy #$FF 85 | bne @movePlayfieldDownOneRow 86 | lda #EMPTY_TILE 87 | ldy #$00 88 | @clearRowTopRow: 89 | sta (playfieldAddr),y 90 | iny 91 | cpy #$0A 92 | bne @clearRowTopRow 93 | lda #$13 94 | sta currentPiece 95 | 96 | ; draw surface of floor in case of top line clear 97 | lda activeFloorMode 98 | beq @incrementLineIndex 99 | lda #$14 100 | sec 101 | sbc currentFloor 102 | tax 103 | ldy multBy10Table,x 104 | ldx #$0A 105 | lda #BLOCK_TILES+3 106 | @drawFloorSurface: 107 | sta playfield,y 108 | iny 109 | dex 110 | bne @drawFloorSurface 111 | 112 | jmp @incrementLineIndex 113 | 114 | @rowNotComplete: 115 | ldx lineIndex 116 | lda #$00 117 | sta completedRow,x 118 | @incrementLineIndex: 119 | 120 | ; patch tapquantity data 121 | lda practiseType 122 | cmp #MODE_TAPQTY 123 | bne @tapQtyEnd 124 | lda completedLines 125 | ; cmp #0 ; lda sets z flag 126 | beq @tapQtyEnd 127 | ; mark as complete 128 | lda tqtyNext 129 | sta tqtyCurrent 130 | ; handle no burns 131 | lda tapqtyModifier 132 | and #$F0 133 | beq @tapQtyEnd 134 | lda #0 135 | sta vramRow 136 | inc playState 137 | inc playState 138 | lda #$07 139 | sta soundEffectSlot1Init 140 | rts 141 | @tapQtyEnd: 142 | 143 | ; update top row for crunch 144 | lda practiseType 145 | cmp #MODE_CRUNCH 146 | bne @crunchEnd 147 | jsr advanceSides ; clobbers generalCounter3 and generalCounter4 148 | @crunchEnd: 149 | 150 | lda completedLines 151 | beq :+ 152 | lda #$0A 153 | sta soundEffectSlot1Init 154 | : 155 | 156 | inc lineIndex 157 | lda lineIndex 158 | cmp #$04 ; check actual height 159 | bmi playState_checkForCompletedRows_return 160 | 161 | lda #$00 162 | sta vramRow 163 | sta rowY 164 | lda completedLines 165 | cmp #$04 166 | bne @skipTetrisSoundEffect 167 | lda #$04 168 | sta soundEffectSlot1Init 169 | @skipTetrisSoundEffect: 170 | inc playState 171 | lda completedLines 172 | bne playState_checkForCompletedRows_return 173 | @skipLines: 174 | playState_completeRowContinue: 175 | inc playState 176 | lda #$07 177 | sta soundEffectSlot1Init 178 | playState_checkForCompletedRows_return: 179 | rts 180 | -------------------------------------------------------------------------------- /src/playstate/garbage.asm: -------------------------------------------------------------------------------- 1 | playState_receiveGarbage: 2 | ldy pendingGarbage 3 | beq @ret 4 | lda multBy10Table,y 5 | sta generalCounter2 6 | lda #$00 7 | sta generalCounter 8 | @shiftPlayfieldUp: 9 | ldy generalCounter2 10 | lda (playfieldAddr),y 11 | ldy generalCounter 12 | sta (playfieldAddr),y 13 | inc generalCounter 14 | inc generalCounter2 15 | lda generalCounter2 16 | cmp #$C8 17 | bne @shiftPlayfieldUp 18 | iny 19 | 20 | ldx #$00 21 | @fillGarbage: 22 | cpx garbageHole 23 | beq @hole 24 | lda #BLOCK_TILES + 3 25 | jmp @set 26 | @hole: 27 | lda #EMPTY_TILE ; was $FF ? 28 | @set: 29 | sta (playfieldAddr),y 30 | inx 31 | cpx #$0A 32 | bne @inc 33 | ldx #$00 34 | @inc: iny 35 | cpy #$C8 36 | bne @fillGarbage 37 | lda #$00 38 | sta pendingGarbage 39 | sta vramRow 40 | @ret: inc playState 41 | lda #$00 ; earliest possible measured point 42 | sta hzSpawnDelay 43 | ldx #$83 ; -3 tap delay 44 | jsr checkNegativeDelay 45 | rts 46 | 47 | 48 | garbageLines: 49 | .byte $00,$00,$01,$02,$04 50 | -------------------------------------------------------------------------------- /src/playstate/lock.asm: -------------------------------------------------------------------------------- 1 | playState_lockTetrimino: 2 | jsr isPositionValid 3 | beq @notGameOver 4 | @gameOver: 5 | lda renderFlags ; Flag needed to reveal hidden score 6 | ora #RENDER_SCORE 7 | sta renderFlags 8 | lda #$02 9 | sta soundEffectSlot0Init 10 | lda #$0A ; playState_checkStartGameOver 11 | sta playState 12 | lda #$F0 13 | sta curtainRow 14 | jsr updateAudio2 15 | 16 | ; reset checkerboard score 17 | lda practiseType 18 | cmp #MODE_CHECKERBOARD 19 | bne @noChecker 20 | lda #0 21 | sta binScore 22 | sta binScore+1 23 | jsr setupScoreForRender 24 | @noChecker: 25 | ; make invisible tiles visible 26 | lda #$00 27 | sta invisibleFlag 28 | sta vramRow 29 | rts 30 | 31 | @notGameOver: 32 | lda vramRow 33 | cmp #$20 34 | bmi @ret 35 | lda tetriminoY 36 | asl a 37 | sta generalCounter 38 | asl a 39 | asl a 40 | clc 41 | adc generalCounter 42 | adc tetriminoX 43 | sta generalCounter 44 | lda currentPiece 45 | asl a 46 | asl a 47 | sta generalCounter2 48 | asl a 49 | clc 50 | adc generalCounter2 51 | tax 52 | ldy #$00 53 | lda #$04 54 | sta generalCounter3 55 | ; Copies a single square of the tetrimino to the playfield 56 | @lockSquare: 57 | lda orientationTable,x 58 | asl a 59 | sta generalCounter4 60 | asl a 61 | asl a 62 | clc 63 | adc generalCounter4 64 | clc 65 | adc generalCounter 66 | sta positionValidTmp 67 | inx 68 | lda orientationTable,x 69 | sta generalCounter5 70 | inx 71 | lda orientationTable,x 72 | clc 73 | adc positionValidTmp 74 | tay 75 | lda generalCounter5 76 | ; BLOCK_TILES 77 | sta (playfieldAddr),y 78 | inx 79 | dec generalCounter3 80 | bne @lockSquare 81 | lda practiseType 82 | cmp #MODE_LOWSTACK 83 | bne @notAboveLowStack 84 | jsr checkIfAboveLowStackLine 85 | bcc @notAboveLowStack 86 | ldx #lowStackNopeGraphic 88 | sec 89 | lda #19 90 | sbc lowStackRowModifier 91 | cmp #$09 92 | bcs @drawOnUpperHalf 93 | ; draw on lower half 94 | adc #$03 ; carry already clear 95 | bne @copyGraphic 96 | @drawOnUpperHalf: 97 | sbc #$04 ; carry already set 98 | @copyGraphic: 99 | jsr copyGraphicToPlayfieldAtCustomRow 100 | jmp @gameOver 101 | @notAboveLowStack: 102 | lda #$00 103 | sta lineIndex 104 | jsr updatePlayfield 105 | jsr updateMusicSpeed 106 | inc playState 107 | @ret: rts 108 | -------------------------------------------------------------------------------- /src/playstate/preparenext.asm: -------------------------------------------------------------------------------- 1 | playState_prepareNext: 2 | lda practiseType 3 | cmp #MODE_CHECKERBOARD 4 | bne @checkBType 5 | lda completedRow+3 6 | cmp #$13 7 | bne @endOfEndingCode 8 | jsr typeBEndingStuff 9 | rts 10 | 11 | ; bTypeGoalCheck 12 | @checkBType: 13 | cmp #MODE_TYPEB 14 | bne @endOfEndingCode 15 | lda lines 16 | bne @endOfEndingCode 17 | 18 | jsr typeBEndingStuff 19 | 20 | ; patch levelNumber with score multiplier 21 | ldx levelNumber 22 | stx tmp3 ; and save a copy 23 | lda levelDisplayTable, x 24 | and #$F 25 | clc 26 | adc typeBModifier 27 | sta levelNumber 28 | beq @typeBScoreDone 29 | dec levelNumber 30 | 31 | ; patch some stuff 32 | lda #$5 33 | sta completedLines 34 | jsr addPointsRaw 35 | 36 | ; restore level 37 | @typeBScoreDone: 38 | lda tmp3 39 | sta levelNumber 40 | 41 | rts 42 | @endOfEndingCode: 43 | 44 | lda linecapState 45 | cmp #LINECAP_HALT 46 | bne @linecapHaltEnd 47 | ldx #haltEndingGraphic 49 | 50 | lda crashState ; LINECAP_HALT set in testCrash 51 | cmp #$F0 52 | bne @nonCrash 53 | ldx #crashGraphic 55 | @nonCrash: 56 | jmp copyGraphic 57 | 58 | @linecapHaltEnd: 59 | jsr practisePrepareNext 60 | inc playState 61 | rts 62 | 63 | typeBEndingStuff: 64 | ldx #typebSuccessGraphic 66 | copyGraphic: 67 | jsr copyGraphicToPlayfield 68 | 69 | typeBEndingStuffEnd: 70 | ; play sfx 71 | lda #$4 72 | sta soundEffectSlot1Init 73 | 74 | lda renderFlags ; Flag needed to reveal hidden score 75 | ora #$4 76 | sta renderFlags 77 | lda #$0A ; playState_checkStartGameOver 78 | sta playState 79 | lda #$30 80 | jsr sleep_gameplay_nextSprite 81 | rts 82 | 83 | sleep_gameplay_nextSprite: 84 | sta sleepCounter 85 | jsr stageSpriteForNextPiece 86 | @loop: jsr updateAudioWaitForNmiAndResetOamStaging 87 | jsr stageSpriteForNextPiece 88 | lda sleepCounter 89 | bne @loop 90 | rts 91 | 92 | copyGraphicToPlayfield: 93 | lda #$09 ; default row 94 | copyGraphicToPlayfieldAtCustomRow: 95 | stx generalCounter 96 | sty generalCounter2 97 | tax 98 | lda multBy10Table,x 99 | clc 100 | adc #$02 ; indent 101 | tax 102 | ldy #$00 103 | @copySuccessGraphic: 104 | lda (generalCounter),y 105 | beq @graphicCopied 106 | sta playfield,x 107 | inx 108 | iny 109 | bne @copySuccessGraphic 110 | @graphicCopied: ; 0 in accumulator 111 | sta vramRow 112 | rts 113 | 114 | ; $28 is ! in game tileset 115 | lowStackNopeGraphic: 116 | .byte "N","O","P","E",$FF,$28,$00 117 | haltEndingGraphic: 118 | .byte $FF,'G','G',$FF,$28,$00 119 | typebSuccessGraphic: 120 | .byte 'N','I','C','E',$FF,$28,$00 121 | crashGraphic: 122 | .byte 'C','R','A','S','H',$28,$00 123 | -------------------------------------------------------------------------------- /src/playstate/util.asm: -------------------------------------------------------------------------------- 1 | isPositionValid: 2 | lda tetriminoY 3 | asl a 4 | sta generalCounter 5 | asl a 6 | asl a 7 | clc 8 | adc generalCounter 9 | adc tetriminoX 10 | sta generalCounter 11 | lda currentPiece 12 | asl a 13 | asl a 14 | sta generalCounter2 15 | asl a 16 | clc 17 | adc generalCounter2 18 | tax 19 | ldy #$00 20 | lda #$04 21 | sta generalCounter3 22 | ; Checks one square within the tetrimino 23 | @checkSquare: 24 | lda orientationTable,x 25 | clc 26 | adc tetriminoY 27 | adc #$02 28 | 29 | cmp #$16 30 | bcs @invalid 31 | lda orientationTable,x 32 | asl a 33 | sta generalCounter4 34 | asl a 35 | asl a 36 | clc 37 | adc generalCounter4 38 | clc 39 | adc generalCounter 40 | sta positionValidTmp 41 | inx 42 | inx 43 | lda orientationTable,x 44 | clc 45 | adc positionValidTmp 46 | tay 47 | lda (playfieldAddr),y 48 | cmp #EMPTY_TILE 49 | bcc @invalid 50 | lda orientationTable,x 51 | clc 52 | adc tetriminoX 53 | cmp #$0A 54 | bcs @invalid 55 | inx 56 | dec generalCounter3 57 | bne @checkSquare 58 | lda #$00 59 | sta generalCounter 60 | rts 61 | 62 | @invalid: 63 | lda #$FF 64 | sta generalCounter 65 | rts 66 | 67 | updatePlayfield: 68 | ldx tetriminoY 69 | dex 70 | dex 71 | txa 72 | bpl @rowInRange 73 | lda #$00 74 | @rowInRange: 75 | cmp vramRow 76 | bpl @ret 77 | sta vramRow 78 | @ret: rts 79 | 80 | crunchLeftColumns = generalCounter3 81 | crunchRightColumns = generalCounter4 82 | 83 | updateMusicSpeed: 84 | 85 | ; ldx #$05 86 | ; lda multBy10Table,x ;this piece of code is parameterized for no reason but the crash checking code relies on the index being 50-59 so if you ever optimize this part out of the code please also adjust the crash test, specifically the part which handles cycles for allegro. 87 | ; tay 88 | 89 | ldy #50 ; replaces above 90 | 91 | ; check if crunch mode 92 | ldx practiseType 93 | cpx #MODE_CRUNCH 94 | bne @notCrunch 95 | 96 | ; add crunch left columns to y 97 | jsr unpackCrunchModifier 98 | tya 99 | clc 100 | adc crunchLeftColumns ; offset y with left column count (generalCounter3) 101 | tay 102 | 103 | ; set x to playable column count 104 | lda #$0A 105 | sec 106 | sbc crunchLeftColumns ; generalCounter3 107 | sbc crunchRightColumns ; generalCounter4 108 | tax 109 | bne @checkForBlockInRow ; unconditional, expected range 4 - 10 110 | 111 | @notCrunch: 112 | ldx #$0A 113 | @checkForBlockInRow: 114 | lda (playfieldAddr),y 115 | cmp #EMPTY_TILE 116 | bne @foundBlockInRow 117 | iny 118 | dex 119 | bne @checkForBlockInRow 120 | lda allegro 121 | sta wasAllegro 122 | beq @ret 123 | lda #$00 124 | sta allegro 125 | ldx musicType 126 | lda musicSelectionTable,x 127 | jsr setMusicTrack 128 | jmp @ret 129 | 130 | @foundBlockInRow: 131 | sty allegroIndex 132 | lda allegro 133 | sta wasAllegro 134 | bne @ret 135 | lda #$FF 136 | sta allegro 137 | lda musicType 138 | clc 139 | adc #$04 140 | tax 141 | lda musicSelectionTable,x 142 | jsr setMusicTrack 143 | @ret: rts 144 | 145 | checkIfAboveLowStackLine: 146 | ; carry set - block found 147 | sec 148 | lda #19 149 | sbc lowStackRowModifier 150 | tax 151 | ldy multBy10Table,x 152 | ldx #$0A 153 | sec 154 | @checkForBlockInRow: 155 | lda playfield,y 156 | bpl @foundBlockInRow 157 | iny 158 | dex 159 | bne @checkForBlockInRow 160 | clc 161 | @foundBlockInRow: 162 | rts 163 | 164 | ; canon is adjustMusicSpeed 165 | setMusicTrack: 166 | .if !NO_MUSIC 167 | sta musicTrack 168 | lda gameMode 169 | cmp #$05 170 | bne @ret 171 | lda #$FF 172 | sta musicTrack 173 | .endif 174 | @ret: rts 175 | -------------------------------------------------------------------------------- /src/presets/presets.asm: -------------------------------------------------------------------------------- 1 | presets: 2 | .byte preset0-presets 3 | .byte preset1-presets 4 | .byte preset2-presets 5 | .byte preset3-presets 6 | .byte preset4-presets 7 | .byte preset5-presets 8 | .byte preset6-presets 9 | .byte preset7-presets 10 | preset0: 11 | .byte $7b, $9d, $a6, $ab, $ac, $b1, $b4, $ba, $bf, $c5, $ff 12 | preset1: 13 | .byte $6e, $aa, $ab, $ac, $b4, $b5, $bb, $be, $c4, $ff 14 | preset2: 15 | .byte $7e, $aa, $b3, $be, $c0, $c5, $c7, $ff 16 | preset3: 17 | .byte $3f, $88, $89, $9c, $9d, $b0, $b1, $c4, $c5, $ff 18 | preset4: 19 | .byte $5d, $9a, $9c, $a5, $af, $b6, $b8, $ba, $bc, $c0, $c2, $c4, $c6, $ff 20 | preset5: 21 | .byte $1c, $98, $a7, $ac, $b0, $ba, $be, $c0, $c2, $c5, $c6, $ff 22 | preset6: 23 | .byte $5d, $aa, $ab, $b2, $b3, $be, $bf, $c6, $c7, $ff 24 | preset7: 25 | .byte $5d, $a0, $a1, $a8, $a9, $aa, $b3, $b4, $bd, $ff 26 | -------------------------------------------------------------------------------- /src/presets/presets.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | [[ 3 | ` X`, 4 | ` X`, 5 | ` XX X`, 6 | `X X`, 7 | ` X X`, 8 | ], 'Z'], 9 | [[ 10 | `XXX`, 11 | `XX X`, 12 | `X X`, 13 | ], 'ST'], 14 | [[ 15 | `X X`, 16 | ` `, 17 | `X X X X`, 18 | ], 'T'], 19 | [[ 20 | ` XX `, 21 | ` `, 22 | ` XX `, 23 | ``, 24 | ` XX `, 25 | ` `, 26 | ` XX `, 27 | ], 'I'], 28 | [[ 29 | ` X X`, 30 | ` X`, 31 | ` X`, 32 | ` X X X X`, 33 | ` X X X X`, 34 | ], 'JL'], 35 | [[ 36 | ` `, 37 | ` `, 38 | ` `, 39 | ` `, 40 | ` `, 41 | ` `, 42 | ` `, 43 | ` `, 44 | ` `, 45 | ` `, 46 | ` `, 47 | ` `, 48 | ` `, 49 | ` `, 50 | ` `, 51 | ` X `, 52 | ` X `, 53 | ` X X `, 54 | ` X `, 55 | `X X X XX `, 56 | ], 'ILJT'], 57 | [[ 58 | `XX XX`, 59 | ``, 60 | `XX XX`, 61 | ], 'JL'], 62 | [[ 63 | `XX XX`, 64 | `X X`, 65 | `X X`, 66 | ` `, 67 | ], 'JL'], 68 | ]; 69 | const tab = ' '; 70 | let out = 'presets:\n'; 71 | presets.forEach((_, i) => { 72 | out += `${tab} .byte preset${i}-presets\n`; 73 | }); 74 | 75 | let total = presets.length; 76 | 77 | const pieceHash = (str) => { 78 | let out = [...'1'.repeat(7)]; 79 | [...str].forEach(ch => out['ILSOZJT'.indexOf(ch)] = '0') 80 | return Number('0b' + out.join``); 81 | }; 82 | 83 | presets.forEach(([preset, pieces], i) => { 84 | preset.length < 20 && 85 | preset.splice(0, 0, ...Array.from({ length: 20 - preset.length }, () => '')); 86 | preset = preset.map(d => d.padEnd(10, ' ')); 87 | const bytes = [...preset.join('')] 88 | .map((ch, i) => ch === 'X' ? i : ' ') 89 | .filter(d => d !== ' '); 90 | out += `preset${i}:\n`; 91 | const bytesList = [ 92 | pieceHash(pieces), 93 | ...bytes, 94 | 0xFF, 95 | ]; 96 | out += `${tab} .byte ${bytesList.map(b => '$' + b.toString(16)).join(', ')}\n`; 97 | total += bytesList.length; 98 | }); 99 | 100 | console.log(out); 101 | require('fs').writeFileSync(__dirname + '/presets.asm', out, 'utf8'); 102 | 103 | console.log(`>> bytes: ${total}`) 104 | -------------------------------------------------------------------------------- /src/reset.asm: -------------------------------------------------------------------------------- 1 | ; incremented to reset MMC1 reg 2 | reset: cld 3 | sei 4 | ldx #$00 5 | stx PPUCTRL 6 | stx PPUMASK 7 | 8 | ; init code from https://www.nesdev.org/wiki/Init_code 9 | bit PPUSTATUS 10 | @vsyncWait1: 11 | bit PPUSTATUS 12 | bpl @vsyncWait1 13 | 14 | ; zero out pages 0 through 6 while waiting 15 | txa 16 | @clrmem: 17 | sta $0000,x 18 | sta $0100,x 19 | sta $0200,x 20 | sta $0300,x 21 | sta $0400,x 22 | sta $0500,x 23 | sta $0600,x 24 | inx 25 | bne @clrmem 26 | 27 | bit PPUSTATUS 28 | @vsyncWait2: 29 | bit PPUSTATUS 30 | bpl @vsyncWait2 31 | 32 | dex ; $FF for stack pointer 33 | txs 34 | jsr mapperInit 35 | jsr setHorizontalMirroring 36 | .if INES_MAPPER <> 0 37 | lda #CHRBankSet0 38 | jsr changeCHRBanks 39 | .endif 40 | jmp initRam 41 | 42 | .macro setMMC1PRG 43 | RESET_MMC1 44 | lda #$00 45 | sta MMC1_PRG 46 | lsr a 47 | sta MMC1_PRG 48 | lsr a 49 | sta MMC1_PRG 50 | lsr a 51 | sta MMC1_PRG 52 | lsr a 53 | sta MMC1_PRG 54 | .endmacro 55 | 56 | mapperInit: 57 | ; autodetect 58 | .if INES_MAPPER = 1000 59 | setMMC1PRG ; initialize mmc1 just in case 60 | ; cnrom can pass one of these tests but not both. 61 | ; Start with the one it's supposed to fail. 62 | jsr testVerticalMirroring 63 | bne not_mmc1 64 | jsr testHorizontalMirroring 65 | bne not_mmc1 66 | inc mapperId ; 1 for MMC1, otherwise 0 for CNROM 67 | not_mmc1: 68 | 69 | ; NROM (no init) 70 | .elseif INES_MAPPER = 0 71 | 72 | ; MMC1 73 | .elseif INES_MAPPER = 1 74 | setMMC1PRG 75 | 76 | ; CNROM (no init) 77 | .elseif INES_MAPPER = 3 78 | 79 | ; MMC3 80 | .elseif INES_MAPPER = 4 81 | ; https://www.nesdev.org/wiki/MMC3 82 | ; 110: R6: Select 8 KB PRG ROM bank at $8000-$9FFF (or $C000-$DFFF) 83 | ldx #$06 84 | ldy #$00 85 | stx MMC3_BANK_SELECT 86 | sty MMC3_BANK_DATA 87 | 88 | ; 111: R7: Select 8 KB PRG ROM bank at $A000-$BFFF 89 | inx 90 | iny 91 | stx MMC3_BANK_SELECT 92 | sty MMC3_BANK_DATA 93 | lda #$80 ; enable PRG RAM 94 | sta MMC3_PRG_RAM 95 | rts 96 | 97 | ; MMC5 98 | .elseif INES_MAPPER = 5 99 | ; https://www.nesdev.org/wiki/MMC5 100 | ldx #$00 101 | stx MMC5_PRG_MODE ; 0: 1 32Kb bank 102 | inx 103 | stx MMC5_CHR_MODE ; 1: 4kb CHR pages 104 | stx MMC5_RAM_PROTECT2 ; 1: enable PRG RAM 105 | inx 106 | stx MMC5_RAM_PROTECT1 ; 2: enable PRG RAM 107 | .endif 108 | rts 109 | -------------------------------------------------------------------------------- /src/sprites/bytesprite.asm: -------------------------------------------------------------------------------- 1 | byteSprite: 2 | menuXTmp := tmp2 3 | ldy #0 4 | @loop: 5 | tya 6 | asl 7 | asl 8 | asl 9 | asl 10 | adc spriteXOffset 11 | sta menuXTmp 12 | 13 | ldx oamStagingLength 14 | lda spriteYOffset 15 | sta oamStaging, x 16 | inx 17 | lda (byteSpriteAddr), y 18 | and #$F0 19 | lsr a 20 | lsr a 21 | lsr a 22 | lsr a 23 | adc byteSpriteTile 24 | sta oamStaging, x 25 | inx 26 | lda #$00 27 | sta oamStaging, x 28 | inx 29 | lda menuXTmp 30 | sta oamStaging, x 31 | inx 32 | 33 | lda spriteYOffset 34 | sta oamStaging, x 35 | inx 36 | lda (byteSpriteAddr), y 37 | and #$F 38 | adc byteSpriteTile 39 | sta oamStaging, x 40 | inx 41 | lda #$00 42 | sta oamStaging, x 43 | inx 44 | lda menuXTmp 45 | adc #$8 46 | sta oamStaging, x 47 | inx 48 | 49 | ; increase OAM index 50 | lda #$08 51 | clc 52 | adc oamStagingLength 53 | sta oamStagingLength 54 | 55 | iny 56 | cpy byteSpriteLen 57 | bne @loop 58 | 59 | rts 60 | -------------------------------------------------------------------------------- /src/sprites/drawrect.asm: -------------------------------------------------------------------------------- 1 | .if INES_MAPPER = 0 2 | ; compact graphic for smaller tileset capacity 3 | spriteCathedral: ; top of UFO 4 | .byte $00, $00, $02, $01, $00, $94, $FF 5 | 6 | spriteCathedralFire0: ; bottom of UFO 1 7 | .byte $00, $08, $02, $01, $00, $A4, $FF 8 | 9 | spriteCathedralFire1: ; bottom of UFO 2 10 | .byte $00, $08, $02, $01, $00, $64, $FF 11 | 12 | .else 13 | 14 | spriteCathedral: 15 | .byte $20, $0, $1, $1, $20, $30 16 | .byte $8, $8, $7, $1, $20, $31 17 | .byte $0, $10, $8, $6, $20, $40 18 | .byte $FF 19 | 20 | spriteCathedralFire0: 21 | .byte $8, $0, $2, $1, $1, $A0, $FF 22 | 23 | spriteCathedralFire1: 24 | .byte $0, $0, $4, $2, $1, $A2, $FF 25 | .endif 26 | 27 | rectBuffer := generalCounter 28 | rectX := rectBuffer+0 29 | rectY := rectBuffer+1 30 | rectW := rectBuffer+2 31 | rectH := rectBuffer+3 32 | rectAttr := rectBuffer+4 33 | rectAddr := rectBuffer+5 ; positionValidTmp 34 | 35 | ; addr in tmp2 36 | ; .byte [x, y, width, height, attr, addr]+ $FF 37 | loadRectIntoOamStaging: 38 | ldy #0 39 | @copyRect: 40 | ldx #0 41 | @copyRectLoop: 42 | lda ($0), y 43 | cmp #$FF 44 | beq @ret 45 | sta rectBuffer, x 46 | iny 47 | inx 48 | cpx #6 49 | bne @copyRectLoop 50 | 51 | @writeLine: 52 | lda rectX 53 | sta tmpX 54 | lda rectW 55 | sta tmpY 56 | 57 | @writeTile: 58 | ; YTAX 59 | ldx oamStagingLength 60 | 61 | lda rectY 62 | clc 63 | adc spriteYOffset 64 | sta oamStaging,x 65 | lda rectAddr 66 | sta oamStaging+1,x 67 | lda rectAttr 68 | sta oamStaging+2,x 69 | lda rectX 70 | clc 71 | adc spriteXOffset 72 | sta oamStaging+3,x 73 | 74 | ; increase OAM index 75 | lda #$4 76 | clc 77 | adc oamStagingLength 78 | sta oamStagingLength 79 | 80 | ; next rightwards tile 81 | lda #$8 82 | clc 83 | adc rectX 84 | sta rectX 85 | inc rectAddr 86 | 87 | dec rectW 88 | lda rectW 89 | bne @writeTile 90 | 91 | ; start a new line 92 | lda tmpX 93 | sta rectX 94 | lda tmpY 95 | sta rectW 96 | 97 | lda rectAddr 98 | sec 99 | sbc rectW 100 | clc 101 | adc #$10 102 | sta rectAddr 103 | 104 | lda #$8 105 | clc 106 | adc rectY 107 | sta rectY 108 | 109 | dec rectH 110 | lda rectH 111 | bne @writeLine 112 | 113 | ; do another rect 114 | jmp @copyRect 115 | @ret: 116 | rts 117 | -------------------------------------------------------------------------------- /src/sprites/piece.asm: -------------------------------------------------------------------------------- 1 | stageSpriteForCurrentPiece: 2 | lda #$0 3 | sta pieceTileModifier 4 | jsr stageSpriteForCurrentPiece_actual 5 | 6 | lda practiseType 7 | cmp #MODE_HARDDROP 8 | beq ghostPiece 9 | rts 10 | 11 | ghostPiece: 12 | lda playState 13 | cmp #3 14 | bpl @noGhost 15 | lda tetriminoY 16 | sta tmp3 17 | @loop: 18 | inc tetriminoY 19 | jsr isPositionValid 20 | beq @loop 21 | dec tetriminoY 22 | 23 | ; check if equal to current position 24 | lda tetriminoY 25 | cmp tmp3 26 | beq @noGhost 27 | 28 | lda frameCounter 29 | and #1 30 | asl 31 | asl 32 | adc #$0D 33 | sta pieceTileModifier 34 | jsr stageSpriteForCurrentPiece_actual 35 | lda tmp3 36 | sta tetriminoY 37 | @noGhost: 38 | rts 39 | 40 | tileModifierForCurrentPiece: 41 | lda pieceTileModifier 42 | beq @tileNormal 43 | and #$80 44 | bne @tileSingle 45 | ; @tileMultiple: 46 | lda orientationTable,x 47 | clc 48 | adc pieceTileModifier 49 | rts 50 | @tileSingle: 51 | lda pieceTileModifier 52 | rts 53 | @tileNormal: 54 | lda orientationTable,x 55 | rts 56 | 57 | stageSpriteForCurrentPiece_actual: 58 | lda tetriminoX 59 | cmp #TETRIMINO_X_HIDE 60 | beq stageSpriteForCurrentPiece_return 61 | asl a 62 | asl a 63 | asl a 64 | adc #$60 65 | sta generalCounter3 66 | clc 67 | lda tetriminoY 68 | rol a 69 | rol a 70 | rol a 71 | adc #$2F 72 | sta generalCounter4 73 | lda currentPiece 74 | sta generalCounter5 75 | clc 76 | lda generalCounter5 77 | rol a 78 | rol a 79 | sta generalCounter 80 | rol a 81 | adc generalCounter 82 | tax 83 | ldy oamStagingLength 84 | lda #$04 85 | sta generalCounter2 86 | @stageMino: 87 | lda orientationTable,x 88 | asl a 89 | asl a 90 | asl a 91 | clc 92 | adc generalCounter4 93 | sta oamStaging,y 94 | sta originalY 95 | inc oamStagingLength 96 | iny 97 | inx 98 | jsr tileModifierForCurrentPiece ; used to just load from orientationTable 99 | ; lda orientationTable, x 100 | sta oamStaging,y 101 | inc oamStagingLength 102 | iny 103 | inx 104 | lda #$02 105 | sta oamStaging,y 106 | lda originalY 107 | cmp #$2F 108 | bcs @validYCoordinate 109 | inc oamStagingLength 110 | dey 111 | lda #$FF 112 | sta oamStaging,y 113 | iny 114 | iny 115 | lda #$00 116 | sta oamStaging,y 117 | jmp @finishLoop 118 | 119 | @validYCoordinate: 120 | inc oamStagingLength 121 | iny 122 | lda orientationTable,x 123 | asl a 124 | asl a 125 | asl a 126 | clc 127 | adc generalCounter3 128 | sta oamStaging,y 129 | @finishLoop: 130 | inc oamStagingLength 131 | iny 132 | inx 133 | dec generalCounter2 134 | bne @stageMino 135 | stageSpriteForCurrentPiece_return: 136 | rts 137 | 138 | stageSpriteForNextPiece: 139 | lda hideNextPiece 140 | bne @maybeDisplayNextPiece 141 | 142 | @displayNextPiece: 143 | lda #$C8 144 | sta spriteXOffset 145 | lda #$77 146 | sta spriteYOffset 147 | ldx nextPiece 148 | lda tetriminoTypeFromOrientation,x 149 | clc 150 | adc #$6 ; piece sprites start at index 6 151 | sta spriteIndexInOamContentLookup 152 | jmp loadSpriteIntoOamStaging 153 | 154 | @maybeDisplayNextPiece: 155 | lda practiseType 156 | cmp #MODE_HARDDROP 157 | beq @displayNextPiece 158 | lda debugFlag 159 | bne @displayNextPiece 160 | rts 161 | -------------------------------------------------------------------------------- /src/tetris.nes.cfg: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | ZP: start = $0000, size = $0100, type = rw, file = ""; 3 | #OAM: start = $0200, size = $0100, type = rw, file = ""; 4 | RAM: start = $0100, size = $1F00, type = rw, file = ""; 5 | HDR: start = $0000, size = $0010, type = ro, fill = yes, fillval = $00; 6 | PRG: start = $8000, size = $8000, type = ro, fill = yes, fillval = $00; 7 | CHR: start = $0000, size = $4000, type = ro, fill = yes, fillval = $00; 8 | } 9 | 10 | SEGMENTS { 11 | ZEROPAGE: load = ZP; 12 | BSS: load = RAM, type = bss; 13 | HEADER: load = HDR, type = ro; 14 | CHR: load = CHR, type = ro; 15 | PRG_chunk1: load = PRG, type = ro, align = $100; 16 | PRG_chunk3: load = PRG, type = ro, start = $FF00; 17 | VECTORS: load = PRG, type = ro, start = $FFFA; 18 | } 19 | -------------------------------------------------------------------------------- /src/util/autodetect.asm: -------------------------------------------------------------------------------- 1 | ; Mapper detect code by pinobatch. Only relevant code has been copied and has been modified. 2 | ; Original code: https://github.com/pinobatch/holy-mapperel/blob/60ea5c0d97dedca1522525b054012b7c8526f1e1/src/mapper_detect.s 3 | 4 | ; Original notice: 5 | 6 | ; Mapper detection for Holy Mapperel 7 | ; 8 | ; Copyright 2013-2017 Damian Yerrick 9 | ; 10 | ; This software is provided 'as-is', without any express or implied 11 | ; warranty. In no event will the authors be held liable for any damages 12 | ; arising from the use of this software. 13 | ; 14 | ; Permission is granted to anyone to use this software for any purpose, 15 | ; including commercial applications, and to alter it and redistribute it 16 | ; freely, subject to the following restrictions: 17 | ; 18 | ; 1. The origin of this software must not be misrepresented; you must not 19 | ; claim that you wrote the original software. If you use this software 20 | ; in a product, an acknowledgment in the product documentation would be 21 | ; appreciated but is not required. 22 | ; 2. Altered source versions must be plainly marked as such, and must not be 23 | ; misrepresented as being the original software. 24 | ; 3. This notice may not be removed or altered from any source distribution. 25 | ; 26 | 27 | MIRRPROBE_V = %0101 28 | MIRRPROBE_H = %0011 29 | 30 | testHorizontalMirroring: 31 | inc mapperId 32 | jsr setHorizontalMirroring 33 | dec mapperId 34 | jsr testMirroring 35 | cmp #MIRRPROBE_H 36 | rts 37 | 38 | testVerticalMirroring: 39 | inc mapperId 40 | jsr setVerticalMirroring 41 | dec mapperId 42 | jsr testMirroring 43 | cmp #MIRRPROBE_V 44 | rts 45 | 46 | testMirroring: 47 | ; write_mirror_probe_vals 48 | ldx #$20 49 | ldy #$00 50 | stx PPUADDR 51 | sty PPUADDR 52 | sty PPUDATA 53 | ldx #$2C 54 | stx PPUADDR 55 | sty PPUADDR 56 | iny 57 | sty PPUDATA 58 | 59 | ; read_mirror_probe_vals 60 | ldx #$20 ; src address high 61 | ldy #$00 ; src address low 62 | lda #$10 ; ring counter loop: finish once the 1 gets rotated out 63 | readloop: 64 | pha 65 | stx PPUADDR 66 | inx 67 | sty PPUADDR 68 | inx 69 | bit PPUDATA 70 | inx 71 | lda PPUDATA 72 | inx 73 | lsr a 74 | pla 75 | rol a 76 | bcc readloop 77 | rts 78 | -------------------------------------------------------------------------------- /src/util/check_region.asm: -------------------------------------------------------------------------------- 1 | checkRegion: 2 | .assert >@vwait1 = >@endVWait, error, "Region detection code crosses page boundary" 3 | 4 | ; region detection via http://forums.nesdev.com/viewtopic.php?p=163258#p163258 5 | ;;; use the power-on wait to detect video system- 6 | ldx #0 7 | stx palFlag ; extra zeroing 8 | ldy #0 9 | @vwait1: 10 | bit $2002 11 | bpl @vwait1 ; at this point, about 27384 cycles have passed 12 | @vwait2: 13 | inx 14 | bne @noincy 15 | iny 16 | @noincy: 17 | bit $2002 18 | bpl @vwait2 ; at this point, about 57165 cycles have passed 19 | @endVWait: 20 | 21 | ;;; BUT because of a hardware oversight, we might have missed a vblank flag. 22 | ;;; so we need to both check for 1Vbl and 2Vbl 23 | ;;; NTSC NES: 29780 cycles / 12.005 -> $9B0 or $1361 (Y:X) 24 | ;;; PAL NES: 33247 cycles / 12.005 -> $AD1 or $15A2 25 | ;;; Dendy: 35464 cycles / 12.005 -> $B8A or $1714 26 | 27 | tya 28 | cmp #16 29 | bcc @nodiv2 30 | lsr 31 | @nodiv2: 32 | clc 33 | adc #<-9 34 | cmp #3 35 | bcc @noclip3 36 | lda #3 37 | @noclip3: 38 | ;;; Right now, A contains 0,1,2,3 for NTSC,PAL,Dendy,Bad 39 | cmp #0 40 | beq @ntsc 41 | lda #1 42 | sta palFlag 43 | @ntsc: 44 | rts 45 | -------------------------------------------------------------------------------- /src/util/mapper.asm: -------------------------------------------------------------------------------- 1 | .macro changeCHRBanksMMC1 2 | RESET_MMC1 3 | sta MMC1_CHR0 4 | lsr a 5 | sta MMC1_CHR0 6 | lsr a 7 | sta MMC1_CHR0 8 | lsr a 9 | sta MMC1_CHR0 10 | lsr a 11 | sta MMC1_CHR0 12 | inc generalCounter 13 | lda generalCounter 14 | RESET_MMC1 15 | sta MMC1_CHR1 16 | lsr a 17 | sta MMC1_CHR1 18 | lsr a 19 | sta MMC1_CHR1 20 | lsr a 21 | sta MMC1_CHR1 22 | lsr a 23 | sta MMC1_CHR1 24 | .endmacro 25 | 26 | .macro changeCHRBanksCNRom 27 | lsr 28 | tax 29 | sta cnromBanks,x 30 | .endmacro 31 | 32 | _setMMC1Control: 33 | RESET_MMC1 34 | sta MMC1_Control 35 | lsr a 36 | sta MMC1_Control 37 | lsr a 38 | sta MMC1_Control 39 | lsr a 40 | sta MMC1_Control 41 | lsr a 42 | sta MMC1_Control 43 | rts 44 | 45 | changeCHRBanks: 46 | ; accum should be 0 or 2 (CHRBankset0 or CHRBankset1) 47 | sta generalCounter 48 | 49 | ; autodetect 50 | .if INES_MAPPER = 1000 51 | ldx mapperId 52 | beq @cnrom 53 | changeCHRBanksMMC1 54 | rts 55 | @cnrom: 56 | changeCHRBanksCNRom 57 | 58 | ; NROM (no action) 59 | .elseif INES_MAPPER = 0 60 | 61 | ; MMC1 62 | .elseif INES_MAPPER = 1 63 | changeCHRBanksMMC1 64 | 65 | ; CNROM 66 | .elseif INES_MAPPER = 3 67 | changeCHRBanksCNRom 68 | 69 | ; MMC3 70 | .elseif INES_MAPPER = 4 71 | asl a 72 | asl a 73 | ldx #$00 74 | stx MMC3_BANK_SELECT 75 | sta MMC3_BANK_DATA 76 | inx 77 | clc 78 | adc #$02 79 | stx MMC3_BANK_SELECT 80 | sta MMC3_BANK_DATA 81 | inc generalCounter 82 | lda generalCounter 83 | asl a 84 | asl a 85 | ldx #$02 86 | stx MMC3_BANK_SELECT 87 | sta MMC3_BANK_DATA 88 | inx 89 | clc 90 | adc #$01 91 | stx MMC3_BANK_SELECT 92 | sta MMC3_BANK_DATA 93 | inx 94 | clc 95 | adc #$01 96 | stx MMC3_BANK_SELECT 97 | sta MMC3_BANK_DATA 98 | inx 99 | clc 100 | adc #$01 101 | stx MMC3_BANK_SELECT 102 | sta MMC3_BANK_DATA 103 | 104 | ; MMC5 105 | .elseif INES_MAPPER = 5 106 | sta MMC5_CHR_BANK0 107 | inc generalCounter 108 | lda generalCounter 109 | sta MMC5_CHR_BANK1 110 | .endif 111 | rts 112 | 113 | 114 | setHorizontalMirroring: 115 | ; autodetect 116 | .if INES_MAPPER = 1000 117 | lda mapperId 118 | beq @cnrom 119 | lda #%10011 120 | jmp _setMMC1Control 121 | @cnrom: 122 | 123 | ; NROM (no action) 124 | .elseif INES_MAPPER = 0 125 | 126 | ; MMC1 127 | .elseif INES_MAPPER = 1 128 | lda #%10011 129 | jmp _setMMC1Control 130 | 131 | ; CNROM (no action) 132 | .elseif INES_MAPPER = 3 133 | 134 | ; MMC3 135 | .elseif INES_MAPPER = 4 136 | lda #$1 137 | sta MMC3_MIRRORING 138 | 139 | ; MMC5 140 | .elseif INES_MAPPER = 5 141 | lda #$50 142 | sta MMC5_NT_MAPPING 143 | .endif 144 | rts 145 | 146 | setVerticalMirroring: 147 | ; Unused except during mapper detect for INES_MAPPER 1000 148 | 149 | ; autodetect 150 | .if INES_MAPPER = 1000 151 | lda mapperId 152 | beq @cnrom 153 | lda #%10010 154 | jmp _setMMC1Control 155 | @cnrom: 156 | 157 | ; NROM (no action) 158 | .elseif INES_MAPPER = 0 159 | 160 | ; MMC1 161 | .elseif INES_MAPPER = 1 162 | lda #%10010 163 | jmp _setMMC1Control 164 | 165 | ; CNROM (no action) 166 | .elseif INES_MAPPER = 3 167 | 168 | ; MMC3 169 | .elseif INES_MAPPER = 4 170 | lda #$0 171 | sta MMC3_MIRRORING 172 | 173 | ; MMC5 174 | .elseif INES_MAPPER = 5 175 | lda #$44 176 | sta MMC5_NT_MAPPING 177 | .endif 178 | rts 179 | 180 | .if INES_MAPPER = 3 .or INES_MAPPER = 1000 181 | ; bus conflict workaround 182 | cnromBanks: 183 | .byte $00,$01 184 | .endif 185 | -------------------------------------------------------------------------------- /src/util/menuthrottle.asm: -------------------------------------------------------------------------------- 1 | menuThrottle: ; add DAS-like movement to the menu 2 | sta menuThrottleTmp 3 | lda newlyPressedButtons_player1 4 | cmp menuThrottleTmp 5 | beq menuThrottleNew 6 | lda heldButtons_player1 7 | cmp menuThrottleTmp 8 | bne @endThrottle 9 | dec menuMoveThrottle 10 | beq menuThrottleContinue 11 | @endThrottle: 12 | lda #0 13 | rts 14 | 15 | menuThrottleStart := $10 16 | menuThrottleRepeat := $4 17 | menuThrottleNew: 18 | lda #menuThrottleStart 19 | sta menuMoveThrottle 20 | rts 21 | menuThrottleContinue: 22 | lda #menuThrottleRepeat 23 | sta menuMoveThrottle 24 | rts 25 | -------------------------------------------------------------------------------- /src/util/modetext.asm: -------------------------------------------------------------------------------- 1 | displayModeText: 2 | ldx practiseType 3 | cpx #MODE_SEED 4 | bne @drawModeName 5 | ; draw seed instead 6 | lda tmp1 7 | sta PPUADDR 8 | lda tmp2 9 | sta PPUADDR 10 | lda set_seed_input 11 | jsr twoDigsToPPU 12 | lda set_seed_input+1 13 | jsr twoDigsToPPU 14 | lda set_seed_input+2 15 | jsr twoDigsToPPU 16 | rts 17 | 18 | @drawModeName: 19 | ; ldx practiseType 20 | lda #0 21 | @loopAddr: 22 | cpx #0 23 | beq @addr 24 | clc 25 | adc #6 26 | dex 27 | jmp @loopAddr 28 | @addr: 29 | ; offset in X 30 | tax 31 | 32 | lda tmp1 33 | sta PPUADDR 34 | lda tmp2 35 | sta PPUADDR 36 | 37 | ldy #6 38 | @writeChar: 39 | lda modeText, x 40 | sta PPUDATA 41 | inx 42 | dey 43 | bne @writeChar 44 | rts 45 | -------------------------------------------------------------------------------- /src/util/strings.asm: -------------------------------------------------------------------------------- 1 | stringBackground: 2 | ldx stringIndexLookup 3 | lda stringLookup, x 4 | tax 5 | lda stringLookup, x 6 | sta tmpZ 7 | inx 8 | ldy #0 9 | @loop: 10 | lda stringLookup, x 11 | sta PPUDATA 12 | inx 13 | iny 14 | cpy tmpZ 15 | bne @loop 16 | rts 17 | 18 | stringSprite: 19 | ldx spriteIndexInOamContentLookup 20 | lda stringLookup, x 21 | tax 22 | lda stringLookup, x 23 | sta tmpZ 24 | inx 25 | lda spriteXOffset 26 | sta tmpX 27 | jmp stringSpriteLoop 28 | 29 | stringSpriteAlignRight: 30 | ldx spriteIndexInOamContentLookup 31 | lda stringLookup, x 32 | tax 33 | lda stringLookup, x 34 | inx 35 | sta tmpZ 36 | lda tmpZ 37 | asl 38 | asl 39 | asl 40 | sta tmpX 41 | clc 42 | lda spriteXOffset 43 | sbc tmpX 44 | sta tmpX 45 | 46 | stringSpriteLoop: 47 | ldy oamStagingLength 48 | sec 49 | lda spriteYOffset 50 | sta oamStaging, y 51 | lda stringLookup, x 52 | inx 53 | sta oamStaging+1, y 54 | lda #$00 55 | sta oamStaging+2, y 56 | lda tmpX 57 | sta oamStaging+3, y 58 | clc 59 | adc #$8 60 | sta tmpX 61 | ; increase OAM index 62 | lda #$04 63 | clc 64 | adc oamStagingLength 65 | sta oamStagingLength 66 | 67 | dec tmpZ 68 | lda tmpZ 69 | bne stringSpriteLoop 70 | rts 71 | 72 | stringLookup: 73 | .byte stringClassic-stringLookup 74 | .byte stringLetters-stringLookup 75 | .byte stringSevenDigit-stringLookup 76 | .byte stringFloat-stringLookup 77 | .byte stringScorecap-stringLookup 78 | .byte stringHidden-stringLookup 79 | .byte stringNull-stringLookup ; reserved for future use 80 | .byte stringNull-stringLookup 81 | .byte stringOff-stringLookup ; 8 82 | .byte stringOn-stringLookup 83 | .byte stringPause-stringLookup 84 | .byte stringDebug-stringLookup 85 | .byte stringClear-stringLookup 86 | .byte stringConfirm-stringLookup 87 | .byte stringV4-stringLookup 88 | .byte stringV5-stringLookup ; F 89 | .byte stringLevel-stringLookup 90 | .byte stringLines-stringLookup 91 | .byte stringKSX2-stringLookup 92 | .byte stringFromBelow-stringLookup 93 | .byte stringInviz-stringLookup 94 | .byte stringHalt-stringLookup 95 | .byte stringShown-stringLookup ;16 96 | .byte stringTopout-stringLookup 97 | .byte stringCrash-stringLookup 98 | .byte stringConfetti-stringLookup ;19 99 | .byte stringStrict-stringLookup 100 | .byte stringNeon-stringLookup 101 | .byte stringLite-stringLookup 102 | .byte stringTeal-stringLookup 103 | .byte stringOG-stringLookup 104 | stringClassic: 105 | .byte $7,'C','L','A','S','S','I','C' 106 | stringLetters: 107 | .byte $7,'L','E','T','T','E','R','S' 108 | stringSevenDigit: 109 | .byte $6,'7','D','I','G','I','T' 110 | stringFloat: 111 | .byte $1,'M' 112 | stringScorecap: 113 | .byte $6,'C','A','P','P','E','D' 114 | stringHidden: 115 | .byte $6,'H','I','D','D','E','N' 116 | stringOff: 117 | .byte $3,'O','F','F' 118 | stringOn: 119 | .byte $2,'O','N' 120 | stringPause: 121 | .byte $5,'P','A','U','S','E' 122 | stringDebug: 123 | .byte $5,'B','L','O','C','K' 124 | stringClear: 125 | .if SAVE_HIGHSCORES 126 | .byte $6,'C','L','E','A','R','?' 127 | .endif 128 | stringConfirm: 129 | .if SAVE_HIGHSCORES 130 | .byte $6,'S','U','R','E','?','!' 131 | .endif 132 | stringV4: 133 | .byte $2,'V','4' 134 | stringV5: 135 | .byte $2,'V','5' 136 | stringLines: 137 | .byte $5,'L','I','N','E','S' 138 | stringLevel: 139 | .byte $5,'L','E','V','E','L' 140 | stringKSX2: 141 | .byte $4,'K','S',$69,'2' 142 | stringFromBelow: 143 | .byte $5,'F','L','O','O','R' 144 | stringInviz: 145 | .byte $5,'I','N','V','I','Z' 146 | stringHalt: 147 | .byte $4,'H','A','L','T' 148 | stringNull: 149 | .byte $0 150 | stringShown: 151 | .byte $4,'S','H','O','W' 152 | stringTopout: 153 | .byte $6,'T','O','P','O','U','T' 154 | stringCrash: 155 | .byte $5,'C','R','A','S','H' 156 | stringConfetti: 157 | .byte $8,'C','O','N','F','E','T','T','I' 158 | stringStrict: 159 | .byte $6,'S','T','R','I','C','T' 160 | stringNeon: 161 | .byte $4,'N','E','O','N' 162 | stringTeal: 163 | .byte $4,'T','E','A','L' 164 | stringLite: 165 | .byte $4,'L','I','T','E' 166 | stringOG: 167 | .byte $2,'O','G' 168 | -------------------------------------------------------------------------------- /tests/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gym-tests" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | gumdrop = "0.8.1" 8 | rusticnes-core = { git = "https://github.com/zeta0134/rusticnes-core.git", version = "0.2.0" } 9 | minifb = "0.27" 10 | 11 | # for patch testing 12 | flips = "0.2.1" 13 | flips-sys = "0.2.1" 14 | md5 = "0.7.0" 15 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | testing / tooling framework for TetrisGYM 2 | 3 | to see commands; 4 | 5 | ```bash 6 | cargo run --release -- --help 7 | ``` 8 | 9 | to verify various behaviour in the game, first build the ROM then run; 10 | 11 | ```bash 12 | cargo run --release -- --test 13 | ``` 14 | 15 | uses [rusticnes-core](https://github.com/zeta0134/rusticnes-core) 16 | -------------------------------------------------------------------------------- /tests/src/block.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug, PartialEq)] 2 | pub enum Block { 3 | T, J, Z, O, S, L, I, Unknown(u8) 4 | } 5 | 6 | impl From for Block { 7 | fn from(value: u8) -> Self { 8 | match value { 9 | 0x2 => Block::T, 10 | 0x7 => Block::J, 11 | 0x8 => Block::Z, 12 | 0xA => Block::O, 13 | 0xB => Block::S, 14 | 0xE => Block::L, 15 | 0x12 => Block::I, 16 | _ => Block::Unknown(value), 17 | } 18 | } 19 | } 20 | 21 | impl From for Block { 22 | fn from(value: char) -> Self { 23 | match value { 24 | 'T' => Block::T, 25 | 'J' => Block::J, 26 | 'Z' => Block::Z, 27 | 'O' => Block::O, 28 | 'S' => Block::S, 29 | 'L' => Block::L, 30 | 'I' => Block::I, 31 | _ => Block::Unknown(value as u8), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/src/constants.rs: -------------------------------------------------------------------------------- 1 | use crate::{labels, util}; 2 | 3 | pub fn test() { 4 | // check some hardcoded ram addresses are aligned 5 | assert_eq!(labels::get("stack"), 0x100); 6 | assert_eq!(labels::get("playfield"), 0x400); 7 | assert_eq!(labels::get("highscores"), 0x700); 8 | assert_eq!(labels::get("menuRAM"), 0x760); 9 | 10 | // check the right amount of menu ram exists 11 | let qty = labels::get("MODE_QUANTITY") as usize; 12 | let cfg = labels::get("menuConfigSizeLookup") as usize; 13 | 14 | let mut menu_options = 0; 15 | 16 | for i in 0..qty { 17 | if util::rom_data()[cfg + i - 0x8000] != 0 { 18 | menu_options += 1; 19 | } 20 | } 21 | 22 | assert_eq!(menu_options, labels::get("palFlag") + 1 - labels::get("menuVars")); 23 | 24 | // check the menu scroll is correct 25 | let y_scroll = labels::get("MENU_MAX_Y_SCROLL"); 26 | 27 | assert_eq!(menu_options - 8, y_scroll / 8); 28 | } 29 | -------------------------------------------------------------------------------- /tests/src/crunch.rs: -------------------------------------------------------------------------------- 1 | use rusticnes_core::nes::NesState; 2 | 3 | use crate::{util, labels, playfield}; 4 | 5 | const CRUNCH_F: &str = r##"### ### 6 | ### ### 7 | ### ### 8 | ### ### 9 | ### ### 10 | ### ### 11 | ### ### 12 | ### ### 13 | ### ### 14 | ### ### 15 | ### ### 16 | ### ### 17 | ### ### 18 | ### ### 19 | ### ### 20 | ### ### 21 | ### ### 22 | ### ### 23 | ### ### 24 | ### ###"##; 25 | 26 | const CRUNCH_D: &str = r##"### # 27 | ### # 28 | ### # 29 | ### # 30 | ### # 31 | ### # 32 | ### # 33 | ### # 34 | ### # 35 | ### # 36 | ### # 37 | ### # 38 | ### # 39 | ### # 40 | ### # 41 | ### # 42 | ### # 43 | ### # 44 | ### # 45 | ### #"##; 46 | 47 | const CRUNCH_7: &str = r##"# ### 48 | # ### 49 | # ### 50 | # ### 51 | # ### 52 | # ### 53 | # ### 54 | # ### 55 | # ### 56 | # ### 57 | # ### 58 | # ### 59 | # ### 60 | # ### 61 | # ### 62 | # ### 63 | # ### 64 | # ### 65 | # ### 66 | # ###"##; 67 | 68 | const CRUNCH_5: &str = r##"# # 69 | # # 70 | # # 71 | # # 72 | # # 73 | # # 74 | # # 75 | # # 76 | # # 77 | # # 78 | # # 79 | # # 80 | # # 81 | # # 82 | # # 83 | # # 84 | # # 85 | # # 86 | # # 87 | # #"##; 88 | 89 | const CRUNCH_4: &str = r##"# 90 | # 91 | # 92 | # 93 | # 94 | # 95 | # 96 | # 97 | # 98 | # 99 | # 100 | # 101 | # 102 | # 103 | # 104 | # 105 | # 106 | # 107 | # 108 | #"##; 109 | 110 | const CRUNCH_1: &str = r##" # 111 | # 112 | # 113 | # 114 | # 115 | # 116 | # 117 | # 118 | # 119 | # 120 | # 121 | # 122 | # 123 | # 124 | # 125 | # 126 | # 127 | # 128 | # 129 | #"##; 130 | 131 | const CRUNCH_0: &str = r##" 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | "##; 151 | 152 | 153 | pub fn test() { 154 | let mut emu = util::emulator(None); 155 | test_crunch(&mut emu, CRUNCH_0, 0x0); 156 | test_crunch(&mut emu, CRUNCH_1, 0x1); 157 | test_crunch(&mut emu, CRUNCH_4, 0x4); 158 | test_crunch(&mut emu, CRUNCH_5, 0x5); 159 | test_crunch(&mut emu, CRUNCH_7, 0x7); 160 | test_crunch(&mut emu, CRUNCH_D, 0xD); 161 | test_crunch(&mut emu, CRUNCH_F, 0xF); 162 | } 163 | 164 | 165 | fn test_crunch(emu: &mut NesState, expected_playfield: &str, crunch_setting: u8) { 166 | emu.reset(); 167 | 168 | for _ in 0..3 { emu.run_until_vblank(); } 169 | 170 | let game_mode = labels::get("gameMode") as usize; 171 | let main_loop = labels::get("mainLoop"); 172 | let level_number = labels::get("levelNumber") as usize; 173 | let practise_type = labels::get("practiseType") as usize; 174 | let mode_crunch = labels::get("MODE_CRUNCH") as u8; 175 | let crunch_modifier = labels::get("crunchModifier") as usize; 176 | let allegro = labels::get("allegro") as usize; 177 | let lines = labels::get("lines") as usize; 178 | 179 | emu.memory.iram_raw[practise_type] = mode_crunch; 180 | emu.memory.iram_raw[level_number] = 0; // intentionally slow 181 | emu.memory.iram_raw[game_mode] = 4; 182 | emu.memory.iram_raw[crunch_modifier] = crunch_setting; 183 | emu.memory.iram_raw[lines] = 0; 184 | emu.registers.pc = main_loop; 185 | playfield::clear(emu); 186 | for _ in 0..9 { emu.run_until_vblank(); } 187 | 188 | 189 | // validate initialized 190 | assert_eq!(expected_playfield, playfield::get_str(emu)); 191 | 192 | emu.memory.iram_raw[labels::get("currentPiece") as usize] = 0x12; 193 | emu.memory.iram_raw[labels::get("tetriminoX") as usize] = 0x5; 194 | emu.memory.iram_raw[labels::get("tetriminoY") as usize] = 0x12; 195 | emu.memory.iram_raw[labels::get("autorepeatY") as usize] = 0; 196 | emu.memory.iram_raw[labels::get("vramRow") as usize] = 0; 197 | 198 | // skip lock tetrimino and setup a tetris to be cleared 199 | emu.memory.iram_raw[labels::get("playState") as usize] = 3; 200 | for block in 0x4a0..0x4c8 { 201 | emu.memory.iram_raw[block as usize] = 0x7b; 202 | }; 203 | 204 | // cycle through remainder of entry delay and animation 205 | for _ in 0..32 { 206 | emu.run_until_vblank(); 207 | } 208 | 209 | // validate tetris was scored and playfield looks the same 210 | assert_eq!(emu.memory.iram_raw[lines], 4); 211 | assert_eq!(expected_playfield, playfield::get_str(emu)); 212 | 213 | // validate allegro not set 214 | assert_eq!(emu.memory.iram_raw[allegro], 0); 215 | } 216 | -------------------------------------------------------------------------------- /tests/src/cycle_count.rs: -------------------------------------------------------------------------------- 1 | use crate::{util, score, labels, playfield}; 2 | 3 | pub fn count_cycles() { 4 | count_hz_cycles(); 5 | count_max_score_cycles(); 6 | count_mode_score_cycles(); 7 | } 8 | 9 | fn count_hz_cycles() { 10 | // check max hz calculation amount 11 | let mut emu = util::emulator(None); 12 | 13 | let hz_flag = labels::get("hzFlag") as usize; 14 | let game_mode = labels::get("gameMode") as usize; 15 | let x = labels::get("tetriminoX") as usize; 16 | let y = labels::get("tetriminoY") as usize; 17 | let main_loop = labels::get("mainLoop"); 18 | let level_number = labels::get("levelNumber") as usize; 19 | let debounce = labels::get("hzDebounceThreshold") as usize; 20 | 21 | util::run_n_vblanks(&mut emu, 3); 22 | 23 | emu.memory.iram_raw[hz_flag] = 1; 24 | emu.memory.iram_raw[level_number] = 18; 25 | emu.memory.iram_raw[game_mode] = 4; 26 | emu.registers.pc = main_loop; 27 | 28 | util::run_n_vblanks(&mut emu, 5); 29 | 30 | let mut highest = 0; 31 | 32 | for buttons in &[ 33 | "L.L.L.L.L", 34 | "RR...R...R...R.R.", 35 | ".....L.L..L.L.", 36 | "LLL..L.L...L.R.R..L...R....L....L...L....L", 37 | "R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R.R", 38 | ] { 39 | buttons.chars().for_each(|button| { 40 | emu.memory.iram_raw[x] = 5; 41 | emu.memory.iram_raw[y] = 0; 42 | util::set_controller(&mut emu, button); 43 | highest = highest.max(util::cycles_to_vblank(&mut emu)); 44 | }); 45 | 46 | util::run_n_vblanks(&mut emu, debounce + 1); 47 | } 48 | 49 | println!("hz display most cycles to vblank: {}", highest); 50 | } 51 | 52 | fn count_max_score_cycles() { 53 | // check max scoring cycle amount 54 | let mut emu = util::emulator(None); 55 | 56 | let completed_lines = labels::get("completedLines") as usize; 57 | let add_points = labels::get("addPointsRaw"); 58 | let level_number = labels::get("levelNumber") as usize; 59 | 60 | let mut score = move |score: u32, lines: u8, level: u8| { 61 | score::set(&mut emu, score); 62 | emu.registers.pc = add_points; 63 | emu.memory.iram_raw[completed_lines] = lines; 64 | emu.memory.iram_raw[level_number] = level; 65 | util::cycles_to_return(&mut emu) 66 | }; 67 | 68 | let mut highest = 0; 69 | 70 | for level in 0..=255 { 71 | for lines in 0..=4 { 72 | let count = score(999999, lines, level); 73 | 74 | if count > highest { 75 | highest = count; 76 | } 77 | } 78 | } 79 | 80 | println!("scoring routine most cycles: {}", highest); 81 | } 82 | 83 | fn count_mode_score_cycles() { 84 | // check clock cycles frames in each mode 85 | let mut emu = util::emulator(None); 86 | 87 | for mode in 0..labels::get("MODE_GAME_QUANTITY") { 88 | 89 | emu.reset(); 90 | 91 | for _ in 0..3 { emu.run_until_vblank(); } 92 | 93 | let practise_type = labels::get("practiseType") as usize; 94 | let game_mode = labels::get("gameMode") as usize; 95 | let main_loop = labels::get("mainLoop"); 96 | let level_number = labels::get("levelNumber") as usize; 97 | 98 | 99 | emu.memory.iram_raw[practise_type] = mode as _; 100 | emu.memory.iram_raw[level_number] = 235; 101 | emu.memory.iram_raw[game_mode] = 4; 102 | emu.registers.pc = main_loop; 103 | 104 | for _ in 0..5 { emu.run_until_vblank(); } 105 | 106 | let (mut highest, mut _level, mut lines) = (0, 0, 0); 107 | 108 | for line in 0..5 { 109 | emu.memory.iram_raw[labels::get("vramRow") as usize] = 0; 110 | 111 | playfield::clear(&mut emu); 112 | 113 | playfield::set_str(&mut emu, match line { 114 | 0 => "", 115 | 1 => "##### ####", 116 | 2 => "##### ####\n##### ####", 117 | 3 => "##### ####\n##### ####\n##### ####", 118 | 4 => "##### ####\n##### ####\n##### ####\n##### ####", 119 | _ => unreachable!("line"), 120 | }); 121 | 122 | emu.memory.iram_raw[labels::get("currentPiece") as usize] = 0x11; 123 | emu.memory.iram_raw[labels::get("tetriminoX") as usize] = 0x5; 124 | emu.memory.iram_raw[labels::get("tetriminoY") as usize] = 0x12; 125 | emu.memory.iram_raw[labels::get("autorepeatY") as usize] = 0; 126 | 127 | for _ in 0..45 { 128 | let cycles = util::cycles_to_vblank(&mut emu); 129 | 130 | if cycles > highest { 131 | highest = cycles; 132 | _level = emu.memory.iram_raw[level_number]; 133 | lines = line; 134 | } 135 | } 136 | } 137 | 138 | println!("cycles to vblank {} lines {} mode {}", highest, lines, mode); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /tests/src/drought.rs: -------------------------------------------------------------------------------- 1 | use crate::{labels, util, block}; 2 | use std::time::{SystemTime, UNIX_EPOCH}; 3 | 4 | fn rand() -> u32 { 5 | (SystemTime::now() 6 | .duration_since(UNIX_EPOCH) 7 | .unwrap() 8 | .subsec_nanos() % 9) + 4 9 | } 10 | 11 | pub fn print_probabilities() { 12 | let mut emu = util::emulator(None); 13 | let rng_seed = labels::get("rng_seed"); 14 | let drought_modifier = labels::get("droughtModifier"); 15 | let pick_next = labels::get("pickRandomTetrimino"); 16 | let prng = labels::get("generateNextPseudorandomNumber"); 17 | 18 | emu.memory.iram_raw[labels::get("practiseType") as usize] = labels::get("MODE_DROUGHT") as u8; 19 | 20 | for modifier in 0..19 { 21 | emu.memory.iram_raw[drought_modifier as usize] = modifier; 22 | 23 | let seed = 0x8988; 24 | emu.memory.iram_raw[(rng_seed + 0) as usize] = (seed >> 8) as _; 25 | emu.memory.iram_raw[(rng_seed + 1) as usize] = seed as u8; 26 | 27 | let mut longbars = 0; 28 | let mut total = 0; 29 | 30 | for _ in 0..100000 { 31 | for _ in 3..rand() { 32 | emu.registers.x = rng_seed as u8; 33 | emu.registers.pc = prng; 34 | util::run_to_return(&mut emu, false); 35 | } 36 | 37 | emu.registers.pc = pick_next; 38 | 39 | util::run_to_return(&mut emu, false); 40 | 41 | let block: block::Block = emu.memory.iram_raw[labels::get("spawnID") as usize].into(); 42 | 43 | if block == block::Block::I { 44 | longbars += 1; 45 | } 46 | 47 | total += 1; 48 | } 49 | println!( 50 | "{} longbar%: {:.2}", 51 | "0123456789ABCDEFGHI".as_bytes()[modifier as usize] as char, 52 | (longbars as f64 / total as f64) * 100. 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/src/garbage.rs: -------------------------------------------------------------------------------- 1 | use crate::{util, labels, playfield}; 2 | use rusticnes_core::memory::read_byte; 3 | 4 | pub fn test_garbage4_crash() { 5 | let mut emu = util::emulator(None); 6 | 7 | let main_loop = labels::get("mainLoop"); 8 | let practise_type = labels::get("practiseType") as usize; 9 | let game_mode = labels::get("gameMode") as usize; 10 | let level_number = labels::get("levelNumber") as usize; 11 | let gmod = labels::get("garbageModifier") as usize; 12 | let mode = labels::get("MODE_GARBAGE") as u8; 13 | let nmi_label = labels::get("nmi") as u16; 14 | let wait_loop_start: u16 = labels::get("updateAudioWaitForNmiAndResetOamStaging") + 7 as u16; 15 | let wait_loop_end: u16 = labels::get("resetOAMStaging") as u16; 16 | 17 | // spend a few frames bootstrapping 18 | for _ in 0..3 { 19 | emu.run_until_vblank(); 20 | } 21 | 22 | emu.memory.iram_raw[practise_type] = mode; 23 | emu.memory.iram_raw[game_mode] = 4; 24 | emu.memory.iram_raw[level_number] = 18; 25 | emu.memory.iram_raw[gmod] = 4; 26 | emu.registers.pc = main_loop; 27 | 28 | for _ in 0..11 { 29 | emu.run_until_vblank(); 30 | } 31 | 32 | emu.memory.iram_raw[labels::get("currentPiece") as usize] = 0x11; 33 | emu.memory.iram_raw[labels::get("tetriminoX") as usize] = 0x5; 34 | emu.memory.iram_raw[labels::get("tetriminoY") as usize] = 0x0; 35 | emu.memory.iram_raw[labels::get("vramRow") as usize] = 0; 36 | emu.memory.iram_raw[labels::get("autorepeatY") as usize] = 0; 37 | emu.memory.iram_raw[labels::get("rng_seed") as usize] = 0xBE; 38 | emu.memory.iram_raw[(labels::get("rng_seed") as usize) + 1] = 0x83; 39 | 40 | playfield::set_str(&mut emu, r##" 41 | ##### ### 42 | ##### ### 43 | ###### ### 44 | ###### ### 45 | ########## 46 | ########## 47 | ########## 48 | ########## 49 | ########## 50 | ########## 51 | ########## 52 | ########## 53 | ########## 54 | ########## 55 | ########## 56 | ########## 57 | ########## 58 | ########## 59 | ########## 60 | ##########"##); 61 | 62 | for _ in 0..40 { 63 | loop { 64 | emu.cycle(); 65 | if emu.registers.pc == nmi_label { break }; 66 | } 67 | let stack_pointer = emu.registers.s; 68 | let return_lo = read_byte(&mut emu, 0x0100 + stack_pointer as u16 + 2) as u16; 69 | let return_hi = read_byte(&mut emu, 0x0100 + stack_pointer as u16 + 3) as u16 ; 70 | let return_addr = return_hi << 8 | return_lo; 71 | assert!(return_addr >= wait_loop_start && return_addr <= wait_loop_end); 72 | } 73 | 74 | assert_ne!(emu.cpu.opcode, 18); 75 | } 76 | -------------------------------------------------------------------------------- /tests/src/input.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] 2 | pub const DOWN: u8 = 0x4u8; 3 | pub const UP: u8 = 0x8u8; 4 | pub const RIGHT: u8 = 0x1u8; 5 | pub const LEFT: u8 = 0x2u8; 6 | pub const B: u8 = 0x40u8; 7 | pub const A: u8 = 0x80u8; 8 | pub const SELECT: u8 = 0x20u8; 9 | pub const START: u8 = 0x10u8; 10 | -------------------------------------------------------------------------------- /tests/src/labels.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::OnceLock; 3 | 4 | fn parse_debug_line(line: &str) -> (String, u16) { 5 | let pairs: Vec<&str> = line.split(',').collect(); 6 | 7 | let mut name = String::new(); 8 | let mut val = 0; 9 | 10 | for pair in pairs { 11 | let key_val: Vec<&str> = pair.split('=').collect(); 12 | if key_val.len() == 2 { 13 | let key = key_val[0].trim(); 14 | let value = key_val[1].trim(); 15 | 16 | match key { 17 | "name" => name = value.trim_matches('"').to_string(), 18 | "val" => { 19 | if let Ok(hex_value) = u16::from_str_radix(value.trim_start_matches("0x"), 16) { 20 | val = hex_value; 21 | } 22 | } 23 | _ => {} 24 | } 25 | } 26 | } 27 | 28 | (name, val) 29 | } 30 | 31 | fn labels() -> &'static HashMap { 32 | static LABELS: OnceLock> = OnceLock::new(); 33 | LABELS.get_or_init(|| { 34 | let text = include_str!("../../tetris.dbg"); 35 | let mut labels: HashMap = HashMap::new(); 36 | 37 | for line in text.lines() { 38 | let (name, val) = parse_debug_line(line); 39 | 40 | if !name.is_empty() { 41 | labels.insert(name, val); 42 | } 43 | } 44 | 45 | labels 46 | }) 47 | } 48 | 49 | fn addrs() -> &'static HashMap { 50 | static LABELS: OnceLock> = OnceLock::new(); 51 | LABELS.get_or_init(|| { 52 | let text = include_str!("../../tetris.dbg"); 53 | let mut labels: HashMap = HashMap::new(); 54 | 55 | for line in text.lines() { 56 | let (name, val) = parse_debug_line(line); 57 | 58 | if !name.is_empty() { 59 | labels.insert(val, name); 60 | } 61 | } 62 | 63 | labels 64 | }) 65 | } 66 | 67 | pub fn get(label: &str) -> u16 { 68 | *labels().get(label).expect(&format!("label {} not found", label)) 69 | } 70 | 71 | pub fn from_addr(addr: u16) -> Option<&'static String> { 72 | addrs().get(&addr) 73 | } 74 | -------------------------------------------------------------------------------- /tests/src/main.rs: -------------------------------------------------------------------------------- 1 | mod block; 2 | mod input; 3 | mod labels; 4 | mod playfield; 5 | mod util; 6 | mod video; 7 | 8 | mod cycle_count; 9 | mod crash; 10 | 11 | mod crunch; 12 | mod drought; 13 | mod floor; 14 | mod garbage; 15 | mod harddrop; 16 | mod mapper; 17 | mod palettes; 18 | mod pushdown; 19 | mod rng; 20 | mod score; 21 | mod sps; 22 | mod toprow; 23 | mod tspins; 24 | mod hz_display; 25 | mod nmi; 26 | mod constants; 27 | mod patch; 28 | 29 | use gumdrop::Options; 30 | 31 | fn parse_hex(s: &str) -> Result { 32 | u32::from_str_radix(s, 16) 33 | } 34 | 35 | #[derive(Debug, Options)] 36 | struct TestOptions { 37 | help: bool, 38 | #[options(help = "run tests")] 39 | test: bool, 40 | #[options(help = "run single tests")] 41 | test_single: Option, 42 | #[options(help = "count cycles")] 43 | cycles: bool, 44 | #[options(help = "fuzz crash")] 45 | crash: bool, 46 | #[options(help = "set SPS seed", parse(try_from_str = "parse_hex"))] 47 | sps_seed: u32, 48 | #[options(help = "print SPS pieces")] 49 | sps_qty: u32, 50 | #[options(help = "list RNG seeds")] 51 | rng_seeds: bool, 52 | #[options(help = "list drought mode probabilities")] 53 | drought_probs: bool, 54 | foo: bool, 55 | } 56 | 57 | fn main() { 58 | let options = TestOptions::parse_args_default_or_exit(); 59 | 60 | let tests: [(&str, fn()); 17] = [ 61 | ("garbage4", garbage::test_garbage4_crash), 62 | ("floor", floor::test), 63 | ("tspins", tspins::test), 64 | ("top row bug", toprow::test), 65 | ("score", score::test), 66 | ("score_render", score::test_render), 67 | ("mapper", mapper::test), 68 | ("pushdown", pushdown::test), 69 | ("rng seeds", rng::test), 70 | ("sps", sps::test), 71 | ("palettes", palettes::test), 72 | ("hz_display", hz_display::test), 73 | ("nmi", nmi::test), 74 | ("constants", constants::test), 75 | ("patch", patch::test), 76 | ("crunch", crunch::test), 77 | ("harddrop", harddrop::test), 78 | ]; 79 | 80 | // run tests 81 | if options.test { 82 | tests.iter().for_each(|(name, test)| { 83 | test(); 84 | println!(">> {name} ✅"); 85 | }); 86 | } 87 | 88 | // run single test 89 | if let Some(name) = options.test_single { 90 | let found = tests.iter().find(|&test| test.0 == name); 91 | if let Some(test) = found { 92 | test.1(); 93 | println!(">> {name} ✅"); 94 | } else { 95 | println!("no such test {name}"); 96 | } 97 | } 98 | 99 | // fuzz crash 100 | if options.crash { 101 | crash::fuzz(); 102 | } 103 | 104 | // count cycles 105 | if options.cycles { 106 | cycle_count::count_cycles(); 107 | } 108 | 109 | // print SPS sequences 110 | if options.sps_qty > 0 { 111 | let mut blocks = sps::SPS::new(); 112 | 113 | blocks.set_input(( 114 | ((options.sps_seed >> 16) & 0xFF) as u8, 115 | ((options.sps_seed >> 8) & 0xFF) as u8, 116 | (options.sps_seed & 0xFF) as u8, 117 | )); 118 | 119 | for _ in 0..options.sps_qty { 120 | print!("{:?}", blocks.next()); 121 | } 122 | println!(""); 123 | } 124 | 125 | if options.rng_seeds { 126 | println!("{:?}", rng::seeds()); 127 | } 128 | 129 | if options.drought_probs { 130 | drought::print_probabilities(); 131 | } 132 | 133 | // other stuff 134 | 135 | if options.foo { 136 | let mut emu = util::emulator(None); 137 | let mut view = video::Video::new(); 138 | 139 | let rng_seed = labels::get("rng_seed") as usize; 140 | let main_loop = labels::get("mainLoop"); 141 | let practise_type = labels::get("practiseType") as usize; 142 | let game_mode = labels::get("gameMode") as usize; 143 | let level_number = labels::get("levelNumber") as usize; 144 | let b_modifier = labels::get("typeBModifier") as usize; 145 | let mode_typeb = labels::get("MODE_TYPEB") as u8; 146 | 147 | rng::seeds().iter().for_each(|seed| { 148 | emu.reset(); 149 | 150 | // spend a few frames bootstrapping 151 | for _ in 0..3 { 152 | emu.run_until_vblank(); 153 | } 154 | 155 | emu.memory.iram_raw[practise_type] = mode_typeb; 156 | emu.memory.iram_raw[game_mode] = 4; 157 | emu.memory.iram_raw[level_number] = 18; 158 | emu.memory.iram_raw[b_modifier] = 5; 159 | 160 | emu.memory.iram_raw[rng_seed] = (seed >> 8) as u8; 161 | emu.memory.iram_raw[rng_seed + 1] = *seed as u8; 162 | 163 | rusticnes_core::opcodes::push(&mut emu, (main_loop >> 8) as u8); 164 | rusticnes_core::opcodes::push(&mut emu, main_loop as u8); 165 | 166 | for _ in 0..23 { 167 | emu.run_until_vblank(); 168 | } 169 | 170 | view.render(&mut emu); 171 | }); 172 | loop {} 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /tests/src/nmi.rs: -------------------------------------------------------------------------------- 1 | use crate::{labels, playfield, util}; 2 | 3 | /// makes sure nmi exits quickly enough 4 | pub fn test() { 5 | let mut emu = util::emulator(None); 6 | 7 | let main_loop = labels::get("mainLoop"); 8 | let game_mode = labels::get("gameMode") as usize; 9 | let level_number = labels::get("levelNumber") as usize; 10 | let nmi_label = labels::get("nmi"); 11 | let hz_flag = labels::get("hzFlag") as usize; 12 | let render_flags = labels::get("renderFlags") as usize; 13 | 14 | // spend a few frames bootstrapping 15 | for _ in 0..3 { 16 | emu.run_until_vblank(); 17 | } 18 | 19 | // copied setup from garbage.rs 20 | emu.memory.iram_raw[hz_flag] = 1; 21 | emu.memory.iram_raw[game_mode] = 4; 22 | emu.memory.iram_raw[level_number] = 18; 23 | emu.registers.pc = main_loop; 24 | 25 | for _ in 0..11 { 26 | emu.run_until_vblank(); 27 | } 28 | 29 | // sets up a triple in the top of the board 30 | emu.memory.iram_raw[labels::get("currentPiece") as usize] = 0x0F; // L piece 31 | emu.memory.iram_raw[labels::get("tetriminoX") as usize] = 0x6; 32 | emu.memory.iram_raw[labels::get("tetriminoY") as usize] = 0x0; 33 | emu.memory.iram_raw[labels::get("vramRow") as usize] = 0; 34 | emu.memory.iram_raw[labels::get("autorepeatY") as usize] = 0; 35 | 36 | playfield::set_str(&mut emu, r##" 37 | ##### ### 38 | ##### ### 39 | ###### ### 40 | ###### ### 41 | ######### 42 | ######### 43 | ######### 44 | ######### 45 | ######### 46 | ######### 47 | ######### 48 | ######### 49 | ######### 50 | ######### 51 | ######### 52 | ######### 53 | ######### 54 | ######### 55 | ######### 56 | ######### "##); 57 | 58 | for _ in 0..50 { 59 | // loop until pc is at the instruction after the jsr copyOamStagingToOam 60 | while emu.registers.pc != nmi_label + 25 { 61 | emu.step(); 62 | if emu.ppu.current_scanline == 261 { 63 | panic!("render took too long!"); 64 | } 65 | } 66 | // cannot render hz on the same frame score/lines are updated 67 | let state = emu.memory.iram_raw[labels::get("playState") as usize]; 68 | if state != 5 { 69 | emu.memory.iram_raw[render_flags] |= 0x10; 70 | } 71 | emu.run_until_vblank(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/src/patch.rs: -------------------------------------------------------------------------------- 1 | pub fn test() { 2 | let clean = crate::util::OG_ROM; 3 | let patch = include_bytes!("../../tetris.bps"); 4 | 5 | // check original file hasn't changed 6 | assert_eq!("ec58574d96bee8c8927884ae6e7a2508", format!("{:x}", md5::compute(clean))); 7 | 8 | // check patch produces the final ROM 9 | let output = flips::BpsPatch::new(patch).apply(clean) 10 | .expect("could not apply patch"); 11 | 12 | assert_eq!(output.as_bytes(), crate::util::ROM); 13 | 14 | // check NES2.0 files work 15 | let mut clean2 = Vec::new(); 16 | clean2.extend([0x4e,0x45,0x53,0x1a,0x02,0x02,0x10,0x08,0x50,0x00,0x00,0x07,0x00,0x00,0x00,0x00]); 17 | clean2.extend(&clean[0x10..]); 18 | 19 | assert_eq!("204c0f64d291737e23c0345b59cf1c05", format!("{:x}", md5::compute(clean2.clone()))); 20 | 21 | let result = { 22 | let slice_p = patch.as_ref(); 23 | let slice_s: &[u8] = clean2.as_ref(); 24 | let mut mem_m = flips_sys::mem::default(); 25 | let mut mem_o = flips_sys::mem::default(); 26 | 27 | let _result = unsafe { 28 | let mem_i = flips_sys::mem::new(slice_s.as_ptr() as *mut _, slice_s.len()); 29 | let mem_p = flips_sys::mem::new(slice_p.as_ptr() as *mut _, slice_p.len()); 30 | flips_sys::bps::bps_apply(mem_p, mem_i, &mut mem_o as *mut _, &mut mem_m as *mut _, true) 31 | }; 32 | 33 | mem_o.to_owned() 34 | }; 35 | 36 | let mut invalid_indices = Vec::new(); 37 | 38 | for (i, v) in result.as_ref().iter().enumerate() { 39 | if v != &crate::util::ROM[i] { 40 | invalid_indices.push(i); 41 | } 42 | } 43 | 44 | assert_eq!(invalid_indices, Vec::new()); 45 | } 46 | -------------------------------------------------------------------------------- /tests/src/playfield.rs: -------------------------------------------------------------------------------- 1 | use rusticnes_core::nes::NesState; 2 | use crate::labels; 3 | 4 | pub fn set(emu: &mut NesState, x: u16, y: u16, value: u8) { 5 | set_addr(emu, labels::get("playfield"), x, y, value); 6 | } 7 | 8 | pub fn set_addr(emu: &mut NesState, playfield_addr: u16, x: u16, y: u16, value: u8) { 9 | let index = ((y * 10) + x) + playfield_addr; 10 | emu.memory.iram_raw[index as usize] = value; 11 | } 12 | 13 | pub fn get(emu: &NesState, x: u16, y: u16) -> u8 { 14 | get_addr(emu, labels::get("playfield"), x, y) 15 | } 16 | 17 | pub fn get_addr(emu: &NesState, playfield_addr: u16, x: u16, y: u16) -> u8 { 18 | let index = ((y * 10) + x) + playfield_addr; 19 | emu.memory.iram_raw[index as usize] 20 | } 21 | 22 | pub fn clear(emu: &mut NesState) { 23 | for y in 0..20 { 24 | for x in 0..10 { 25 | self::set(emu, x, y, 0xEF); 26 | } 27 | } 28 | } 29 | 30 | pub fn set_str(emu: &mut NesState, playfield: &str) { 31 | set_str_inner_addr(emu, labels::get("playfield"), playfield, false); 32 | } 33 | 34 | pub fn set_str_addr(emu: &mut NesState, playfield_addr: u16, playfield: &str) { 35 | set_str_inner_addr(emu, playfield_addr, playfield, false); 36 | } 37 | 38 | #[allow(dead_code)] 39 | pub fn set_str_top(emu: &mut NesState, playfield: &str) { 40 | set_str_inner_addr(emu, labels::get("playfield"), playfield, true); 41 | } 42 | 43 | fn set_str_inner_addr(emu: &mut NesState, playfield_addr: u16, playfield: &str, top: bool) { 44 | let rows = playfield.trim_start_matches('\n').split("\n").collect::>(); 45 | let offset = if top { 46 | 0 47 | } else { 48 | 20 - rows.len() as u16 49 | }; 50 | rows.iter().enumerate().for_each(|(y, line)| { 51 | line.chars().enumerate().for_each(|(x, ch)| { 52 | self::set_addr(emu, playfield_addr, x as _, offset + y as u16, if ch == '#' { 0x7b } else { 0xef }); 53 | }); 54 | }); 55 | 56 | } 57 | 58 | #[allow(dead_code)] 59 | pub fn get_str(emu: &NesState) -> String { 60 | let mut s = String::new(); 61 | for y in 0..20 { 62 | let mut line = String::new(); 63 | for x in 0..10 { 64 | line.push_str(if self::get(emu, x, y) == 0xEF { " " } else { "#" }); 65 | } 66 | s.push_str(line.trim_end()); 67 | if y != 19 { 68 | s.push_str("\n"); 69 | } 70 | } 71 | 72 | s 73 | } 74 | -------------------------------------------------------------------------------- /tests/src/pushdown.rs: -------------------------------------------------------------------------------- 1 | use crate::{labels, util, score}; 2 | 3 | pub fn test() { 4 | let mut emu = util::emulator(None); 5 | 6 | for pushdown in 2..15 { 7 | [0..1000, 24500..25500, 60000..65536].into_iter().for_each(|range| { 8 | for score in range { 9 | score::set(&mut emu, score); 10 | 11 | emu.registers.pc = labels::get("addPushDownPoints"); 12 | emu.memory.iram_raw[labels::get("holdDownPoints") as usize] = pushdown; 13 | 14 | util::run_to_return(&mut emu, false); 15 | 16 | let reference = pushdown_impl(pushdown, score as u16) as u32; 17 | 18 | assert_eq!(reference, score::get(&mut emu) - score); 19 | } 20 | }); 21 | } 22 | } 23 | 24 | // reference implementation - tested against the original game 25 | fn pushdown_impl(pushdown: u8, score: u16) -> u16 { 26 | let ones = score % 10; 27 | let hundredths = score % 100; 28 | 29 | let mut added = ones + (pushdown as u16 - 1); 30 | 31 | if added & 0xF > 9 { 32 | added += 6; 33 | } 34 | 35 | let low = added & 0xF; 36 | let high = (added >> 4) * 10; 37 | 38 | // you can dedupe the `- ones` here and just subtract it from `high` instead 39 | if high + low + hundredths - ones <= 100 { 40 | high + low - ones 41 | } else { 42 | high - ones 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/src/rng.rs: -------------------------------------------------------------------------------- 1 | use crate::{util, labels}; 2 | 3 | pub fn test() { 4 | assert_eq!( 5 | seeds(), 6 | seeds_impl(), 7 | ); 8 | } 9 | 10 | pub fn seeds_impl() -> Vec { 11 | let mut seeds: Vec = Vec::new(); 12 | 13 | let mut seed = 0x8988; 14 | 15 | loop { 16 | seeds.push(seed); 17 | 18 | let new_bit = ((seed >> 9) ^ (seed >> 1)) & 1; 19 | seed = (new_bit << 15) | (seed >> 1); 20 | 21 | if seed == 0x8988 { 22 | break; 23 | } 24 | } 25 | 26 | seeds 27 | } 28 | 29 | pub fn seeds() -> Vec { 30 | let mut emu = util::emulator(None); 31 | let rng_seed = labels::get("rng_seed"); 32 | let next_rng = labels::get("generateNextPseudorandomNumber"); 33 | 34 | let mut seeds: Vec = Vec::new(); 35 | 36 | let mut seed = 0x8988; 37 | 38 | loop { 39 | seeds.push(seed); 40 | 41 | emu.memory.iram_raw[(rng_seed + 0) as usize] = (seed >> 8) as _; 42 | emu.memory.iram_raw[(rng_seed + 1) as usize] = seed as u8; 43 | 44 | emu.registers.x = rng_seed as u8; 45 | emu.registers.y = 0x2; 46 | emu.registers.pc = next_rng; 47 | 48 | util::run_to_return(&mut emu, false); 49 | 50 | seed = 51 | ((emu.memory.iram_raw[rng_seed as usize] as u16) << 8) 52 | + emu.memory.iram_raw[(rng_seed + 1) as usize] as u16; 53 | 54 | if seed == 0x8988 { 55 | break; 56 | } 57 | } 58 | 59 | seeds 60 | } 61 | -------------------------------------------------------------------------------- /tests/src/score.rs: -------------------------------------------------------------------------------- 1 | use rusticnes_core::nes::NesState; 2 | use crate::{labels, score, util}; 3 | 4 | pub fn set(emu: &mut NesState, score: u32) { 5 | let score_addr = labels::get("score"); 6 | let binscore_addr = labels::get("binScore"); 7 | let bcd_str = format!("{:08}", score); 8 | let bcd_a = i64::from_str_radix(&bcd_str[0..2], 16).unwrap(); 9 | let bcd_b = i64::from_str_radix(&bcd_str[2..4], 16).unwrap(); 10 | let bcd_c = i64::from_str_radix(&bcd_str[4..6], 16).unwrap(); 11 | let bcd_d = i64::from_str_radix(&bcd_str[6..8], 16).unwrap(); 12 | 13 | emu.memory.iram_raw[(score_addr + 3) as usize] = bcd_a as u8; 14 | emu.memory.iram_raw[(score_addr + 2) as usize] = bcd_b as u8; 15 | emu.memory.iram_raw[(score_addr + 1) as usize] = bcd_c as u8; 16 | emu.memory.iram_raw[score_addr as usize] = bcd_d as u8; 17 | emu.memory.iram_raw[binscore_addr as usize] = score as u8; 18 | emu.memory.iram_raw[(binscore_addr + 1) as usize] = (score >> 8) as u8; 19 | emu.memory.iram_raw[(binscore_addr + 2) as usize] = (score >> 16) as u8; 20 | emu.memory.iram_raw[(binscore_addr + 3) as usize] = (score >> 24) as u8; 21 | } 22 | 23 | pub fn get(emu: &mut NesState) -> u32 { 24 | let binscore_addr = labels::get("binScore"); 25 | emu.memory.iram_raw[binscore_addr as usize] as u32 26 | + ((emu.memory.iram_raw[(binscore_addr + 1) as usize] as u32) << 8) 27 | + ((emu.memory.iram_raw[(binscore_addr + 2) as usize] as u32) << 16) 28 | + ((emu.memory.iram_raw[(binscore_addr + 3) as usize] as u32) << 24) 29 | } 30 | 31 | pub fn test() { 32 | let mut emu = util::emulator(None); 33 | 34 | let completed_lines = labels::get("completedLines") as usize; 35 | let add_points = labels::get("addPointsRaw"); 36 | let level_number = labels::get("levelNumber") as usize; 37 | 38 | let mut score = move |score: u32, lines: u8, level: u8| { 39 | score::set(&mut emu, score); 40 | emu.registers.pc = add_points; 41 | emu.memory.iram_raw[completed_lines] = lines; 42 | emu.memory.iram_raw[level_number] = level; 43 | util::run_to_return(&mut emu, false); 44 | score::get(&mut emu) 45 | }; 46 | 47 | // check every linecount on every level 48 | for level in 0..=255 { 49 | for lines in 0..=4 { 50 | assert_eq!(score(0, lines, level), score_impl(lines, level)); 51 | } 52 | } 53 | 54 | // check tetris value from lots of scores 55 | for initial_score in 999499..=1000501 { 56 | assert_eq!(score(initial_score, 4, 18), initial_score + score_impl(4, 18)); 57 | } 58 | 59 | // keep adding scores 60 | let mut accumulator = 0; 61 | let mut emu = util::emulator(None); 62 | 63 | for _ in 0..2000 { 64 | accumulator += score_impl(4, 235); 65 | emu.registers.pc = add_points; 66 | emu.memory.iram_raw[completed_lines] = 4; 67 | emu.memory.iram_raw[level_number] = 235; 68 | util::run_to_return(&mut emu, false); 69 | assert_eq!(score::get(&mut emu), accumulator); 70 | } 71 | 72 | } 73 | 74 | pub fn score_impl(lines: u8, level: u8) -> u32 { 75 | [0, 40, 100, 300, 1200][lines as usize] * (level as u32 + 1) 76 | } 77 | 78 | pub fn test_render() { 79 | let mut emu = util::emulator(None); 80 | 81 | let rendered_score = |emu: &mut NesState| { 82 | let vram_offset = emu.ppu.current_vram_address - 6; 83 | 84 | (vram_offset..vram_offset + 6) 85 | .map(|i| emu.ppu.read_byte(&mut *emu.mapper, i)) 86 | .collect::>() 87 | }; 88 | 89 | // check classic score rendering works 90 | 91 | for i in 0..1000 { 92 | let score = i * 100000; 93 | 94 | score::set(&mut emu, score); 95 | emu.registers.pc = labels::get("renderClassicScore"); 96 | util::run_to_return(&mut emu, false); 97 | 98 | assert_eq!((i % 16) as u8, rendered_score(&mut emu)[0]); 99 | } 100 | 101 | // check letter score rendering works 102 | 103 | for i in 0..256 { // TODO: fix breaking at 25.5m 104 | let score = i * 100000; 105 | 106 | score::set(&mut emu, score); 107 | emu.registers.pc = labels::get("renderLettersScore"); 108 | util::run_to_return(&mut emu, false); 109 | 110 | assert_eq!((i % 36) as u8, rendered_score(&mut emu)[0]); 111 | } 112 | 113 | // check score cap works 114 | 115 | for score in [8952432, 999999, 1000000, 1000010, 10000000, 100000000] { 116 | score::set(&mut emu, score); 117 | emu.registers.pc = labels::get("renderScoreCap"); 118 | util::run_to_return(&mut emu, false); 119 | assert_eq!(vec![9, 9, 9, 9, 9, 9], rendered_score(&mut emu)); 120 | } 121 | 122 | for score in [100000, 512345, 999998] { 123 | score::set(&mut emu, score); 124 | emu.registers.pc = labels::get("renderScoreCap"); 125 | util::run_to_return(&mut emu, false); 126 | assert_ne!(vec![9, 9, 9, 9, 9, 9], rendered_score(&mut emu)); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/src/sps.rs: -------------------------------------------------------------------------------- 1 | use crate::{block, labels, util}; 2 | use rusticnes_core::nes::NesState; 3 | 4 | pub struct SPS { 5 | emu: NesState, 6 | } 7 | 8 | impl SPS { 9 | pub fn new() -> Self { 10 | Self { 11 | emu: util::emulator(None), 12 | } 13 | } 14 | 15 | pub fn set_input(&mut self, seed: (u8, u8, u8)) { 16 | self.emu.memory.iram_raw[labels::get("spawnID") as usize] = 0; 17 | self.emu.memory.iram_raw[(labels::get("set_seed_input") + 0) as usize] = seed.0; 18 | self.emu.memory.iram_raw[(labels::get("set_seed") + 0) as usize] = seed.0; 19 | self.emu.memory.iram_raw[(labels::get("set_seed_input") + 1) as usize] = seed.1; 20 | self.emu.memory.iram_raw[(labels::get("set_seed") + 1) as usize] = seed.1; 21 | self.emu.memory.iram_raw[(labels::get("set_seed_input") + 2) as usize] = seed.2; 22 | self.emu.memory.iram_raw[(labels::get("set_seed") + 2) as usize] = seed.2; 23 | } 24 | 25 | pub fn next(&mut self) -> block::Block { 26 | self.emu.registers.pc = labels::get("pickTetriminoSeed"); 27 | 28 | util::run_to_return(&mut self.emu, false); 29 | 30 | self.emu.memory.iram_raw[labels::get("spawnID") as usize].into() 31 | } 32 | } 33 | 34 | pub fn test() { 35 | let mut blocks = SPS::new(); 36 | 37 | blocks.set_input((0x10, 0x10, 0x10)); 38 | "ZJOTLTZJLZJSZISIJOLITJSILZJILITSISZOITIZSZJLLTIOZJZSZISIJZTIZJTSOJSJISJOOTSJTOTZSZTZSLTZTOTSIZJZIJIL".chars().for_each(|block| { 39 | assert_eq!(blocks.next(), block.into()); 40 | }); 41 | 42 | blocks.set_input((0x12, 0x34, 0x56)); 43 | "ZTZIJIJOZTSOSZJZOSLIOIJIJSTZSTTJISSTOIZJITJOZJITSOSZSJLTISJOITTLSLJTZTZOZSLJTJZSLTSOTLOJLSJSJTJILOJS".chars().for_each(|block| { 44 | assert_eq!(blocks.next(), block.into()); 45 | }); 46 | 47 | blocks.set_input((0x87, 0xAB, 0x12)); 48 | "OZIJSOTZSJTSTJZLOLJOJISOZOIOZJITILSSJZLOIJSTITLSOJILTSOOLZOOIJOZLTLSISIJIJTOLSIJILSLOLJLTOSOSLOIZSIS".chars().for_each(|block| { 49 | assert_eq!(blocks.next(), block.into()); 50 | }); 51 | 52 | blocks.set_input((0x13, 0x37, 0x02)); 53 | "OJSTZSIOLSIJTSZILJZJJLZLISISJTLZTSZTJOJOSJSZLITJOIOTITILTOSTJSZTSOOIOJSIITLJOZSIOJOTZTLJLIOJLITSSLSLIJIIOLOISLZJLIJJTIZIJOJISLTIJTOTZIOILSTTLTZIZJSOLOZOZOLOTZTZOTZOSIOTJJTSIZSOLTOLIZSOZOTZISLJTSZLOISO".chars().for_each(|block| { 54 | assert_eq!(blocks.next(), block.into()); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /tests/src/tspins.rs: -------------------------------------------------------------------------------- 1 | use crate::{util, labels, playfield}; 2 | 3 | pub fn test() { 4 | let mut emu = util::emulator(None); 5 | 6 | for _ in 0..4 { 7 | emu.run_until_vblank(); 8 | } 9 | 10 | let practise_type = labels::get("practiseType") as usize; 11 | let game_mode = labels::get("gameMode") as usize; 12 | let main_loop = labels::get("mainLoop"); 13 | let level_number = labels::get("levelNumber") as usize; 14 | 15 | 16 | emu.memory.iram_raw[practise_type] = labels::get("MODE_TSPINS") as _; 17 | emu.memory.iram_raw[level_number] = 18; 18 | emu.memory.iram_raw[game_mode] = 4; 19 | 20 | emu.registers.pc = main_loop; 21 | 22 | for _ in 0..10 { 23 | emu.run_until_vblank(); 24 | } 25 | 26 | let offset = |x, y| x + (y * 256); 27 | 28 | // check playfield 29 | assert_eq!(r##" 30 | ###### ## 31 | ##### ## 32 | ###### ### 33 | ########## 34 | "##.trim(), playfield::get_str(&emu).trim()); 35 | 36 | // check that correct tile is rendered 37 | assert_eq!(emu.ppu.read_byte(&mut *emu.mapper, 0x22CC), 0x7E); 38 | // check pixel is actually rendered 39 | assert_eq!(emu.ppu.screen[offset(96, 176) as usize], 0x30); 40 | } 41 | -------------------------------------------------------------------------------- /tests/src/video.rs: -------------------------------------------------------------------------------- 1 | use rusticnes_core::nes::NesState; 2 | use minifb::{Window, WindowOptions, Key, KeyRepeat}; 3 | 4 | pub const WIDTH: usize = 256; 5 | pub const HEIGHT: usize = 240; 6 | 7 | pub struct Video { 8 | pub window: Window, 9 | } 10 | 11 | impl Video { 12 | pub fn new() -> Self { 13 | let mut window = Window::new( 14 | "video", 15 | WIDTH, 16 | HEIGHT, 17 | WindowOptions::default(), 18 | ) 19 | .unwrap_or_else(|e| { panic!("{}", e); }); 20 | 21 | window.set_position(20, 30); 22 | 23 | Self { 24 | window 25 | } 26 | } 27 | 28 | pub fn render(&mut self, emu: &mut NesState) { 29 | emu.ppu.render_ntsc(WIDTH); 30 | self.window.update_with_buffer(&emu.ppu.filtered_screen, WIDTH, HEIGHT).unwrap(); 31 | } 32 | } 33 | 34 | /// debug helper for showing the current visual state. 35 | /// hotkeys: 'q' closes window, 's' steps by a frame, 'p' toggles autoplay 36 | #[allow(dead_code)] 37 | pub fn preview(emu: &mut NesState) { 38 | preview_base(emu, false); 39 | } 40 | 41 | /// debug viewer with keyboard input 42 | #[allow(dead_code)] 43 | pub fn preview_input(emu: &mut NesState) { 44 | preview_base(emu, true); 45 | } 46 | 47 | fn preview_base(emu: &mut NesState, has_input: bool) { 48 | let mut view = Video::new(); 49 | let mut running = false; 50 | view.window.set_key_repeat_rate(0.1); 51 | view.window.set_target_fps(60); 52 | 53 | loop { 54 | if has_input { 55 | use crate::input::*; 56 | let mut buttons = 0; 57 | 58 | if view.window.is_key_down(Key::Up) { buttons |= UP; } 59 | if view.window.is_key_down(Key::Down) { buttons |= DOWN; } 60 | if view.window.is_key_down(Key::Left) { buttons |= LEFT; } 61 | if view.window.is_key_down(Key::Right) { buttons |= RIGHT; } 62 | if view.window.is_key_down(Key::V) { buttons |= START; } 63 | if view.window.is_key_down(Key::C) { buttons |= SELECT; } 64 | if view.window.is_key_down(Key::X) { buttons |= A; } 65 | if view.window.is_key_down(Key::Z) { buttons |= B; } 66 | 67 | crate::util::set_controller_raw(emu, buttons); 68 | } 69 | 70 | if !view.window.is_open() { 71 | break; 72 | } 73 | if view.window.is_key_pressed(Key::Q, KeyRepeat::No) { 74 | break; 75 | } 76 | if view.window.is_key_pressed(Key::P, KeyRepeat::No) { 77 | running = !running; 78 | } 79 | if running || view.window.is_key_pressed(Key::S, KeyRepeat::Yes) { 80 | emu.run_until_vblank(); 81 | } 82 | 83 | view.render(emu); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tools/assemble/README.md: -------------------------------------------------------------------------------- 1 | cc65 binaries built as WebAssembly for TetrisGYM 2 | 3 | generated with emscripten 4 | 5 | ```bash 6 | emmake make ld65 CC=emcc CFLAGS="-O3 -Wall" -Isrc/common/ LD=emcc OBJDIR="" HOST_OBJEXTENSION=".o" LDFLAGS="-sEXPORTED_RUNTIME_METHODS=FS -s FORCE_FILESYSTEM=1 -lnodefs.js -lnoderawfs.js" 7 | ``` 8 | 9 | original source: https://github.com/cc65/cc65 10 | -------------------------------------------------------------------------------- /tools/assemble/ca65.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/tools/assemble/ca65.wasm -------------------------------------------------------------------------------- /tools/assemble/ld65.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kirjavascript/TetrisGYM/d4ffde98d5629ef4790ffa47699f7cb439631e8d/tools/assemble/ld65.wasm -------------------------------------------------------------------------------- /tools/cycles.lua: -------------------------------------------------------------------------------- 1 | waitForVBlankTrigger = false 2 | lastCycles = nil 3 | highestShort = 0 4 | highCountShort = 0 5 | highestLong = 0 6 | highCountLong = 0 7 | 8 | function startFrame(address, value) 9 | state = emu.getState() 10 | waitForVBlankTrigger = false 11 | lastCycles = state["cpu.cycleCount"] 12 | highCountShort = highCountShort + 1 13 | highCountLong = highCountLong + 1 14 | if highCountShort > 30 then 15 | highCountShort = 0 16 | highestShort = 0 17 | end 18 | if highCountLong > 60*2 then 19 | highCountLong = 0 20 | highestLong = 0 21 | end 22 | end 23 | 24 | function cyclePCT(cycles) 25 | return string.format(" (%d%%)", math.floor((cycles / 29780.5)*100)) 26 | end 27 | 28 | function vblankCheck(address, value) 29 | if waitForVBlankTrigger == false then 30 | waitForVBlankTrigger = true 31 | state = emu.getState() 32 | diff = state["cpu.cycleCount"] - lastCycles 33 | if diff > highestShort then 34 | highestShort = diff 35 | end 36 | if diff > highestLong then 37 | highestLong = diff 38 | end 39 | emu.drawRectangle(8, 8, 150, 36, 0x000000, true, 1) 40 | emu.drawString(12, 9, "used cycles - " .. diff .. cyclePCT(diff), 0xFFFFFF, 0xFF000000, 0, 1) 41 | emu.drawString(12, 21, "highest/sec/2 - " .. highestShort .. cyclePCT(highestShort), 0xFFFFFF, 0xFF000000, 0, 1) 42 | emu.drawString(12, 33, "highest/2sec - " .. highestLong .. cyclePCT(highestLong), 0xFFFFFF, 0xFF000000, 0, 1) 43 | end 44 | end 45 | emu.addMemoryCallback(vblankCheck, emu.callbackType.read, 0x33) 46 | emu.addEventCallback(startFrame, emu.eventType.startFrame) 47 | -------------------------------------------------------------------------------- /tools/format.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | 4 | function format(line) { 5 | line = line.trimEnd().replace(/^\t+/, (_) => ' '.repeat(4 * _.length)); 6 | 7 | if (!line.trim().startsWith(';')) { 8 | line = line.replace(/^(\s+\w+)(\s+)([^;\s]+)/, '$1 $3'); 9 | } 10 | 11 | return line; 12 | 13 | } 14 | 15 | (function processFiles(directory) { 16 | const files = fs.readdirSync(directory); 17 | 18 | files.forEach(file => { 19 | const filePath = path.join(directory, file); 20 | const stat = fs.statSync(filePath); 21 | 22 | if (stat.isDirectory()) { 23 | processFiles(filePath); 24 | } else if (file.endsWith('.asm')) { 25 | const content = fs.readFileSync(filePath, 'utf8'); 26 | let formatted = content.split('\n').map(format).join('\n'); 27 | if (formatted.at(-1) !== '\n') { 28 | formatted += '\n'; 29 | } 30 | fs.writeFileSync(filePath, formatted); 31 | } 32 | }); 33 | })(process.cwd()); 34 | -------------------------------------------------------------------------------- /tools/modes.awk: -------------------------------------------------------------------------------- 1 | #! /usr/bin/awk -f 2 | 3 | # Run against tetris.dbg to see modes and values 4 | 5 | BEGIN { FS="," } 6 | 7 | /MODE_/ && /scope=0/ { 8 | gsub("name=", "", $2 ); 9 | gsub("\"", "", $2 ); 10 | for (i=3;i<=NF;i++) { 11 | if ($i ~ /^val=/) { 12 | gsub("val=", "", $i ); 13 | print sprintf("%02d",strtonum($i)), $2 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /tools/nametablepatch.js: -------------------------------------------------------------------------------- 1 | const lookup = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-.\'>!^()############qweadzxc############################################################################################################################################################################################### '; 2 | 3 | const convert = (patch, pos) => { 4 | const lines = patch.trim().split('\n'); 5 | return lines.map((line, i) => { 6 | const tiles = [...line].map(ch => lookup.indexOf(ch)); 7 | const addr = pos + (i * 0x20); 8 | const ending = lines.length - 1 != i ? 0xFE : 0xFD; 9 | return [addr >> 8, addr & 0xFF, ...tiles, ending]; 10 | }); 11 | }; 12 | 13 | const print = bytes => bytes.map(line => ' .byte ' + line.map(d => '$' + d.toString(16).padStart(2,'0').toUpperCase()).join`,`).join`\n`; 14 | 15 | // nametable patches 16 | 17 | console.log(print(convert(` 18 | qwwwwwwe 19 | aSLOT d 20 | a d 21 | zxxxxxxc 22 | `, 0x22F7))); 23 | 24 | console.log(print(convert(` 25 | a d 26 | `, 0x20A2))); 27 | -------------------------------------------------------------------------------- /tools/pace.rb: -------------------------------------------------------------------------------- 1 | Targets = %q( 2 | .byte $0,$0,$0,$0 3 | .byte $68,$1,$4B,$0 ; 1 4 | .byte $F8,$2,$6E,$0 ; 2 5 | .byte $7E,$4,$9A,$0 ; 3 6 | .byte $E6,$5,$E5,$0 ; 4 7 | .byte $6C,$7,$12,$1 ; 5 8 | .byte $CA,$8,$67,$1 ; 6 9 | .byte $5A,$A,$89,$1 ; 7 10 | .byte $B8,$B,$DE,$1 ; 8 11 | .byte $3E,$D,$B,$2 ; 9 12 | .byte $F2,$E,$A,$2 ; A 13 | .byte $2C,$10,$83,$2 ; B 14 | .byte $94,$11,$CD,$2 ; C 15 | .byte $38,$13,$DC,$2 ; D 16 | .byte $B4,$14,$13,$3 ; E 17 | .byte $08,$16,$72,$3 18 | ) 19 | .scan(/\$(.+),\$(.+),\$(.+),\$([0-9A-F]+)( |$)/i) 20 | .map.with_index { |a, i| 21 | [ 22 | i.to_s(16).upcase, 23 | (a[1].to_i(16) << 8) + a[0].to_i(16), 24 | (a[3].to_i(16) << 8) + a[2].to_i(16) 25 | ] 26 | } 27 | 28 | def print_pace(threshold, target, range, step = 1) 29 | _, base, mult = Targets[target] 30 | 31 | for i in range 32 | index = i * step 33 | if index <= threshold 34 | points = base 35 | else 36 | points = base + (((index-threshold) / (230.0-threshold)) * mult) 37 | end 38 | 39 | print " 40 | T: #{target.to_s(16).upcase}\ 41 | L: #{index}\ 42 | M: #{points.floor}\ 43 | P: #{(points * index).floor}\ 44 | " 45 | 46 | # print "| #{target.to_s(16).upcase} | #{(points * index).floor} |\n" 47 | end 48 | end 49 | 50 | # pp targets 51 | 52 | for target in 0..0xF 53 | # print_pace 110, target, 130..130, 1 54 | end 55 | 56 | print_pace 110, 0xA, 1..23, 10 57 | -------------------------------------------------------------------------------- /tools/patch/create.js: -------------------------------------------------------------------------------- 1 | const { 2 | MarcFile, 3 | createBPSFromFiles, 4 | parseBPSFile, 5 | crc32, 6 | } = require('./bps.js'); 7 | const fs = require('fs'); 8 | 9 | // https://github.com/blakesmith/rombp/blob/master/docs/bps_spec.md 10 | 11 | module.exports = function patch(original, modified, destination) { 12 | const originalFile = new MarcFile(original); 13 | const modifiedFile = new MarcFile(modified); 14 | const patch = createBPSFromFiles(originalFile, modifiedFile, true); 15 | 16 | // post-process BPS so that header bytes are ignored 17 | 18 | let outputOffset = 0; 19 | 20 | patch.actions.forEach((action) => { 21 | if (action.type === 0) { 22 | let { length } = action; 23 | 24 | let usesHeader = false; 25 | for (let i = 0; length--; i++) { 26 | if (outputOffset + i < 0x10) { 27 | usesHeader = true; 28 | break; 29 | } 30 | } 31 | 32 | if (usesHeader) { 33 | action.type = 1; 34 | action.bytes = Array.from( 35 | modifiedFile._u8array.slice( 36 | outputOffset, 37 | outputOffset + action.length, 38 | ), 39 | ); 40 | } 41 | 42 | outputOffset += action.length; 43 | } else { 44 | outputOffset += action.length; 45 | } 46 | }); 47 | 48 | // reapply the checksum 49 | patch.patchChecksum = crc32(patch.export(), 0, true); 50 | 51 | fs.writeFileSync(destination, patch.export()._u8array); 52 | 53 | const bps = parseBPSFile(new MarcFile('tetris.bps')); 54 | 55 | // count patch sizes 56 | let source = 0; 57 | bps.actions.forEach((action) => { 58 | if ([0, 2].includes(action.type)) { 59 | source += action.length; 60 | } 61 | }); 62 | 63 | return ((source / bps.sourceSize) * 100) | 0; 64 | }; 65 | -------------------------------------------------------------------------------- /tools/png2chr/convert.js: -------------------------------------------------------------------------------- 1 | const { PNG } = require('./png.js'); 2 | 3 | module.exports = function png2chr(file) { 4 | const png = PNG.sync.read(file); 5 | 6 | const palettes = png.palette.map((arr) => String(arr)); 7 | 8 | // get an array of pixel indices 9 | 10 | const pixels = []; 11 | for (let cursor = 0; cursor < png.data.length; cursor += 4) { 12 | const chunk = png.data.slice(cursor, cursor + 4); 13 | const paletteIndex = palettes.indexOf(String([...chunk])); 14 | if (paletteIndex === -1) { 15 | console.error('Palette index not found'); 16 | } 17 | pixels.push(paletteIndex); 18 | } 19 | 20 | // rearrange into groups of tiles 21 | 22 | const tilePixels = []; 23 | 24 | function offset(i) { 25 | return (i % 8) + ((i / 8) | 0) * png.width; 26 | } 27 | 28 | for (let tile = 0; tile < 256; tile++) { 29 | const index = tile * 8 + ((tile / 16) | 0) * png.width * 7; 30 | 31 | for (let i = 0; i < 64; i++) { 32 | tilePixels.push(pixels[index + offset(i)]); 33 | } 34 | } 35 | 36 | // convert tiles into bytes 37 | 38 | const bytes = []; 39 | 40 | for (let cursor = 0; cursor < pixels.length; cursor += 64) { 41 | const indices = tilePixels.slice(cursor, cursor + 64); 42 | const indicesBin = indices.map((idx) => idx.toString(2).padStart(2, 0)); 43 | 44 | for (let i = 0; i < 64; i += 8) { 45 | bytes.push( 46 | parseInt( 47 | indicesBin 48 | .slice(i, i + 8) 49 | .map((idx) => idx[1]) 50 | .join(''), 51 | 2, 52 | ), 53 | ); 54 | } 55 | 56 | for (let i = 0; i < 64; i += 8) { 57 | bytes.push( 58 | parseInt( 59 | indicesBin 60 | .slice(i, i + 8) 61 | .map((idx) => idx[0]) 62 | .join(''), 63 | 2, 64 | ), 65 | ); 66 | } 67 | } 68 | 69 | return Uint8Array.from(bytes); 70 | } 71 | --------------------------------------------------------------------------------