├── .gitignore ├── res ├── sprite.png └── background.png ├── screenshot ├── table_tennis_gameplay_00.png └── table_tennis_start_screen.png ├── .travis.yml ├── table_tennis.cfg ├── LICENSE ├── README.md ├── tools ├── table_tennis_fceux_symbols.py └── chr_tool.py └── table_tennis.s /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | -------------------------------------------------------------------------------- /res/sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike42/8bit-table-tennis/master/res/sprite.png -------------------------------------------------------------------------------- /res/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike42/8bit-table-tennis/master/res/background.png -------------------------------------------------------------------------------- /screenshot/table_tennis_gameplay_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike42/8bit-table-tennis/master/screenshot/table_tennis_gameplay_00.png -------------------------------------------------------------------------------- /screenshot/table_tennis_start_screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mike42/8bit-table-tennis/master/screenshot/table_tennis_start_screen.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dist: bionic 3 | sudo: required 4 | 5 | before_install: 6 | - sudo apt-get -qq update 7 | - sudo apt-get install -y cc65 python3-pil 8 | 9 | script: 10 | - ./build.sh 11 | ... 12 | -------------------------------------------------------------------------------- /table_tennis.cfg: -------------------------------------------------------------------------------- 1 | MEMORY { 2 | ZP: start = $00, size = $0100, type = rw, file = ""; 3 | OAM: start = $0200, size = $0100, type = rw, file = ""; 4 | RAM: start = $0300, size = $0500, type = rw, file = ""; 5 | HDR: start = $0000, size = $0010, type = ro, file = %O, fill = yes, fillval = $00; 6 | PRG: start = $8000, size = $8000, type = ro, file = %O, fill = yes, fillval = $00; 7 | CHR: start = $0000, size = $2000, type = ro, file = %O, fill = yes, fillval = $00; 8 | } 9 | 10 | SEGMENTS { 11 | ZEROPAGE: load = ZP, type = zp; 12 | OAM: load = OAM, type = bss, align = $100; 13 | BSS: load = RAM, type = bss; 14 | HEADER: load = HDR, type = ro; 15 | CODE: load = PRG, type = ro, start = $8000; 16 | RODATA: load = PRG, type = ro; 17 | VECTORS: load = PRG, type = ro, start = $FFFA; 18 | TILES: load = CHR, type = ro; 19 | } 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Michael Billington 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 8-Bit Table Tennis [![Build Status](https://travis-ci.org/mike42/8bit-table-tennis.svg?branch=master)](https://travis-ci.org/mike42/8bit-table-tennis) 2 | 3 | 8-Bit Table Tennis is a homebrew 2-player game for the Nintendo Entertainment System (NES). 4 | 5 | ![Start screen](screenshot/table_tennis_start_screen.png) 6 | 7 | ![Start screen](screenshot/table_tennis_gameplay_00.png) 8 | 9 | ## Gameplay 10 | 11 | - Push START to begin. 12 | - Press UP/DOWN to move. 13 | - Press A to serve. 14 | - Player 1 serves first. 15 | - If you lose the point, then you serve next. 16 | - First player to 11 points wins. 17 | 18 | ## Try it 19 | 20 | The most recent version is available in the [releases](https://github.com/mike42/8bit-table-tennis/releases) section. It is shipped as an iNES ROM (`.nes`), which is suitable for use in a NES emulator or flash cartridge. 21 | 22 | ## Build it 23 | 24 | The steps to build this project are listed in [build.sh](https://github.com/mike42/8bit-table-tennis/blob/master/build.sh). You will need `python3` and the `cc65` toolchain installed. 25 | 26 | ## License 27 | 28 | 8-Bit Table Tennis may be used, distributed and modified under the terms of the MIT license, see [LICENSE](https://github.com/mike42/8bit-table-tennis/blob/master/LICENSE) for details. 29 | 30 | The code is based on a NES example project by Brad Smith, which can be found at [bbbradsmith/NES-ca65-example](https://github.com/bbbradsmith/NES-ca65-example). 31 | 32 | -------------------------------------------------------------------------------- /tools/table_tennis_fceux_symbols.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | table_tennis_fceux_symbols: Convert debug symbols to FCEUX-format. 4 | 5 | Based on https://github.com/bbbradsmith/NES-ca65-example 6 | """ 7 | import sys 8 | assert sys.version_info[0] >= 3, "Python 3 required." 9 | 10 | from collections import OrderedDict 11 | 12 | def label_to_nl(label_file, nl_file, range_min, range_max): 13 | labels = [] 14 | try: 15 | of = open(label_file, "rt") 16 | labels = of.readlines() 17 | except IOError: 18 | print("skipped: "+label_file) 19 | return 20 | labs = {} 21 | sout = "" 22 | for line in labels: 23 | words = line.split() 24 | if (words[0] == "al"): 25 | adr = int(words[1], 16) 26 | sym = words[2] 27 | sym = sym.lstrip('.') 28 | if (sym[0] == '_' and sym[1] == '_'): 29 | continue # skip compiler internals 30 | if (adr >= range_min and adr <= range_max): 31 | if (adr in labs): 32 | # multiple symbol 33 | text = labs[adr] 34 | textsplit = text.split() 35 | if (sym not in textsplit): 36 | text = text + " " + sym 37 | labs[adr] = text 38 | else: 39 | labs[adr] = sym 40 | for (adr, sym) in labs.items(): 41 | sout += ("$%04X#%s#\n" % (adr, sym)) 42 | open(nl_file, "wt").write(sout) 43 | print("debug symbols: " + nl_file) 44 | 45 | if __name__ == "__main__": 46 | label_to_nl("build/table_tennis.labels.txt", "build/table_tennis.nes.ram.nl", 0x0000, 0x7FF) 47 | label_to_nl("build/table_tennis.labels.txt", "build/table_tennis.nes.0.nl", 0x8000, 0xBFFF) 48 | label_to_nl("build/table_tennis.labels.txt", "build/table_tennis.nes.1.nl", 0xC000, 0xFFFF) 49 | -------------------------------------------------------------------------------- /tools/chr_tool.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | chr_tool: Convert between PNG and CHR formats. 4 | """ 5 | import argparse 6 | import logging 7 | import os 8 | 9 | from PIL import Image 10 | from PIL.PngImagePlugin import PngImageFile 11 | 12 | 13 | def indexes_from_image(image: PngImageFile, sprite_idx: int) -> list: 14 | assert(0 <= sprite_idx <= 255) 15 | sprite_x = (sprite_idx % 16) * 8 16 | sprite_y = (sprite_idx // 16) * 8 17 | assert (0 <= sprite_x <= (128 - 8)) 18 | assert (0 <= sprite_y <= (128 - 8)) 19 | ret = [0 for _ in range(0, 64)] 20 | for y in range(0, 8): 21 | for x in range(0, 8): 22 | ret[y * 8 + x] = image.getpixel((x + sprite_x, y + sprite_y)) 23 | assert(len(ret) == 64) 24 | return ret 25 | 26 | 27 | def pixel_values_to_chr(values: list) -> bytes: 28 | assert(len(values) == 64) 29 | # First color bit 30 | ret_list = [0 for _ in range(0, 16)] 31 | for byte_id in range(0, 8): 32 | plane0_byte_val = 0 33 | plane1_byte_val = 0 34 | for pixel_id in range(0, 8): 35 | plane0_byte_val |= (values[byte_id * 8 + pixel_id] & 0x01) << (7 - pixel_id) 36 | plane1_byte_val |= ((values[byte_id * 8 + pixel_id] >> 1) & 0x01) << (7 - pixel_id) 37 | ret_list[byte_id] = plane0_byte_val 38 | ret_list[byte_id + 8] = plane1_byte_val 39 | return bytes(ret_list) 40 | 41 | 42 | def png_to_chr(png_file: str, chr_file: str): 43 | logging.info("PNG to CHR: {} {}".format(png_file, chr_file)) 44 | im = Image.open(png_file) 45 | # Do some checks 46 | if not isinstance(im, PngImageFile): 47 | logging.error("Expected PNG image file") 48 | return 1 49 | if im.size != (128, 128): 50 | # Note: Could change this code to handle any multiple of 8 pixels, but 128x128 is fairly conventional 51 | logging.error("Expected 128 x 128 image, got {}".format(im.size)) 52 | return 1 53 | pixel_range = im.getextrema() 54 | if pixel_range != (0, 3): 55 | # TODO lax this up a max and a min 56 | logging.error("Expected indexed data in the range (0, 3), got {}".format(pixel_range)) 57 | return 1 58 | # Load in each sprite 59 | sprite_pixel_values = [indexes_from_image(im, idx) for idx in range(0, 256)] 60 | # Convert to CHR-formatted sprites 61 | sprite_chr_data = [pixel_values_to_chr(x) for x in sprite_pixel_values] 62 | # Stitch together 63 | chr_file_content = b"".join(sprite_chr_data) 64 | assert(len(chr_file_content) == 4096) 65 | open(chr_file, "wb").write(chr_file_content) 66 | return 0 67 | 68 | 69 | def chr_to_pixel_values(chr_data: bytes): 70 | assert(len(chr_data) == 16) 71 | int_data = list(chr_data) 72 | pixel_values = [0 for _ in range(0, 64)] 73 | for pixel_id in range(0, 64): 74 | byte_id_1 = pixel_id // 8 75 | byte_id_2 = byte_id_1 + 8 76 | byte_1 = int_data[byte_id_1] 77 | byte_2 = int_data[byte_id_2] 78 | bit_id = 7 - (pixel_id % 8) 79 | bit_1 = (byte_1 >> bit_id) & 0x01 80 | bit_2 = (byte_2 >> bit_id) & 0x01 81 | idx = (bit_1 << 1) | bit_2 82 | pixel_values[pixel_id] = idx 83 | return pixel_values 84 | 85 | 86 | def add_indexes_to_image(image: Image, sprite_pixel_values: list, sprite_idx): 87 | assert(len(sprite_pixel_values) == 64) 88 | assert(0 <= sprite_idx <= 255) 89 | sprite_x = (sprite_idx % 16) * 8 90 | sprite_y = (sprite_idx // 16) * 8 91 | assert (0 <= sprite_x <= (128 - 8)) 92 | assert (0 <= sprite_y <= (128 - 8)) 93 | for y in range(0, 8): 94 | for x in range(0, 8): 95 | image.putpixel((x + sprite_x, y + sprite_y), sprite_pixel_values[y * 8 + x]) 96 | 97 | def chr_to_png(chr_file: str, png_file: str): 98 | data_bytes = open(chr_file, "rb").read() 99 | if len(data_bytes) != 4096: 100 | logging.error("Expected exactly 4096 bytes in {}, got {}".format(chr_file, len(data_bytes))) 101 | # Calculate per-sprite pixel values 102 | sprite_chr_data = [data_bytes[sprite_idx * 16:(sprite_idx + 1) * 16] for sprite_idx in range(0, 256)] 103 | sprite_pixel_values = [chr_to_pixel_values(x) for x in sprite_chr_data] 104 | # Set up new image and palette 105 | im = Image.new(mode="P", size=(128, 128), color=0) 106 | im_palette = [0 for _ in range(0, 768)] 107 | display_cols = [ 108 | (0x00, 0x00, 0x00), 109 | (0x7c, 0x7c, 0x7c), 110 | (0xbc, 0xbc, 0xbc), 111 | (0xfc, 0xfc, 0xfc) 112 | ] 113 | for x in range(0, 4): 114 | for y in range(0, 3): 115 | im_palette[x * 3 + y] = display_cols[x][y] 116 | im.putpalette(im_palette) 117 | # Paste in the sprites 118 | for sprite_idx in range(0, 256): 119 | add_indexes_to_image(im, sprite_pixel_values[sprite_idx], sprite_idx) 120 | im.save(png_file) 121 | 122 | 123 | def main(): 124 | parser = argparse.ArgumentParser(description='Tool to read and write NES CHR graphics data. PNG files must be a ' 125 | '2bpp pixel map at 128x128, while CHR files must be 4096 bytes each') 126 | parser.add_argument('--verbose', '-v', action='count', default=0) 127 | parser.add_argument('input', type=str, help='Input filename (required). If this ends with ".PNG" extension, ' 128 | 'then it will be read and converted to CHR. If it does not, ' 129 | 'then it will be read as a CHR file, and converted to PNG.', 130 | metavar='INPUT') 131 | parser.add_argument('--output', type=str, help='Output filename', metavar='OUTPUT') 132 | args = parser.parse_args() 133 | 134 | # Set log level 135 | if args.verbose > 1: 136 | log_level = logging.DEBUG 137 | elif args.verbose == 1: 138 | log_level = logging.INFO 139 | else: 140 | log_level = logging.WARNING 141 | logging.basicConfig(level=log_level) 142 | 143 | # Actually run 144 | if args.input.endswith('.png') or args.input.endswith('.PNG'): 145 | output_filename = os.path.splitext(args.input)[0]+'.chr' if args.output is None else args.output 146 | return png_to_chr(args.input, output_filename) 147 | else: 148 | output_filename = os.path.splitext(args.input)[0]+'.png' if args.output is None else args.output 149 | return chr_to_png(args.input, output_filename) 150 | 151 | 152 | if __name__ == "__main__": 153 | ret = main() 154 | exit(ret) 155 | -------------------------------------------------------------------------------- /table_tennis.s: -------------------------------------------------------------------------------- 1 | ; 2 | ; table_tennis.s 3 | ; 4 | ; 6502 assembly for 8bit table tennis 5 | ; Based on https://github.com/bbbradsmith/NES-ca65-example 6 | 7 | ; iNES header 8 | ; 9 | 10 | .segment "HEADER" 11 | 12 | INES_MAPPER = 0 ; 0 = NROM 13 | INES_MIRROR = 1 ; 0 = horizontal mirroring, 1 = vertical mirroring 14 | INES_SRAM = 0 ; 1 = battery backed SRAM at $6000-7FFF 15 | 16 | .byte 'N', 'E', 'S', $1A ; ID 17 | .byte $02 ; 16k PRG chunk count 18 | .byte $01 ; 8k CHR chunk count 19 | .byte INES_MIRROR | (INES_SRAM << 1) | ((INES_MAPPER & $f) << 4) 20 | .byte (INES_MAPPER & %11110000) 21 | .byte $0, $0, $0, $0, $0, $0, $0, $0 ; padding 22 | 23 | ; 24 | ; CHR ROM 25 | ; 26 | 27 | .segment "TILES" 28 | .incbin "build/background.chr" 29 | .incbin "build/sprite.chr" 30 | 31 | ; 32 | ; vectors placed at top 6 bytes of memory area 33 | ; 34 | 35 | .segment "VECTORS" 36 | .word nmi 37 | .word reset 38 | .word irq 39 | 40 | ; 41 | ; reset routine 42 | ; 43 | 44 | .segment "CODE" 45 | reset: 46 | sei ; mask interrupts 47 | lda #0 48 | sta $2000 ; disable NMI 49 | sta $2001 ; disable rendering 50 | sta $4015 ; disable APU sound 51 | sta $4010 ; disable DMC IRQ 52 | lda #$40 53 | sta $4017 ; disable APU IRQ 54 | cld ; disable decimal mode 55 | ldx #$FF 56 | txs ; initialize stack 57 | ; wait for first vblank 58 | bit $2002 59 | : 60 | bit $2002 61 | bpl :- 62 | ; clear all RAM to 0 63 | lda #0 64 | ldx #0 65 | : 66 | sta $0000, X 67 | sta $0100, X 68 | sta $0200, X 69 | sta $0300, X 70 | sta $0400, X 71 | sta $0500, X 72 | sta $0600, X 73 | sta $0700, X 74 | inx 75 | bne :- 76 | ; place all sprites offscreen at Y=255 77 | lda #255 78 | ldx #0 79 | : 80 | sta oam, X 81 | inx 82 | inx 83 | inx 84 | inx 85 | bne :- 86 | ; wait for second vblank 87 | : 88 | bit $2002 89 | bpl :- 90 | ; NES is initialized, ready to begin! 91 | ; enable the NMI for graphical updates, and jump to our main program 92 | lda #%10001000 93 | sta $2000 94 | jmp title_screen 95 | 96 | ; 97 | ; nmi routine 98 | ; 99 | 100 | .segment "ZEROPAGE" 101 | nmi_lock: .res 1 ; prevents NMI re-entry 102 | nmi_count: .res 1 ; is incremented every NMI 103 | nmi_ready: .res 1 ; set to 1 to push a PPU frame update, 2 to turn rendering off next NMI 104 | nmt_update_len: .res 1 ; number of bytes in nmt_update buffer 105 | scroll_x: .res 1 ; x scroll position 106 | scroll_y: .res 1 ; y scroll position 107 | scroll_nmt: .res 1 ; nametable select (0-3 = $2000,$2400,$2800,$2C00) 108 | temp: .res 1 ; temporary variable 109 | i: .res 1 ; loop indices 110 | j: .res 1 111 | 112 | .segment "BSS" 113 | nmt_update: .res 256 ; nametable update entry buffer for PPU update 114 | palette: .res 32 ; palette buffer for PPU update 115 | player1_name: .res 8 116 | player2_name: .res 8 117 | 118 | .segment "OAM" 119 | oam: .res 256 ; sprite OAM data to be uploaded by DMA 120 | 121 | .segment "CODE" 122 | nmi: 123 | ; save registers 124 | pha 125 | txa 126 | pha 127 | tya 128 | pha 129 | ; prevent NMI re-entry 130 | lda nmi_lock 131 | beq :+ 132 | jmp @nmi_end 133 | : 134 | lda #1 135 | sta nmi_lock 136 | ; increment frame counter 137 | inc nmi_count 138 | ; 139 | lda nmi_ready 140 | bne :+ ; nmi_ready == 0 not ready to update PPU 141 | jmp @ppu_update_end 142 | : 143 | cmp #2 ; nmi_ready == 2 turns rendering off 144 | bne :+ 145 | lda #%00000000 146 | sta $2001 147 | ldx #0 148 | stx nmi_ready 149 | jmp @ppu_update_end 150 | : 151 | ; sprite OAM DMA 152 | ldx #0 153 | stx $2003 154 | lda #>oam 155 | sta $4014 156 | ; palettes 157 | lda #%10001000 158 | sta $2000 ; set horizontal nametable increment 159 | lda $2002 160 | lda #$3F 161 | sta $2006 162 | stx $2006 ; set PPU address to $3F00 163 | ldx #0 164 | : 165 | lda palette, X 166 | sta $2007 167 | inx 168 | cpx #32 169 | bcc :- 170 | ; nametable update 171 | ldx #0 172 | cpx nmt_update_len 173 | bcs @scroll 174 | @nmt_update_loop: 175 | lda nmt_update, X 176 | sta $2006 177 | inx 178 | lda nmt_update, X 179 | sta $2006 180 | inx 181 | lda nmt_update, X 182 | sta $2007 183 | inx 184 | cpx nmt_update_len 185 | bcc @nmt_update_loop 186 | lda #0 187 | sta nmt_update_len 188 | @scroll: 189 | lda scroll_nmt 190 | and #%00000011 ; keep only lowest 2 bits to prevent error 191 | ora #%10001000 192 | sta $2000 193 | lda scroll_x 194 | sta $2005 195 | lda scroll_y 196 | sta $2005 197 | ; enable rendering 198 | lda #%00011110 199 | sta $2001 200 | ; flag PPU update complete 201 | ldx #0 202 | stx nmi_ready 203 | @ppu_update_end: 204 | ; if this engine had music/sound, this would be a good place to play it 205 | ; unlock re-entry flag 206 | lda #0 207 | sta nmi_lock 208 | @nmi_end: 209 | ; restore registers and return 210 | pla 211 | tay 212 | pla 213 | tax 214 | pla 215 | rti 216 | 217 | ; 218 | ; irq 219 | ; 220 | 221 | .segment "CODE" 222 | irq: 223 | rti 224 | 225 | ; 226 | ; drawing utilities 227 | ; 228 | 229 | .segment "CODE" 230 | 231 | ; ppu_update: waits until next NMI, turns rendering on (if not already), uploads OAM, palette, and nametable update to PPU 232 | ppu_update: 233 | lda #1 234 | sta nmi_ready 235 | : 236 | lda nmi_ready 237 | bne :- 238 | rts 239 | 240 | ; ppu_skip: waits until next NMI, does not update PPU 241 | ppu_skip: 242 | lda nmi_count 243 | : 244 | cmp nmi_count 245 | beq :- 246 | rts 247 | 248 | ; ppu_off: waits until next NMI, turns rendering off (now safe to write PPU directly via $2007) 249 | ppu_off: 250 | lda #2 251 | sta nmi_ready 252 | : 253 | lda nmi_ready 254 | bne :- 255 | rts 256 | 257 | ; ppu_address_tile: use with rendering off, sets memory address to tile at X/Y, ready for a $2007 write 258 | ; Y = 0- 31 nametable $2000 259 | ; Y = 32- 63 nametable $2400 260 | ; Y = 64- 95 nametable $2800 261 | ; Y = 96-127 nametable $2C00 262 | ppu_address_tile: 263 | lda $2002 ; reset latch 264 | tya 265 | lsr 266 | lsr 267 | lsr 268 | ora #$20 ; high bits of Y + $20 269 | sta $2006 270 | tya 271 | asl 272 | asl 273 | asl 274 | asl 275 | asl 276 | sta temp 277 | txa 278 | ora temp 279 | sta $2006 ; low bits of Y + X 280 | rts 281 | 282 | ; ppu_update_tile: can be used with rendering on, sets the tile at X/Y to tile A next time you call ppu_update 283 | ppu_update_tile: 284 | pha ; temporarily store A on stack 285 | txa 286 | pha ; temporarily store X on stack 287 | ldx nmt_update_len 288 | tya 289 | lsr 290 | lsr 291 | lsr 292 | ora #$20 ; high bits of Y + $20 293 | sta nmt_update, X 294 | inx 295 | tya 296 | asl 297 | asl 298 | asl 299 | asl 300 | asl 301 | sta temp 302 | pla ; recover X value (but put in A) 303 | ora temp 304 | sta nmt_update, X 305 | inx 306 | pla ; recover A value (tile) 307 | sta nmt_update, X 308 | inx 309 | stx nmt_update_len 310 | rts 311 | 312 | ; ppu_update_byte: like ppu_update_tile, but X/Y makes the high/low bytes of the PPU address to write 313 | ; this may be useful for updating attribute tiles 314 | ppu_update_byte: 315 | pha ; temporarily store A on stack 316 | tya 317 | pha ; temporarily store Y on stack 318 | ldy nmt_update_len 319 | txa 320 | sta nmt_update, Y 321 | iny 322 | pla ; recover Y value (but put in Y) 323 | sta nmt_update, Y 324 | iny 325 | pla ; recover A value (byte) 326 | sta nmt_update, Y 327 | iny 328 | sty nmt_update_len 329 | rts 330 | 331 | ; 332 | ; gamepad 333 | ; 334 | 335 | PAD_A = $01 336 | PAD_B = $02 337 | PAD_SELECT = $04 338 | PAD_START = $08 339 | PAD_U = $10 340 | PAD_D = $20 341 | PAD_L = $40 342 | PAD_R = $80 343 | 344 | .segment "ZEROPAGE" 345 | gamepad_player_1: .res 1 346 | gamepad_player_2: .res 1 347 | 348 | .segment "CODE" 349 | ; gamepad_poll: this reads the gamepad state into the variable labelled "gamepad" 350 | ; If DPCM samples are played they can conflict with gamepad reading, which 351 | ; may give incorrect results. 352 | gamepad_poll_player1: 353 | ; strobe the gamepad to latch current button state 354 | lda #1 355 | sta $4016 356 | lda #0 357 | sta $4016 358 | ; read 8 bytes from the interface at $4016 359 | ldx #8 360 | : 361 | pha 362 | lda $4016 363 | ; combine low two bits and store in carry bit 364 | and #%00000011 365 | cmp #%00000001 366 | pla 367 | ; rotate carry into gamepad variable 368 | ror 369 | dex 370 | bne :- 371 | sta gamepad_player_1 372 | rts 373 | 374 | gamepad_poll_player2: 375 | ; strobe the gamepad to latch current button state 376 | lda #1 377 | sta $4017 378 | lda #0 379 | sta $4017 380 | ; read 8 bytes from the interface at $4016 381 | ldx #8 382 | : 383 | pha 384 | lda $4017 385 | ; combine low two bits and store in carry bit 386 | and #%00000011 387 | cmp #%00000001 388 | pla 389 | ; rotate carry into gamepad variable 390 | ror 391 | dex 392 | bne :- 393 | sta gamepad_player_2 394 | rts 395 | 396 | ; 397 | ; main 398 | ; 399 | 400 | .segment "RODATA" 401 | example_palette: 402 | .byte $0F,$15,$26,$37 ; bg0 purple/pink 403 | .byte $0F,$09,$19,$29 ; bg1 green 404 | .byte $0F,$01,$11,$21 ; bg2 blue 405 | .byte $0F,$18,$28,$38 ; bg3 yellow 406 | .byte $0F,$12,$22,$32 ; sp0 marine 407 | .byte $0F,$00,$10,$30 ; sp1 greyscale 408 | .byte $0F,$14,$24,$34 ; sp2 purple 409 | .byte $0F,$1B,$2B,$3B ; sp3 teal 410 | 411 | player1_name_default: 412 | .byte $12,$0E,$03,$1B,$07,$14,$00,$21 413 | player2_name_default: 414 | .byte $12,$0E,$03,$1B,$07,$14,$00,$22 415 | game_over_text: 416 | .byte $09,$03,$03,$07,$00,$11,$18,$07,$14 417 | start_screen_tiles_1: 418 | .byte $2a,$32,$28,$1d,$04,$0b,$16,$30,$31,$31,$31,$31,$31,$31,$31,$31,$31,$31,$31,$2b 419 | .byte $2c,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$2f 420 | .byte $2c,$00,$34,$35,$36,$39,$3a,$3b,$42,$43,$44,$4c,$00,$00,$42,$4d,$36,$00,$00,$2f 421 | .byte $2c,$00,$00,$37,$00,$3c,$3d,$3e,$3c,$45,$46,$37,$00,$00,$3c,$4e,$00,$00,$00,$2f 422 | .byte $2c,$00,$00,$37,$00,$3f,$40,$41,$3f,$47,$48,$37,$00,$00,$3f,$4f,$00,$00,$00,$2f 423 | .byte $2c,$00,$00,$38,$00,$38,$00,$38,$49,$4a,$4b,$49,$4d,$36,$49,$4d,$36,$00,$00,$2f 424 | .byte $2c,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$2f 425 | .byte $2c,$00,$34,$35,$36,$42,$4d,$36,$50,$51,$4c,$50,$51,$4c,$4c,$58,$4d,$36,$00,$2f 426 | start_screen_tiles_2: 427 | .byte $2c,$00,$00,$37,$00,$3c,$4e,$00,$52,$53,$37,$52,$53,$37,$37,$59,$3d,$5a,$00,$2f 428 | .byte $2c,$00,$00,$37,$00,$3f,$4f,$00,$37,$54,$55,$37,$54,$55,$37,$5b,$40,$41,$00,$2f 429 | .byte $2c,$00,$00,$38,$00,$49,$4d,$36,$38,$56,$57,$38,$56,$57,$38,$34,$4d,$5c,$00,$2f 430 | .byte $2c,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$2f 431 | .byte $2d,$33,$33,$33,$33,$33,$33,$33,$33,$33,$33,$33,$33,$33,$33,$33,$33,$33,$33,$2e 432 | .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 433 | .byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00 434 | .byte $00,$00,$00,$00,$00,$12,$17,$15,$0a,$00,$15,$16,$03,$14,$16,$00,$00,$00,$00,$00 435 | 436 | .segment "ZEROPAGE" 437 | player1_x: .res 1 438 | player1_y: .res 1 439 | player1_size: .res 1 ; not yet used 440 | player2_x: .res 1 441 | player2_y: .res 1 442 | player2_size: .res 1 ; not yet used 443 | ball_x: .res 1 444 | ball_y: .res 1 445 | ball_x_speed: .res 1 446 | ball_y_speed: .res 1 447 | ball_state: .res 1 448 | player1_score: .res 1 449 | player2_score: .res 1 450 | state_delay_counter: .res 1 451 | 452 | BALL_LEFT = $01 453 | BALL_UP = $02 454 | BALL_SERVE = $04 455 | BALL_OUT = $08 456 | GAME_OVER = $10 457 | 458 | .segment "CODE" 459 | title_screen: 460 | ; setup 461 | ldx #0 462 | : 463 | lda example_palette, X 464 | sta palette, X 465 | inx 466 | cpx #32 467 | bcc :- 468 | ; default player names 469 | ldx #0 470 | : 471 | lda player1_name_default, X 472 | sta player1_name, X 473 | lda player2_name_default, X 474 | sta player2_name, X 475 | inx 476 | cpx #8 477 | bcc :- 478 | jsr setup_background 479 | lda #1 480 | sta scroll_nmt 481 | 482 | title_screen_loop: 483 | ; read gamepad 484 | jsr gamepad_poll_player1 485 | jsr gamepad_poll_player2 486 | lda gamepad_player_1 487 | and #PAD_START 488 | beq :+ 489 | lda #0 490 | sta scroll_nmt 491 | jmp main 492 | : 493 | 494 | @title_screen_draw: 495 | ; draw everything and finish the frame 496 | jsr ppu_update 497 | ; keep doing this forever! 498 | jmp title_screen_loop 499 | 500 | main: 501 | ; place player sprites 502 | lda #0 503 | sta player1_x 504 | lda #248 505 | sta player2_x 506 | lda #110 507 | sta player1_y 508 | sta player2_y 509 | lda #4 ; not yet used 510 | sta player1_size 511 | sta player2_size 512 | lda #((256/2) - 4) 513 | sta ball_x 514 | lda #((240/2) - 4) 515 | sta ball_y 516 | lda #BALL_SERVE 517 | sta ball_state 518 | lda #2 519 | sta ball_x_speed 520 | lda #0 521 | sta player1_score 522 | sta player2_score 523 | lda #1 524 | sta ball_y_speed 525 | ; show the screen 526 | jsr draw_game_sprites 527 | jsr ppu_update 528 | ; main loop 529 | @loop: 530 | ; read gamepad 531 | jsr gamepad_poll_player1 532 | jsr gamepad_poll_player2 533 | ; Game over - reset after a while 534 | clc 535 | lda ball_state 536 | and #GAME_OVER 537 | beq :++ 538 | ; freeze the game for a moment 539 | dec state_delay_counter 540 | lda state_delay_counter 541 | clc 542 | cmp #0 543 | bne :+ 544 | ; reset counters, ball state 545 | lda #BALL_SERVE 546 | sta ball_state 547 | lda #0 548 | sta player1_score 549 | sta player2_score 550 | jsr update_scores 551 | ; Reset to center 552 | jsr clear_game_sprites 553 | lda #1 554 | sta scroll_nmt 555 | jmp title_screen_loop 556 | : 557 | jmp @draw 558 | : 559 | ; Ball out - change to a serve after a while 560 | clc 561 | lda ball_state 562 | and #BALL_OUT 563 | beq :++ 564 | ; freeze the game for a moment 565 | dec state_delay_counter 566 | lda state_delay_counter 567 | clc 568 | cmp #0 569 | bne :+ 570 | ; Reset to center 571 | lda #((256/2) - 4) 572 | sta ball_x 573 | lda #((240/2) - 4) 574 | sta ball_y 575 | ; Being careful to preserve L/R direction 576 | lda #BALL_SERVE 577 | eor ball_state 578 | sta ball_state 579 | lda #BALL_OUT 580 | eor ball_state 581 | sta ball_state 582 | : 583 | jmp @draw 584 | : 585 | ; respond to gamepad state 586 | lda gamepad_player_1 587 | and #PAD_U 588 | beq :+ 589 | jsr player_1_up 590 | : 591 | lda gamepad_player_1 592 | and #PAD_D 593 | beq :+ 594 | jsr player_1_down 595 | : 596 | lda gamepad_player_1 597 | and #PAD_A 598 | beq :+ 599 | jsr player_1_a 600 | : 601 | lda gamepad_player_2 602 | and #PAD_U 603 | beq :+ 604 | jsr player_2_up 605 | : 606 | lda gamepad_player_2 607 | and #PAD_D 608 | beq :+ 609 | jsr player_2_down 610 | : 611 | lda gamepad_player_2 612 | and #PAD_A 613 | beq :+ 614 | jsr player_2_a 615 | : 616 | jsr apply_physics 617 | @draw: 618 | ; draw everything and finish the frame 619 | jsr draw_game_sprites 620 | jsr ppu_update 621 | ; keep doing this forever! 622 | jmp @loop 623 | 624 | apply_physics: ; yeah 'physics' 625 | ; Waiting for a serve 626 | clc 627 | lda ball_state 628 | and #BALL_SERVE 629 | beq :+ 630 | rts 631 | : 632 | clc 633 | lda ball_state 634 | and #BALL_UP 635 | beq :+ 636 | jsr ball_up_physics 637 | : 638 | clc 639 | lda ball_state 640 | and #BALL_UP 641 | bne :+ 642 | jsr ball_down_physics 643 | : 644 | ; Move left 645 | clc 646 | lda ball_state 647 | and #BALL_LEFT 648 | beq :+ 649 | jsr ball_left_physics 650 | : 651 | clc 652 | lda ball_state 653 | and #BALL_LEFT 654 | bne :+ 655 | jsr ball_right_physics 656 | : 657 | rts 658 | 659 | ball_up_physics: 660 | ; Move up 661 | lda ball_y 662 | sec 663 | sbc ball_y_speed 664 | sta ball_y 665 | ; Test if bouncing (up) 666 | cmp #(4*8) 667 | bcs :+ 668 | lda #(4*8) 669 | sta ball_y 670 | lda #BALL_UP 671 | eor ball_state 672 | sta ball_state 673 | : 674 | rts 675 | 676 | ball_down_physics: 677 | ; Move down 678 | lda ball_y_speed 679 | adc ball_y 680 | sta ball_y 681 | ; Test if bouncing (bottom) 682 | cmp #(27*8) 683 | bcc :+ 684 | lda #(27*8) 685 | sta ball_y 686 | lda #BALL_UP 687 | eor ball_state 688 | sta ball_state 689 | : 690 | rts 691 | 692 | ball_left_physics: 693 | lda ball_x 694 | sec 695 | sbc ball_x_speed 696 | sta ball_x 697 | ; Test if bouncing (left) 698 | cmp #(8-1) 699 | bcs :+ 700 | ; always bounce off left 701 | lda #BALL_LEFT 702 | eor ball_state 703 | sta ball_state 704 | ; test if ball is too high 705 | clc 706 | lda ball_y 707 | adc #(8) 708 | cmp player1_y 709 | bcc @player_1_missed 710 | ; test if ball is too low 711 | clc 712 | lda player1_y 713 | adc #(8*4) 714 | cmp ball_y 715 | bcc @player_1_missed 716 | @player_1_ok: 717 | lda #(8) 718 | sta ball_x 719 | : 720 | rts 721 | @player_1_missed: 722 | lda #BALL_OUT 723 | eor ball_state 724 | sta ball_state 725 | lda #100 726 | sta state_delay_counter 727 | inc player2_score 728 | jsr update_scores 729 | jsr check_game_over 730 | rts 731 | 732 | 733 | ball_right_physics: 734 | ; Move right 735 | lda ball_x 736 | adc ball_x_speed 737 | sta ball_x 738 | ; Test if bouncing (right) 739 | cmp #(30*8+1) 740 | bcc :+ 741 | ; always bounce off right 742 | lda #BALL_LEFT 743 | eor ball_state 744 | sta ball_state 745 | ; test if ball is too high 746 | clc 747 | lda ball_y 748 | adc #(8) 749 | cmp player2_y 750 | bcc @player_2_missed 751 | ; test if ball is too low 752 | clc 753 | lda player2_y 754 | adc #(8*4) 755 | cmp ball_y 756 | bcc @player_2_missed 757 | @player_2_ok: 758 | lda #(30*8) 759 | sta ball_x 760 | : 761 | rts 762 | @player_2_missed: 763 | lda #BALL_OUT 764 | eor ball_state 765 | sta ball_state 766 | lda #100 767 | sta state_delay_counter 768 | inc player1_score 769 | jsr update_scores 770 | jsr check_game_over 771 | rts 772 | 773 | 774 | player_1_up: 775 | dec player1_y 776 | dec player1_y 777 | dec player1_y 778 | ; max y-value 779 | lda player1_y 780 | cmp #(4*8-1) 781 | bcs :+ 782 | lda #(4*8-1) 783 | sta player1_y 784 | : 785 | rts 786 | 787 | player_1_down: 788 | ; TODO account for paddle height 789 | inc player1_y 790 | inc player1_y 791 | inc player1_y 792 | lda player1_y 793 | cmp #(24*8-1) 794 | bcc :+ 795 | lda #(24*8-1) 796 | sta player1_y 797 | : 798 | rts 799 | 800 | player_2_up: 801 | dec player2_y 802 | dec player2_y 803 | dec player2_y 804 | ; max y-value 805 | lda player2_y 806 | cmp #(4*8-1) 807 | bcs :+ 808 | lda #(4*8-1) 809 | sta player2_y 810 | : 811 | rts 812 | 813 | player_2_down: 814 | ; TODO account for paddle height 815 | inc player2_y 816 | inc player2_y 817 | inc player2_y 818 | lda player2_y 819 | cmp #(24*8-1) 820 | bcc :+ 821 | lda #(24*8-1) 822 | sta player2_y 823 | : 824 | rts 825 | 826 | player_1_a: 827 | ; Might need to serve 828 | clc 829 | lda ball_state 830 | and #BALL_SERVE 831 | beq :++ 832 | lda ball_state 833 | and #BALL_LEFT 834 | bne :+ 835 | lda #BALL_SERVE 836 | eor ball_state 837 | sta ball_state 838 | : 839 | : 840 | rts 841 | 842 | player_2_a: 843 | ; Might need to serve 844 | clc 845 | lda ball_state 846 | and #BALL_SERVE 847 | beq :++ 848 | lda ball_state 849 | and #BALL_LEFT 850 | beq :+ 851 | lda #BALL_SERVE 852 | eor ball_state 853 | sta ball_state 854 | : 855 | : 856 | rts 857 | 858 | clear_game_sprites: 859 | ; TODO 860 | lda #250 861 | clc 862 | sta oam+(0*4)+0 863 | sta oam+(1*4)+0 864 | sta oam+(2*4)+0 865 | sta oam+(3*4)+0 866 | sta oam+(4*4)+0 867 | sta oam+(5*4)+0 868 | sta oam+(6*4)+0 869 | sta oam+(7*4)+0 870 | sta oam+(8*4)+0 871 | rts 872 | 873 | draw_game_sprites: 874 | ; player 1 paddle bricks y-pos 875 | lda player1_y 876 | clc 877 | sta oam+(0*4)+0 878 | adc #8 879 | sta oam+(1*4)+0 880 | adc #8 881 | sta oam+(2*4)+0 882 | adc #8 883 | sta oam+(3*4)+0 884 | ; player 2 paddle bricks y-pos 885 | lda player2_y 886 | clc 887 | sta oam+(4*4)+0 888 | adc #8 889 | sta oam+(5*4)+0 890 | adc #8 891 | sta oam+(6*4)+0 892 | adc #8 893 | sta oam+(7*4)+0 894 | lda ball_y 895 | sta oam+(8*4)+0 896 | lda #0 ; paddle brick tile 897 | sta oam+(0*4)+1 898 | sta oam+(1*4)+1 899 | sta oam+(2*4)+1 900 | sta oam+(3*4)+1 901 | sta oam+(4*4)+1 902 | sta oam+(5*4)+1 903 | sta oam+(6*4)+1 904 | sta oam+(7*4)+1 905 | lda #2 ; ball tile 906 | sta oam+(8*4)+1 907 | ; attributes 908 | lda #%00000000 ; no flip 909 | sta oam+(0*4)+2 910 | sta oam+(1*4)+2 911 | sta oam+(2*4)+2 912 | sta oam+(3*4)+2 913 | sta oam+(4*4)+2 914 | sta oam+(5*4)+2 915 | sta oam+(6*4)+2 916 | sta oam+(7*4)+2 917 | lda #%00000001 ; greyscale 918 | sta oam+(8*4)+2 919 | ; x position 920 | lda player1_x 921 | sta oam+(0*4)+3 922 | sta oam+(1*4)+3 923 | sta oam+(2*4)+3 924 | sta oam+(3*4)+3 925 | lda player2_x 926 | sta oam+(4*4)+3 927 | sta oam+(5*4)+3 928 | sta oam+(6*4)+3 929 | sta oam+(7*4)+3 930 | lda ball_x 931 | sta oam+(8*4)+3 932 | rts 933 | 934 | draw_scores: 935 | ; (x, y) of score 936 | ldy #2 937 | ldx #13 938 | jsr ppu_address_tile 939 | lda #$20 940 | sta $2007 941 | sta $2007 942 | lda #0 943 | sta $2007 944 | sta $2007 945 | lda #$20 946 | sta $2007 947 | sta $2007 948 | rts 949 | 950 | update_scores: 951 | ; wow this is inefficient 952 | ; Player 1 score 953 | lda player1_score 954 | cmp #(10) 955 | bcc :+ 956 | ; Number >= 10 957 | ldy #2 958 | ldx #13 959 | lda #$21 960 | jsr ppu_update_tile 961 | ldx #14 962 | lda player1_score 963 | adc #($20 - 10) 964 | jsr ppu_update_tile 965 | jmp @end_update_player_1 966 | : 967 | ; Number < 10 968 | ldy #2 969 | ldx #13 970 | lda #$20 971 | jsr ppu_update_tile 972 | ldy #2 973 | ldx #14 974 | lda player1_score 975 | adc #$20 976 | jsr ppu_update_tile 977 | @end_update_player_1: 978 | ; Player 2 score 979 | lda player2_score 980 | cmp #(10) 981 | bcc :+ 982 | ; Number >= 10 983 | ldy #2 984 | ldx #17 985 | lda #$21 986 | jsr ppu_update_tile 987 | ldx #18 988 | lda player2_score 989 | adc #($20 - 10) 990 | jsr ppu_update_tile 991 | jmp @end_update_player_2 992 | : 993 | ; Number < 10 994 | ldy #2 995 | ldx #17 996 | lda #$20 997 | jsr ppu_update_tile 998 | ldy #2 999 | ldx #18 1000 | lda player2_score 1001 | adc #$20 1002 | jsr ppu_update_tile 1003 | @end_update_player_2: 1004 | rts 1005 | 1006 | check_game_over: 1007 | lda player1_score 1008 | cmp #(11) 1009 | bcs @it_is_game_over 1010 | lda player2_score 1011 | cmp #(11) 1012 | bcs @it_is_game_over 1013 | rts 1014 | @it_is_game_over: 1015 | lda #GAME_OVER 1016 | sta ball_state 1017 | rts 1018 | 1019 | setup_background: 1020 | ; first nametable, start by clearing to empty 1021 | lda $2002 ; reset latch 1022 | lda #$20 1023 | sta $2006 1024 | lda #$00 1025 | sta $2006 1026 | ; empty nametable 1027 | lda #0 1028 | ldy #30 ; 30 rows 1029 | : 1030 | ldx #32 ; 32 columns 1031 | : 1032 | sta $2007 1033 | dex 1034 | bne :- 1035 | dey 1036 | bne :-- 1037 | ; set all attributes to 0 1038 | ldx #64 ; 64 bytes 1039 | : 1040 | sta $2007 1041 | dex 1042 | bne :- 1043 | ; draw player 1 name 1044 | ldy #1 ; start at row 1 1045 | ldx #0 ; start at column 0 1046 | jsr ppu_address_tile 1047 | ldx #0 1048 | : 1049 | lda player1_name, X 1050 | sta $2007 1051 | inx 1052 | cpx #8 ; 8 characters to draw 1053 | bcc :- 1054 | ; draw player 2 name 1055 | ldy #1 ; start at row 1 1056 | ldx #24 ; start at column 24 1057 | jsr ppu_address_tile 1058 | ldx #0 1059 | : 1060 | lda player2_name, X 1061 | sta $2007 1062 | inx 1063 | cpx #8 ; 8 characters to draw 1064 | bcc :- 1065 | jsr draw_scores 1066 | ; draw top border 1067 | ldy #3 ; start at row 3 1068 | ldx #0 ; start at column 0 1069 | jsr ppu_address_tile 1070 | lda #1 ; geometric brick 1071 | ldx #32 ; columns to write 1072 | : 1073 | sta $2007 1074 | dex 1075 | bne :- 1076 | ; draw lower border 1077 | ldy #28 ; start at row 30 1078 | ldx #0 ; start at column 0 1079 | jsr ppu_address_tile 1080 | lda #1 ; geometric brick 1081 | ldx #32 ; columns to write 1082 | : 1083 | sta $2007 1084 | dex 1085 | bne :- 1086 | ; clear second nametable 1087 | lda $2002 ; reset latch 1088 | lda #$24 1089 | sta $2006 1090 | lda #$00 1091 | sta $2006 1092 | lda #0 ; blank tile 1093 | ldy #30 ; 30 rows 1094 | : 1095 | ldx #32 ; 32 columns 1096 | : 1097 | sta $2007 1098 | dex 1099 | bne :- 1100 | dey 1101 | bne :-- 1102 | ; set all attributes 1103 | lda #%01010101 ; palette 1 1104 | ldx #64 ; 64 bytes 1105 | : 1106 | sta $2007 1107 | dex 1108 | bne :- 1109 | ; Start screen - upper part 1110 | lda #0 ; tile number 0-100 1111 | sta i 1112 | ldy #0 ; row number 0-7 1113 | sty j 1114 | : ; for each row 1115 | ; set $2006 to memory address at start of row 1116 | clc 1117 | lda j 1118 | adc #(32+6) ; Y-position 1119 | clc 1120 | tay 1121 | ldx #6 ; X-position 1122 | jsr ppu_address_tile 1123 | ; write 20 tiles for the row 1124 | ldy i 1125 | ldx #0 1126 | : 1127 | lda start_screen_tiles_1, Y 1128 | sta $2007 1129 | inx 1130 | iny 1131 | cpx #20 ; 20 tiles for the row 1132 | bcc :- 1133 | ; for next iteration.. 1134 | sty i 1135 | ; increment row number, compare 1136 | inc j 1137 | ldx j 1138 | cpx #8 ; 8 rows to draw 1139 | bcc :-- 1140 | ; Start screen - lower part 1141 | lda #0 ; tile number 0-100 1142 | sta i 1143 | ldy #0 ; row number 0-7 1144 | sty j 1145 | : ; for each row 1146 | ; set $2006 to memory address at start of row 1147 | clc 1148 | lda j 1149 | adc #(32+6+8) ; Y-position 1150 | clc 1151 | tay 1152 | ldx #6 ; X-position 1153 | jsr ppu_address_tile 1154 | ; write 20 tiles for the row 1155 | ldy i 1156 | ldx #0 1157 | : 1158 | lda start_screen_tiles_2, Y 1159 | sta $2007 1160 | inx 1161 | iny 1162 | cpx #20 ; 20 tiles for the row 1163 | bcc :- 1164 | ; for next iteration.. 1165 | sty i 1166 | ; increment row number, compare 1167 | inc j 1168 | ldx j 1169 | cpx #8 ; 8 rows to draw 1170 | bcc :-- 1171 | rts 1172 | 1173 | ; 1174 | ; end of file 1175 | ; 1176 | --------------------------------------------------------------------------------