-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgame.s
More file actions
1176 lines (1061 loc) · 32 KB
/
Copy pathgame.s
File metadata and controls
1176 lines (1061 loc) · 32 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
; =============================================================================
; NES Diamond-Chase Game
; game.s -- 6502 assembly source (ca65 syntax)
;
; Controls : D-Pad moves the diamond sprite.
; Guide it into the yellow target zone to trigger a win.
; Press Start to play again.
; Easter egg: Up Up Down Down Left Right Left Right → 3-second colour flash.
;
; Mapper : NROM-256 (mapper 0), 2×16 KB PRG, 1×8 KB CHR
; Audio : Pulse 1 = background melody, Pulse 2 = sound effects
; =============================================================================
; ---------------------------------------------------------------------------
; iNES header
; ---------------------------------------------------------------------------
.segment "HEADER"
.byte $4E,$45,$53,$1A ; "NES" + MS-DOS EOF (bytes 0-3)
.byte 2 ; byte 4: 2×16 KB PRG-ROM banks (32 KB)
.byte 1 ; byte 5: 1×8 KB CHR-ROM bank
.byte $00 ; byte 6: mapper 0, horizontal mirroring
.byte $00 ; byte 7: mapper 0 upper nibble, iNES 1.0
.byte $00,$00,$00,$00,$00,$00,$00,$00 ; bytes 8-15: padding
; ---------------------------------------------------------------------------
; Zero-page variables
; ---------------------------------------------------------------------------
.segment "ZEROPAGE"
player_x: .res 1 ; sprite X pixel position
player_y: .res 1 ; sprite Y pixel position
game_state: .res 1 ; 0 = playing, 1 = won
frame_ready: .res 1 ; set by NMI, cleared by main loop
curr_btns: .res 1 ; current controller bitmask
prev_btns: .res 1 ; previous frame bitmask (edge detection)
new_dirs: .res 1 ; newly-pressed directional bits this frame
win_text_state: .res 1 ; 0=idle, 1=draw win text, 2=clear win text
; audio
music_idx: .res 1 ; current byte index into music_data
music_timer: .res 1 ; frames remaining on current note
music_active: .res 1 ; 1 = music is playing
sfx_active: .res 1 ; 0=none, 1=move blip, 2=win chime
sfx_idx: .res 1 ; current byte index into sfx data
sfx_timer: .res 1 ; frames remaining on current SFX note
; easter egg
code_buf: .res 8 ; ring buffer of last 8 directional inputs (1-4)
code_pos: .res 1 ; ring-buffer write head (0-7)
easter_active: .res 1 ; 1 = colour flash active
easter_timer: .res 1 ; frames remaining (starts at 180)
easter_color: .res 1 ; index into easter_colors table
; ---------------------------------------------------------------------------
; OAM shadow buffer at page $02 ($0200-$02FF)
; ---------------------------------------------------------------------------
.segment "OAM"
oam_buf: .res 256
; ---------------------------------------------------------------------------
; Hardware register equates
; ---------------------------------------------------------------------------
PPU_CTRL = $2000
PPU_MASK = $2001
PPU_STATUS = $2002
OAM_ADDR = $2003
PPU_SCROLL = $2005
PPU_ADDR = $2006
PPU_DATA = $2007
OAM_DMA = $4014
P1_CTRL = $4000
P1_SWEEP = $4001
P1_LO = $4002
P1_HI = $4003
P2_CTRL = $4004
P2_SWEEP = $4005
P2_LO = $4006
P2_HI = $4007
APU_STATUS = $4015
APU_FRAME = $4017
CTRL1 = $4016
; Controller button bits (read order: A B Sel Start Up Down Left Right)
BTN_A = $80
BTN_B = $40
BTN_SEL = $20
BTN_SRT = $10
BTN_UP = $08
BTN_DOWN = $04
BTN_LEFT = $02
BTN_RIGHT = $01
; Game states
STATE_PLAY = 0
STATE_WIN = 1
; Player starting position
PLAYER_X0 = 24
PLAYER_Y0 = 24
; Target zone: tile columns 12-15, tile rows 8-11
; pixel rectangle X: 96-127, Y: 64-95
TGT_L = 96
TGT_T = 64
TGT_R = 127
TGT_B = 95
; Collision: 8×8 sprite overlaps target when
; player_x in [TGT_L-7 .. TGT_R] => [89 .. 127]
; player_y in [TGT_T-7 .. TGT_B] => [57 .. 95]
COL_XL = 89
COL_XR = 127
COL_YT = 57
COL_YB = 95
; Screen clamp limits (OAM pixel coords)
SCRL = 0
SCRR = 248 ; 256 - 8 (sprite width)
SCRT = 8 ; first visible scanline (with top border)
SCRB = 224 ; 240 - 8 - 8 (safe bottom)
; Background CHR tile indices (pattern table 0 = PPU $0000-$0FFF)
TILE_BLANK = $00
TILE_TGT = $01
TILE_Y = $02
TILE_O = $03
TILE_U = $04
TILE_W = $05
TILE_I = $06
TILE_N = $07
TILE_EXCL = $08
TILE_P = $09
TILE_R = $0A
TILE_E = $0B
TILE_S = $0C
TILE_T_LTR = $0D
TILE_A = $0E
TILE_G = $0F
TILE_L = $10
; Sprite pattern table is at PPU $1000 (PPU_CTRL bit 3 = 1)
TILE_DIAM = $00 ; diamond sprite tile index
; Nametable / attribute table base
AT0 = $23C0 ; attribute table for nametable 0
; Win-text nametable positions:
; "YOU WIN!" row 18, col 12 => $2000 + 18*32 + 12 = $224C
; "PRESS START" row 20, col 10 => $2000 + 20*32 + 10 = $228A
WIN_HI = $22
WIN_LO = $4C
RST_HI = $22
RST_LO = $8A
; Konami code buffer length
KONAMI_LEN = 8
; APU envelope constants
P1_ENV = $B8 ; Pulse1: 50% duty, length halt, const vol 8
P2_BLIP = $34 ; Pulse2: 12.5% duty, length halt, const vol 4
P2_CHIME = $BC ; Pulse2: 50% duty, length halt, const vol 12
; ===========================================================================
; CODE segment
; ===========================================================================
.segment "CODE"
; ===========================================================================
; RESET — standard NES initialisation
; ===========================================================================
.proc reset_handler
sei ; disable IRQs
cld ; clear decimal mode
ldx #$FF
txs ; stack pointer = $01FF
; disable PPU rendering and NMI
lda #$00
sta PPU_CTRL
sta PPU_MASK
; wait for first vblank
@vbl1:
bit PPU_STATUS
bpl @vbl1
; clear CPU RAM pages $00, $03-$07 (skip stack $01xx, skip OAM $02xx)
lda #$00
ldx #$00
@clr:
sta $0000,x
sta $0300,x
sta $0400,x
sta $0500,x
sta $0600,x
sta $0700,x
inx
bne @clr
; fill OAM shadow ($0200-$02FF) with $FF to hide all sprites
lda #$FF
ldx #$00
@oam:
sta $0200,x
inx
bne @oam
; wait for second vblank (PPU fully warmed up)
@vbl2:
bit PPU_STATUS
bpl @vbl2
; silence APU
lda #$00
sta APU_STATUS ; disable all channels
lda #$40
sta APU_FRAME ; 4-step mode, disable frame IRQ
lda #$30
sta P1_CTRL ; silence pulse 1 ($30 = duty 0, length halt, const vol 0)
sta P2_CTRL ; silence pulse 2 (same value)
jmp game_init
.endproc
; ===========================================================================
; GAME_INIT — set up a fresh game (called once from reset_handler)
; ===========================================================================
.proc game_init
; initialise game-state variables
lda #STATE_PLAY
sta game_state
lda #PLAYER_X0
sta player_x
lda #PLAYER_Y0
sta player_y
lda #$00
sta frame_ready
sta prev_btns
sta curr_btns
sta new_dirs
sta win_text_state
; clear audio state
sta music_idx
sta music_timer
sta music_active
sta sfx_active
sta sfx_idx
sta sfx_timer
; clear easter-egg state
sta code_pos
sta easter_active
sta easter_timer
sta easter_color
ldx #7
@clrbuf:
sta code_buf,x
dex
bpl @clrbuf
; -----------------------------------------------------------------------
; load palettes: write 32 bytes to PPU $3F00
; -----------------------------------------------------------------------
bit PPU_STATUS ; reset PPU address latch
lda #$3F
sta PPU_ADDR
lda #$00
sta PPU_ADDR
ldx #0
@pal:
lda palette_data,x
sta PPU_DATA
inx
cpx #32
bne @pal
; -----------------------------------------------------------------------
; clear nametable 0 + 1 ($2000-$27FF, 2048 bytes) to TILE_BLANK
; -----------------------------------------------------------------------
bit PPU_STATUS
lda #$20
sta PPU_ADDR
lda #$00
sta PPU_ADDR
lda #$00
ldx #$00
ldy #$00
@clrnt:
sta PPU_DATA
inx
bne @clrnt
iny
cpy #8 ; 8 × 256 = 2048 bytes
bne @clrnt
; -----------------------------------------------------------------------
; set entire attribute table to $55 (all tiles use BG palette 1)
; -----------------------------------------------------------------------
bit PPU_STATUS
lda #>AT0
sta PPU_ADDR
lda #<AT0
sta PPU_ADDR
lda #$55
ldx #64
@attr:
sta PPU_DATA
dex
bne @attr
; -----------------------------------------------------------------------
; draw target zone: 4×4 block of TILE_TGT at tile rows 8-11, cols 12-15
; nametable row 8, col 12 => $2000 + 8*32 + 12 = $210C
; row offsets (low bytes of nametable address within page $21):
; row 8 col 12 => lo = $0C
; row 9 col 12 => lo = $2C
; row 10 col 12 => lo = $4C
; row 11 col 12 => lo = $6C
; -----------------------------------------------------------------------
ldx #3 ; loop 4 rows (index 3 down to 0)
@trow:
bit PPU_STATUS
lda #$21
sta PPU_ADDR
lda tgt_row_lo,x
sta PPU_ADDR
lda #TILE_TGT
sta PPU_DATA
sta PPU_DATA
sta PPU_DATA
sta PPU_DATA
dex
bpl @trow
; -----------------------------------------------------------------------
; set up player sprite in OAM shadow ($0200-$0203)
; -----------------------------------------------------------------------
lda #PLAYER_Y0-1 ; OAM Y = sprite top edge - 1
sta $0200
lda #TILE_DIAM
sta $0201 ; tile index
lda #$00
sta $0202 ; attributes: palette 0, no flip
lda #PLAYER_X0
sta $0203 ; X position
; -----------------------------------------------------------------------
; start background music and enable APU
; -----------------------------------------------------------------------
lda #1
sta music_active
lda #$00
sta music_idx
sta music_timer
lda #$03
sta APU_STATUS ; enable pulse 1 + pulse 2
; -----------------------------------------------------------------------
; enable PPU rendering and NMI
; -----------------------------------------------------------------------
lda #$1E
sta PPU_MASK ; show BG + sprites, full screen
; PPU_CTRL: NMI enable | sprite pattern=$1000 | BG pattern=$0000
; bit7=1 (NMI), bit3=1 (sprite PT at $1000) => $88
lda #$88
sta PPU_CTRL
jmp main_loop
.endproc
; Target-zone nametable row low bytes (within nametable hi-page $21)
; Rows 8-11, column 12: addresses $210C, $212C, $214C, $216C
tgt_row_lo:
.byte $6C, $4C, $2C, $0C ; stored in reverse so index 3->0 draws 8->11
; ===========================================================================
; NMI HANDLER — called on every vblank (~60 Hz)
; ===========================================================================
.proc nmi_handler
pha
txa
pha
tya
pha
; --- OAM DMA (sprite data from $0200 to OAM, 513 cycles) ---
lda #$00
sta OAM_ADDR
lda #$02
sta OAM_DMA
; --- nametable text updates (must happen during vblank) ---
lda win_text_state
beq @text_done
cmp #1
bne @clear_text
jsr draw_win_text
lda #0
sta win_text_state
jmp @text_done
@clear_text:
jsr clear_win_text
lda #0
sta win_text_state
@text_done:
; --- easter-egg colour flash ---
lda easter_active
beq @easter_done
; write cycling colour to PPU $3F00 (universal background colour)
bit PPU_STATUS
lda #$3F
sta PPU_ADDR
lda #$00
sta PPU_ADDR
ldx easter_color
lda easter_colors,x
sta PPU_DATA
; advance colour index mod 8
inx
cpx #8
bne @esave
ldx #0
@esave:
stx easter_color
; tick timer
dec easter_timer
bne @easter_done
; timer expired: restore black background, deactivate
bit PPU_STATUS
lda #$3F
sta PPU_ADDR
lda #$00
sta PPU_ADDR
lda #$0F
sta PPU_DATA
lda #0
sta easter_active
@easter_done:
; --- background music update ---
lda music_active
beq @music_done
jsr update_music
@music_done:
; --- SFX update ---
lda sfx_active
beq @sfx_done
jsr update_sfx
@sfx_done:
; --- set scroll to (0,0) ---
bit PPU_STATUS
lda #$00
sta PPU_SCROLL
sta PPU_SCROLL
; --- signal main loop ---
lda #1
sta frame_ready
pla
tay
pla
tax
pla
rti
.endproc
; ===========================================================================
; IRQ HANDLER — unused
; ===========================================================================
.proc irq_handler
rti
.endproc
; ===========================================================================
; MAIN LOOP
; ===========================================================================
.proc main_loop
@top:
; spin until NMI signals a new frame
lda frame_ready
beq @top
lda #0
sta frame_ready
; read controller into curr_btns / prev_btns
jsr read_controller
lda game_state
cmp #STATE_WIN
beq @won
; --- STATE: PLAYING ---
jsr handle_movement
jsr check_collision
jmp @top
; --- STATE: WON ---
@won:
; detect newly-pressed Start button
lda curr_btns
and #BTN_SRT
beq @top ; Start not pressed
lda prev_btns
and #BTN_SRT
bne @top ; Start was already held, not a new press
jsr restart_game
jmp @top
.endproc
; ===========================================================================
; READ_CONTROLLER — strobe $4016, shift 8 bits into curr_btns
; ===========================================================================
.proc read_controller
; save current as previous
lda curr_btns
sta prev_btns
; strobe controller latch
lda #1
sta CTRL1
lda #0
sta CTRL1
; read 8 buttons (A B Sel Start Up Down Left Right)
lda #0
sta curr_btns
ldx #8
@loop:
lda CTRL1
lsr a ; bit 0 → carry
rol curr_btns ; carry into bit 0, everything shifts left
dex
bne @loop
rts
.endproc
; ===========================================================================
; HANDLE_MOVEMENT — move player with d-pad, clamp, track Konami code
; ===========================================================================
.proc handle_movement
; compute newly-pressed directional bits: new = curr & ~prev
lda prev_btns
eor #$FF ; ~prev
and curr_btns
and #(BTN_UP|BTN_DOWN|BTN_LEFT|BTN_RIGHT)
sta new_dirs ; save for Konami tracking and blip trigger
; ----- movement (uses HELD buttons for smooth motion) -----
lda curr_btns
and #BTN_UP
beq @no_up
lda player_y
cmp #SCRT+1 ; stop when player_y <= SCRT (bcc: player_y < SCRT+1)
bcc @no_up
dec player_y
@no_up:
lda curr_btns
and #BTN_DOWN
beq @no_down
lda player_y
cmp #SCRB
bcs @no_down ; already at/past bottom edge
inc player_y
@no_down:
lda curr_btns
and #BTN_LEFT
beq @no_left
lda player_x
beq @no_left ; already at left edge (SCRL=0)
dec player_x
@no_left:
lda curr_btns
and #BTN_RIGHT
beq @no_right
lda player_x
cmp #SCRR
bcs @no_right ; already at/past right edge
inc player_x
@no_right:
; update OAM shadow with new position
lda player_y
sec
sbc #1 ; OAM Y = visible Y - 1
sta $0200
lda player_x
sta $0203
; trigger move-blip SFX on first directional press (not while win chime plays)
lda new_dirs
beq @no_blip
jsr trigger_blip
@no_blip:
; ----- Konami code tracking (newly-pressed directions only) -----
lda new_dirs
beq @done
; encode first direction found (priority: Up > Down > Left > Right)
and #BTN_UP
beq @ck_down
lda #1
jsr push_code
jmp @done
@ck_down:
lda new_dirs
and #BTN_DOWN
beq @ck_left
lda #2
jsr push_code
jmp @done
@ck_left:
lda new_dirs
and #BTN_LEFT
beq @ck_right
lda #3
jsr push_code
jmp @done
@ck_right:
lda new_dirs
and #BTN_RIGHT
beq @done
lda #4
jsr push_code
@done:
rts
.endproc
; ===========================================================================
; PUSH_CODE — add encoded direction (A) to ring buffer, then check sequence
; ===========================================================================
.proc push_code
ldx code_pos
sta code_buf,x
inx
cpx #KONAMI_LEN
bne @ok
ldx #0
@ok:
stx code_pos
jsr check_konami
rts
.endproc
; ===========================================================================
; CHECK_KONAMI — compare ring buffer against konami_seq, activate easter egg
; ===========================================================================
.proc check_konami
; walk the ring starting at code_pos (oldest entry) and compare
ldx code_pos
ldy #0
@cmp:
lda code_buf,x
cmp konami_seq,y
bne @no_match
inx
cpx #KONAMI_LEN
bne @nowrap
ldx #0
@nowrap:
iny
cpy #KONAMI_LEN
bne @cmp
; full sequence matched — activate easter egg (ignore if already active)
lda easter_active
bne @no_match
lda #1
sta easter_active
lda #180
sta easter_timer
lda #0
sta easter_color
@no_match:
rts
.endproc
; ===========================================================================
; TRIGGER_BLIP — queue a short high-pitch tick on Pulse 2
; (skipped if the win chime is currently playing)
; ===========================================================================
.proc trigger_blip
lda sfx_active
cmp #2
beq @skip ; win chime in progress, do not interrupt
lda #P2_BLIP ; 12.5% duty, length halt, vol 4
sta P2_CTRL
lda #$00
sta P2_SWEEP
lda #$50 ; period lo ($0050 ≈ ~1 kHz blip)
sta P2_LO
lda #$00
sta P2_HI
lda #1
sta sfx_active
lda #3
sta sfx_timer
@skip:
rts
.endproc
; ===========================================================================
; CHECK_COLLISION — AABB test of sprite against target zone
; ===========================================================================
.proc check_collision
lda game_state
cmp #STATE_WIN
beq @done ; already won, skip
lda player_x
cmp #COL_XL
bcc @done ; too far left
cmp #COL_XR+1
bcs @done ; too far right
lda player_y
cmp #COL_YT
bcc @done ; too high
cmp #COL_YB+1
bcs @done ; too low
; ----- collision! -----
lda #STATE_WIN
sta game_state
; stop background music
lda #0
sta music_active
lda #$30
sta P1_CTRL ; silence pulse 1
; start win-chime SFX
lda #2
sta sfx_active
lda #0
sta sfx_idx
sta sfx_timer ; update_sfx will load first note next NMI
; request win-text draw (NMI will handle it)
lda #1
sta win_text_state
@done:
rts
.endproc
; ===========================================================================
; RESTART_GAME — reset player and game state for a new round
; ===========================================================================
.proc restart_game
; request nametable clear of win text
lda #2
sta win_text_state
; reset player position
lda #PLAYER_X0
sta player_x
lda #PLAYER_Y0
sta player_y
; update OAM shadow immediately
lda #PLAYER_Y0-1
sta $0200
lda #PLAYER_X0
sta $0203
; go back to playing
lda #STATE_PLAY
sta game_state
; restart music
lda #0
sta music_idx
sta music_timer
lda #1
sta music_active
; clear SFX
lda #0
sta sfx_active
sta sfx_timer
sta sfx_idx
lda #$30
sta P2_CTRL ; silence pulse 2
rts
.endproc
; ===========================================================================
; DRAW_WIN_TEXT — write tile indices to nametable (called inside NMI vblank)
; ===========================================================================
.proc draw_win_text
; "YOU WIN!" at row 18, col 12 (nametable $224C)
bit PPU_STATUS
lda #WIN_HI
sta PPU_ADDR
lda #WIN_LO
sta PPU_ADDR
lda #TILE_Y
sta PPU_DATA
lda #TILE_O
sta PPU_DATA
lda #TILE_U
sta PPU_DATA
lda #TILE_BLANK
sta PPU_DATA
lda #TILE_W
sta PPU_DATA
lda #TILE_I
sta PPU_DATA
lda #TILE_N
sta PPU_DATA
lda #TILE_EXCL
sta PPU_DATA
; "PRESS START" at row 20, col 10 (nametable $228A)
bit PPU_STATUS
lda #RST_HI
sta PPU_ADDR
lda #RST_LO
sta PPU_ADDR
lda #TILE_P
sta PPU_DATA
lda #TILE_R
sta PPU_DATA
lda #TILE_E
sta PPU_DATA
lda #TILE_S
sta PPU_DATA
lda #TILE_S
sta PPU_DATA
lda #TILE_BLANK
sta PPU_DATA
lda #TILE_S
sta PPU_DATA
lda #TILE_T_LTR
sta PPU_DATA
lda #TILE_A
sta PPU_DATA
lda #TILE_R
sta PPU_DATA
lda #TILE_T_LTR
sta PPU_DATA
rts
.endproc
; ===========================================================================
; CLEAR_WIN_TEXT — overwrite win/restart text with blank tiles (inside NMI)
; ===========================================================================
.proc clear_win_text
bit PPU_STATUS
lda #WIN_HI
sta PPU_ADDR
lda #WIN_LO
sta PPU_ADDR
lda #TILE_BLANK
ldx #8
@l1:
sta PPU_DATA
dex
bne @l1
bit PPU_STATUS
lda #RST_HI
sta PPU_ADDR
lda #RST_LO
sta PPU_ADDR
lda #TILE_BLANK
ldx #11
@l2:
sta PPU_DATA
dex
bne @l2
rts
.endproc
; ===========================================================================
; UPDATE_MUSIC — advance Pulse-1 melody one frame (called from NMI)
; ===========================================================================
.proc update_music
lda music_timer
beq @load ; timer expired → load next note
dec music_timer
rts
@load:
ldx music_idx
lda music_data,x ; load byte: note index, $FE=rest, $FF=loop
cmp #$FF
bne @not_loop
; loop marker: reset to beginning
lda #0
sta music_idx
ldx #0
lda music_data,x ; reload first byte (same as music_data[0])
@not_loop:
cmp #$FE
bne @play
; rest: silence pulse 1
lda #$30
sta P1_CTRL
lda music_data+1,x ; duration in frames
sta music_timer
lda music_idx
clc
adc #2
sta music_idx
rts
@play:
; A = note index, X = music_idx
tay
lda note_lo,y
sta P1_LO
lda note_hi,y
sta P1_HI
lda #P1_ENV
sta P1_CTRL
lda #$00
sta P1_SWEEP
lda music_data+1,x ; duration
sta music_timer
lda music_idx
clc
adc #2
sta music_idx
rts
.endproc
; ===========================================================================
; UPDATE_SFX — advance Pulse-2 sound effects one frame (called from NMI)
; ===========================================================================
.proc update_sfx
lda sfx_active
cmp #1
beq @blip
cmp #2
beq @chime
rts
@blip:
; count down the short blip timer
lda sfx_timer
beq @silence
dec sfx_timer
rts
@silence:
lda #$30
sta P2_CTRL
lda #0
sta sfx_active
rts
@chime:
; count down current chime note duration
lda sfx_timer
beq @chime_next
dec sfx_timer
rts
@chime_next:
ldx sfx_idx
lda win_sfx,x ; note index or $FF (end)
cmp #$FF
bne @chime_play
; chime finished
lda #$30
sta P2_CTRL
lda #0
sta sfx_active
rts
@chime_play:
tay
lda note_lo,y
sta P2_LO
lda note_hi,y
sta P2_HI
lda #P2_CHIME
sta P2_CTRL
lda #$00
sta P2_SWEEP
lda win_sfx+1,x ; duration
sta sfx_timer
lda sfx_idx
clc
adc #2
sta sfx_idx
rts
.endproc
; ===========================================================================
; RODATA — read-only constant data
; ===========================================================================
.segment "RODATA"
; ---------------------------------------------------------------------------
; Palette: 32 bytes written to PPU $3F00
; BG palette 1 col1=$28 (yellow) used for target-border tiles
; col2=$30 (white) used for win-text tiles
; Sprite palette 0 col1=$20 (white) for diamond sprite
; ---------------------------------------------------------------------------
palette_data: