-
Notifications
You must be signed in to change notification settings - Fork 42
/
pacman.c
4238 lines (3939 loc) · 176 KB
/
pacman.c
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
/*------------------------------------------------------------------------------
pacman.c
A Pacman clone written in C99 using the sokol headers for platform
abstraction.
The git repository is here:
https://github.com/floooh/pacman.c
A WASM version running in browsers can be found here:
https://floooh.github.io/pacman.c/pacman.html
Some basic concepts and ideas are worth explaining upfront:
The game code structure is a bit "radical" and sometimes goes against
what is considered good practice for medium and large code bases. This is
fine because this is a small game written by a single person. Small
code bases written by small teams allow a different organizational
approach than large code bases written by large teams.
Here are some of those "extremist" methods used in this tiny project:
Instead of artificially splitting the code into many small source files,
everything is in a single source file readable from top to bottom.
Instead of data-encapsulation and -hiding, all data lives in a single,
global, nested data structure (this isn't actually as radical and
experimental as it sounds, I've been using this approach for quite a
while now for all my hobby code). An interesting side effect of this
upfront-defined static memory layout is that there are no dynamic
allocations in the entire game code (only a handful allocations during
initialization of the Sokol headers).
Instead of "wasting" time thinking too much about high-level abstractions
and reusability, the code has been written in a fairly adhoc-manner "from
start to finish", solving problems as they showed up in the most direct
way possible. When parts of the code became too entangled I tried to step
back a bit, take a pause and come back later with a better idea how to
rewrite those parts in a more straightforward manner. Of course
"straightforward" and "readability" are in the eye of the beholder.
The actual gameplay code (Pacman and ghost behaviours) has been
implemented after the "Pacman Dossier" by Jamey Pittman (a PDF copy has
been included in the project), but there are some minor differences to a
Pacman arcade machine emulator, some intended, some not
(https://floooh.github.io/tiny8bit/pacman.html):
- The attract mode animation in the intro screen is missing (where
Pacman is chased by ghosts, eats a pill and then hunts the ghost).
- Likewise, the 'interlude' animation between levels is missing.
- Various difficulty-related differences in later maps are ignored
(such a faster movement speed, smaller dot-counter-limits for ghosts etc)
The rendering and audio code resembles the original Pacman arcade machine
hardware:
- the tile and sprite pixel data, hardware color palette data and
sound wavetable data is taken directly from embedded arcade machine
ROM dumps
- background tiles are rendered from two 28x36 byte buffers (one for
tile-codes, the other for color-codes), this is similar to an actual
arcade machine, with the only difference that the tile- and color-buffer
has a straightforward linear memory layout
- background tile rendering is done with dynamically uploaded vertex
data (two triangles per tile), with color-palette decoding done in
the pixel shader
- up to 8 16x16 sprites are rendered as vertex quads, with the same
color palette decoding happening in the pixel shader as for background
tiles.
- audio output works through an actual Namco WSG emulator which generates
sound samples for 3 hardware voices from a 20-bit frequency counter,
4-bit volume and 3-bit wave type (for 8 wavetables made of 32 sample
values each stored in a ROM dump)
- sound effects are implemented by writing new values to the hardware
voice 'registers' once per 60Hz tick, this can happen in two ways:
- as 'procedural' sound effects, where a callback function computes
the new voice register values
- or via 'register dump' playback, where the voice register values
have been captured at 60Hz frequency from an actual Pacman arcade
emulator
Only two sound effects are register dumps: the little music track at
the start of a game, and the sound effect when Pacman dies. All other
effects are simple procedural effects.
The only concept worth explaining in the gameplay code is how timing
and 'asynchronous actions' work:
The entire gameplay logic is driven by a global 60 Hz game tick which is
counting upward.
Gameplay actions are initiated by a combination of 'time triggers' and a simple
vocabulary to initialize and test trigger conditions. This time trigger system
is an extremely simple replacement for more powerful event systems in
'proper' game engines.
Here are some pseudo-code examples how time triggers can be used (unrelated
to Pacman):
To immediately trigger an action in one place of the code, and 'realize'
this action in one or several other places:
// if a monster has been eaten, trigger the 'monster eaten' action:
if (monster_eaten()) {
start(&state.game.monster_eaten);
}
// ...somewhere else, we might increase the score if a monster has been eaten:
if (now(state.game.monster_eaten)) {
state.game.score += 10;
}
// ...and yet somewhere else in the code, we might want to play a sound effect
if (now(state.game.monster_eaten)) {
// play sound effect...
}
We can also start actions in the future, which allows to batch multiple
followup-actions in one place:
// start fading out now, after one second (60 ticks) start a new
// game round, and fade in, after another second when fadein has
// finished, start the actual game loop
start(&state.gfx.fadeout);
start_after(&state.game.started, 60);
start_after(&state.gfx.fadein, 60);
start_after(&state.game.gameloop_started, 2*60);
As mentioned above, there's a whole little function vocabulary built around
time triggers, but those are hopefully all self-explanatory.
*/
#include "sokol_app.h"
#include "sokol_gfx.h"
#include "sokol_audio.h"
#include "sokol_log.h"
#include "sokol_glue.h"
#include <assert.h>
#include <string.h> // memset()
#include <stdlib.h> // abs()
// config defines and global constants
#define AUDIO_VOLUME (0.5f)
#define DBG_SKIP_INTRO (0) // set to (1) to skip intro
#define DBG_SKIP_PRELUDE (0) // set to (1) to skip game prelude
#define DBG_START_ROUND (0) // set to any starting round <=255
#define DBG_MARKERS (0) // set to (1) to show debug markers
#define DBG_ESCAPE (0) // set to (1) to leave game loop with Esc
#define DBG_DOUBLE_SPEED (0) // set to (1) to speed up game (useful with godmode)
#define DBG_GODMODE (0) // set to (1) to disable dying
// NOTE: DO NOT CHANGE THESE DEFINES TO AN ENUM
// gcc-13 will turn the backing type into an unsigned integer which then
// causes all sorts of trouble further down
// tick duration in nanoseconds
#if DBG_DOUBLE_SPEED
#define TICK_DURATION_NS (8333333)
#else
#define TICK_DURATION_NS (16666666)
#endif
#define TICK_TOLERANCE_NS (1000000) // per-frame tolerance in nanoseconds
#define NUM_VOICES (3) // number of sound voices
#define NUM_SOUNDS (3) // max number of sounds effects that can be active at a time
#define NUM_SAMPLES (128) // max number of audio samples in local sample buffer
#define DISABLED_TICKS (0xFFFFFFFF) // magic tick value for a disabled timer
#define TILE_WIDTH (8) // width and height of a background tile in pixels
#define TILE_HEIGHT (8)
#define SPRITE_WIDTH (16) // width and height of a sprite in pixels
#define SPRITE_HEIGHT (16)
#define DISPLAY_TILES_X (28) // tile buffer width and height
#define DISPLAY_TILES_Y (36)
#define DISPLAY_PIXELS_X (DISPLAY_TILES_X * TILE_WIDTH)
#define DISPLAY_PIXELS_Y (DISPLAY_TILES_Y * TILE_HEIGHT)
#define NUM_SPRITES (8)
#define NUM_DEBUG_MARKERS (16)
#define TILE_TEXTURE_WIDTH (256 * TILE_WIDTH)
#define TILE_TEXTURE_HEIGHT (TILE_HEIGHT + SPRITE_HEIGHT)
#define MAX_VERTICES (((DISPLAY_TILES_X * DISPLAY_TILES_Y) + NUM_SPRITES + NUM_DEBUG_MARKERS) * 6)
#define FADE_TICKS (30) // duration of fade-in/out
#define NUM_LIVES (3)
#define NUM_STATUS_FRUITS (7) // max number of displayed fruits at bottom right
#define NUM_DOTS (244) // 240 small dots + 4 pills
#define NUM_PILLS (4) // number of energizer pills on playfield
#define ANTEPORTAS_X (14*TILE_WIDTH) // pixel position of the ghost house enter/leave point
#define ANTEPORTAS_Y (14*TILE_HEIGHT + TILE_HEIGHT/2)
#define GHOST_EATEN_FREEZE_TICKS (60) // number of ticks the game freezes after Pacman eats a ghost
#define PACMAN_EATEN_TICKS (60) // number of ticks to freeze game when Pacman is eaten
#define PACMAN_DEATH_TICKS (150) // number of ticks to show the Pacman death sequence before starting new round
#define GAMEOVER_TICKS (3*60) // number of ticks the game over message is shown
#define ROUNDWON_TICKS (4*60) // number of ticks to wait after a round was won
#define FRUITACTIVE_TICKS (10*60) // number of ticks a bonus fruit is shown
/* common tile, sprite and color codes, these are the same as on the Pacman
arcade machine and extracted by looking at memory locations of a Pacman emulator
*/
enum {
TILE_SPACE = 0x40,
TILE_DOT = 0x10,
TILE_PILL = 0x14,
TILE_GHOST = 0xB0,
TILE_LIFE = 0x20, // 0x20..0x23
TILE_CHERRIES = 0x90, // 0x90..0x93
TILE_STRAWBERRY = 0x94, // 0x94..0x97
TILE_PEACH = 0x98, // 0x98..0x9B
TILE_BELL = 0x9C, // 0x9C..0x9F
TILE_APPLE = 0xA0, // 0xA0..0xA3
TILE_GRAPES = 0xA4, // 0xA4..0xA7
TILE_GALAXIAN = 0xA8, // 0xA8..0xAB
TILE_KEY = 0xAC, // 0xAC..0xAF
TILE_DOOR = 0xCF, // the ghost-house door
SPRITETILE_INVISIBLE = 30,
SPRITETILE_SCORE_200 = 40,
SPRITETILE_SCORE_400 = 41,
SPRITETILE_SCORE_800 = 42,
SPRITETILE_SCORE_1600 = 43,
SPRITETILE_CHERRIES = 0,
SPRITETILE_STRAWBERRY = 1,
SPRITETILE_PEACH = 2,
SPRITETILE_BELL = 3,
SPRITETILE_APPLE = 4,
SPRITETILE_GRAPES = 5,
SPRITETILE_GALAXIAN = 6,
SPRITETILE_KEY = 7,
SPRITETILE_PACMAN_CLOSED_MOUTH = 48,
COLOR_BLANK = 0x00,
COLOR_DEFAULT = 0x0F,
COLOR_DOT = 0x10,
COLOR_PACMAN = 0x09,
COLOR_BLINKY = 0x01,
COLOR_PINKY = 0x03,
COLOR_INKY = 0x05,
COLOR_CLYDE = 0x07,
COLOR_FRIGHTENED = 0x11,
COLOR_FRIGHTENED_BLINKING = 0x12,
COLOR_GHOST_SCORE = 0x18,
COLOR_EYES = 0x19,
COLOR_CHERRIES = 0x14,
COLOR_STRAWBERRY = 0x0F,
COLOR_PEACH = 0x15,
COLOR_BELL = 0x16,
COLOR_APPLE = 0x14,
COLOR_GRAPES = 0x17,
COLOR_GALAXIAN = 0x09,
COLOR_KEY = 0x16,
COLOR_WHITE_BORDER = 0x1F,
COLOR_FRUIT_SCORE = 0x03,
};
// the top-level game states (intro => game => intro)
typedef enum {
GAMESTATE_INTRO,
GAMESTATE_GAME,
} gamestate_t;
// directions NOTE: bit0==0: horizontal movement, bit0==1: vertical movement
typedef enum {
DIR_RIGHT, // 000
DIR_DOWN, // 001
DIR_LEFT, // 010
DIR_UP, // 011
NUM_DIRS
} dir_t;
// bonus fruit types
typedef enum {
FRUIT_NONE,
FRUIT_CHERRIES,
FRUIT_STRAWBERRY,
FRUIT_PEACH,
FRUIT_APPLE,
FRUIT_GRAPES,
FRUIT_GALAXIAN,
FRUIT_BELL,
FRUIT_KEY,
NUM_FRUITS
} fruit_t;
// sprite 'hardware' indices
typedef enum {
SPRITE_PACMAN,
SPRITE_BLINKY,
SPRITE_PINKY,
SPRITE_INKY,
SPRITE_CLYDE,
SPRITE_FRUIT,
} sprite_index_t;
// ghost types
typedef enum {
GHOSTTYPE_BLINKY,
GHOSTTYPE_PINKY,
GHOSTTYPE_INKY,
GHOSTTYPE_CLYDE,
NUM_GHOSTS
} ghosttype_t;
// ghost AI states
typedef enum {
GHOSTSTATE_NONE,
GHOSTSTATE_CHASE, // currently chasing Pacman
GHOSTSTATE_SCATTER, // currently heading to the corner scatter targets
GHOSTSTATE_FRIGHTENED, // frightened after Pacman has eaten an energizer pill
GHOSTSTATE_EYES, // eaten by Pacman and heading back to the ghost house
GHOSTSTATE_HOUSE, // currently inside the ghost house
GHOSTSTATE_LEAVEHOUSE, // currently leaving the ghost house
GHOSTSTATE_ENTERHOUSE // currently entering the ghost house
} ghoststate_t;
// reasons why game loop is frozen
typedef enum {
FREEZETYPE_PRELUDE = (1<<0), // game prelude is active (with the game start tune playing)
FREEZETYPE_READY = (1<<1), // READY! phase is active (at start of a new game round)
FREEZETYPE_EAT_GHOST = (1<<2), // Pacman has eaten a ghost
FREEZETYPE_DEAD = (1<<3), // Pacman was eaten by a ghost
FREEZETYPE_WON = (1<<4), // game round was won by eating all dots
} freezetype_t;
// a trigger holds a specific game-tick when an action should be started
typedef struct {
uint32_t tick;
} trigger_t;
// a 2D integer vector (used both for pixel- and tile-coordinates)
typedef struct {
int16_t x;
int16_t y;
} int2_t;
// common state for pacman and ghosts
typedef struct {
dir_t dir; // current movement direction
int2_t pos; // position of sprite center in pixel coords
uint32_t anim_tick; // incremented when actor moved in current tick
} actor_t;
// ghost AI state
typedef struct {
actor_t actor;
ghosttype_t type;
dir_t next_dir; // ghost AI looks ahead one tile when deciding movement direction
int2_t target_pos; // current target position in tile coordinates
ghoststate_t state;
trigger_t frightened; // game tick when frightened mode was entered
trigger_t eaten; // game tick when eaten by Pacman
uint16_t dot_counter; // used to decide when to leave the ghost house
uint16_t dot_limit;
} ghost_t;
// pacman state
typedef struct {
actor_t actor;
} pacman_t;
// the tile- and sprite-renderer's vertex structure
typedef struct {
float x, y; // screen coords [0..1]
float u, v; // tile texture coords
uint32_t attr; // x: color code, y: opacity (opacity only used for fade effect)
} vertex_t;
// sprite state
typedef struct {
bool enabled; // if false sprite is deactivated
uint8_t tile, color; // sprite-tile number (0..63), color code
bool flipx, flipy; // horizontal/vertical flip
int2_t pos; // pixel position of the sprite's top-left corner
} sprite_t;
// debug visualization markers (see DBG_MARKERS)
typedef struct {
bool enabled;
uint8_t tile, color; // tile and color code
int2_t tile_pos;
} debugmarker_t;
// callback function prototype for procedural sounds
typedef void (*sound_func_t)(int sound_slot);
// a sound effect description used as param for snd_start()
typedef struct {
sound_func_t func; // callback function (if procedural sound)
const uint32_t* ptr; // pointer to register dump data (if a register-dump sound)
uint32_t size; // byte size of register dump data
bool voice[3]; // true to activate voice
} sound_desc_t;
// a sound 'hardware' voice
typedef struct {
uint32_t counter; // 20-bit counter, top 5 bits are index into wavetable ROM
uint32_t frequency; // 20-bit frequency (added to counter at 96kHz)
uint8_t waveform; // 3-bit waveform index
uint8_t volume; // 4-bit volume
float sample_acc; // current float sample accumulator
float sample_div; // current float sample divisor
} voice_t;
// flags for sound_t.flags
typedef enum {
SOUNDFLAG_VOICE0 = (1<<0),
SOUNDFLAG_VOICE1 = (1<<1),
SOUNDFLAG_VOICE2 = (1<<2),
SOUNDFLAG_ALL_VOICES = (1<<0)|(1<<1)|(1<<2)
} soundflag_t;
// a currently playing sound effect
typedef struct {
uint32_t cur_tick; // current tick counter
sound_func_t func; // optional function pointer for prodecural sounds
uint32_t num_ticks; // length of register dump sound effect in 60Hz ticks
uint32_t stride; // number of uint32_t values per tick (only for register dump effects)
const uint32_t* data; // 3 * num_ticks register dump values
uint8_t flags; // combination of soundflag_t (active voices)
} sound_t;
// all state is in a single nested struct
static struct {
gamestate_t gamestate; // the current gamestate (intro => game => intro)
struct {
uint32_t tick; // the central game tick, this drives the whole game
uint64_t laptime_store; // helper variable to measure frame duration
int32_t tick_accum; // helper variable to decouple ticks from frame rate
} timing;
// intro state
struct {
trigger_t started; // tick when intro-state was started
} intro;
// game state
struct {
uint32_t xorshift; // current xorshift random-number-generator state
uint32_t hiscore; // hiscore / 10
trigger_t started;
trigger_t ready_started;
trigger_t round_started;
trigger_t round_won;
trigger_t game_over;
trigger_t dot_eaten; // last time Pacman ate a dot
trigger_t pill_eaten; // last time Pacman ate a pill
trigger_t ghost_eaten; // last time Pacman ate a ghost
trigger_t pacman_eaten; // last time Pacman was eaten by a ghost
trigger_t fruit_eaten; // last time Pacman has eaten the bonus fruit
trigger_t force_leave_house; // starts when a dot is eaten
trigger_t fruit_active; // starts when bonus fruit is shown
uint8_t freeze; // combination of FREEZETYPE_* flags
uint8_t round; // current game round, 0, 1, 2...
uint32_t score; // score / 10
int8_t num_lives;
uint8_t num_ghosts_eaten; // number of ghosts easten with current pill
uint8_t num_dots_eaten; // if == NUM_DOTS, Pacman wins the round
bool global_dot_counter_active; // set to true when Pacman loses a life
uint8_t global_dot_counter; // the global dot counter for the ghost-house-logic
ghost_t ghost[NUM_GHOSTS];
pacman_t pacman;
fruit_t active_fruit;
} game;
// the current input state
struct {
bool enabled;
bool up;
bool down;
bool left;
bool right;
bool esc; // only for debugging (see DBG_ESCACPE)
bool anykey;
} input;
// the audio subsystem is essentially a Namco arcade board sound emulator
struct {
voice_t voice[NUM_VOICES];
sound_t sound[NUM_SOUNDS];
int32_t voice_tick_accum;
int32_t voice_tick_period;
int32_t sample_duration_ns;
int32_t sample_accum;
uint32_t num_samples;
float sample_buffer[NUM_SAMPLES];
} audio;
// the gfx subsystem implements a simple tile+sprite renderer
struct {
// fade-in/out timers and current value
trigger_t fadein;
trigger_t fadeout;
uint8_t fade;
// the 36x28 tile framebuffer
uint8_t video_ram[DISPLAY_TILES_Y][DISPLAY_TILES_X]; // tile codes
uint8_t color_ram[DISPLAY_TILES_Y][DISPLAY_TILES_X]; // color codes
// up to 8 sprites
sprite_t sprite[NUM_SPRITES];
// up to 16 debug markers
debugmarker_t debug_marker[NUM_DEBUG_MARKERS];
// sokol-gfx resources
sg_pass_action pass_action;
struct {
sg_buffer vbuf;
sg_image tile_img;
sg_image palette_img;
sg_image render_target;
sg_sampler sampler;
sg_pipeline pip;
sg_attachments attachments;
} offscreen;
struct {
sg_buffer quad_vbuf;
sg_pipeline pip;
sg_sampler sampler;
} display;
// intermediate vertex buffer for tile- and sprite-rendering
int num_vertices;
vertex_t vertices[MAX_VERTICES];
// scratch-buffer for tile-decoding (only happens once)
uint8_t tile_pixels[TILE_TEXTURE_HEIGHT][TILE_TEXTURE_WIDTH];
// scratch buffer for the color palette
uint32_t color_palette[256];
} gfx;
} state;
// scatter target positions (in tile coords)
static const int2_t ghost_scatter_targets[NUM_GHOSTS] = {
{ 25, 0 }, { 2, 0 }, { 27, 34 }, { 0, 34 }
};
// starting positions for ghosts (pixel coords)
static const int2_t ghost_starting_pos[NUM_GHOSTS] = {
{ 14*8, 14*8 + 4 },
{ 14*8, 17*8 + 4 },
{ 12*8, 17*8 + 4 },
{ 16*8, 17*8 + 4 },
};
// target positions for ghost entering the ghost house (pixel coords)
static const int2_t ghost_house_target_pos[NUM_GHOSTS] = {
{ 14*8, 17*8 + 4 },
{ 14*8, 17*8 + 4 },
{ 12*8, 17*8 + 4 },
{ 16*8, 17*8 + 4 },
};
// fruit tiles, sprite tiles and colors
static const uint8_t fruit_tiles_colors[NUM_FRUITS][3] = {
{ 0, 0, 0 }, // FRUIT_NONE
{ TILE_CHERRIES, SPRITETILE_CHERRIES, COLOR_CHERRIES },
{ TILE_STRAWBERRY, SPRITETILE_STRAWBERRY, COLOR_STRAWBERRY },
{ TILE_PEACH, SPRITETILE_PEACH, COLOR_PEACH },
{ TILE_APPLE, SPRITETILE_APPLE, COLOR_APPLE },
{ TILE_GRAPES, SPRITETILE_GRAPES, COLOR_GRAPES },
{ TILE_GALAXIAN, SPRITETILE_GALAXIAN, COLOR_GALAXIAN },
{ TILE_BELL, SPRITETILE_BELL, COLOR_BELL },
{ TILE_KEY, SPRITETILE_KEY, COLOR_KEY }
};
// the tiles for displaying the bonus-fruit-score, this is a number built from 4 tiles
static const uint8_t fruit_score_tiles[NUM_FRUITS][4] = {
{ 0x40, 0x40, 0x40, 0x40 }, // FRUIT_NONE
{ 0x40, 0x81, 0x85, 0x40 }, // FRUIT_CHERRIES: 100
{ 0x40, 0x82, 0x85, 0x40 }, // FRUIT_STRAWBERRY: 300
{ 0x40, 0x83, 0x85, 0x40 }, // FRUIT_PEACH: 500
{ 0x40, 0x84, 0x85, 0x40 }, // FRUIT_APPLE: 700
{ 0x40, 0x86, 0x8D, 0x8E }, // FRUIT_GRAPES: 1000
{ 0x87, 0x88, 0x8D, 0x8E }, // FRUIT_GALAXIAN: 2000
{ 0x89, 0x8A, 0x8D, 0x8E }, // FRUIT_BELL: 3000
{ 0x8B, 0x8C, 0x8D, 0x8E }, // FRUIT_KEY: 5000
};
// level specifications (see pacman_dossier.pdf)
typedef struct {
fruit_t bonus_fruit;
uint16_t bonus_score;
uint16_t fright_ticks;
// FIXME: the various Pacman and ghost speeds
} levelspec_t;
enum {
MAX_LEVELSPEC = 21,
};
static const levelspec_t levelspec_table[MAX_LEVELSPEC] = {
{ FRUIT_CHERRIES, 10, 6*60, },
{ FRUIT_STRAWBERRY, 30, 5*60, },
{ FRUIT_PEACH, 50, 4*60, },
{ FRUIT_PEACH, 50, 3*60, },
{ FRUIT_APPLE, 70, 2*60, },
{ FRUIT_APPLE, 70, 5*60, },
{ FRUIT_GRAPES, 100, 2*60, },
{ FRUIT_GRAPES, 100, 2*60, },
{ FRUIT_GALAXIAN, 200, 1*60, },
{ FRUIT_GALAXIAN, 200, 5*60, },
{ FRUIT_BELL, 300, 2*60, },
{ FRUIT_BELL, 300, 1*60, },
{ FRUIT_KEY, 500, 1*60, },
{ FRUIT_KEY, 500, 3*60, },
{ FRUIT_KEY, 500, 1*60, },
{ FRUIT_KEY, 500, 1*60, },
{ FRUIT_KEY, 500, 1, },
{ FRUIT_KEY, 500, 1*60, },
{ FRUIT_KEY, 500, 1, },
{ FRUIT_KEY, 500, 1, },
{ FRUIT_KEY, 500, 1, },
// from here on repeating
};
// forward-declared sound-effect register dumps (recorded from Pacman arcade emulator)
static const uint32_t snd_dump_prelude[490];
static const uint32_t snd_dump_dead[90];
// procedural sound effect callbacks
static void snd_func_eatdot1(int slot);
static void snd_func_eatdot2(int slot);
static void snd_func_eatghost(int slot);
static void snd_func_eatfruit(int slot);
static void snd_func_weeooh(int slot);
static void snd_func_frightened(int slot);
// sound effect description structs
static const sound_desc_t snd_prelude = {
.ptr = snd_dump_prelude,
.size = sizeof(snd_dump_prelude),
.voice = { true, true, false }
};
static const sound_desc_t snd_dead = {
.ptr = snd_dump_dead,
.size = sizeof(snd_dump_dead),
.voice = { false, false, true }
};
static const sound_desc_t snd_eatdot1 = {
.func = snd_func_eatdot1,
.voice = { false, false, true }
};
static const sound_desc_t snd_eatdot2 = {
.func = snd_func_eatdot2,
.voice = { false, false, true }
};
static const sound_desc_t snd_eatghost = {
.func = snd_func_eatghost,
.voice = { false, false, true }
};
static const sound_desc_t snd_eatfruit = {
.func = snd_func_eatfruit,
.voice = { false, false, true }
};
static const sound_desc_t snd_weeooh = {
.func = snd_func_weeooh,
.voice = { false, true, false }
};
static const sound_desc_t snd_frightened = {
.func = snd_func_frightened,
.voice = { false, true, false }
};
// forward declarations
static void init(void);
static void frame(void);
static void cleanup(void);
static void input(const sapp_event*);
static void start(trigger_t* t);
static bool now(trigger_t t);
static void intro_tick(void);
static void game_tick(void);
static void input_enable(void);
static void input_disable(void);
static void gfx_init(void);
static void gfx_shutdown(void);
static void gfx_fade(void);
static void gfx_draw(void);
static void snd_init(void);
static void snd_shutdown(void);
static void snd_tick(void); // called per game tick
static void snd_frame(int32_t frame_time_ns); // called per frame
static void snd_clear(void);
static void snd_start(int sound_slot, const sound_desc_t* snd);
static void snd_stop(int sound_slot);
// forward-declared ROM dumps
static const uint8_t rom_tiles[4096];
static const uint8_t rom_sprites[4096];
static const uint8_t rom_hwcolors[32];
static const uint8_t rom_palette[256];
static const uint8_t rom_wavetable[256];
/*== APPLICATION ENTRY AND CALLBACKS =========================================*/
sapp_desc sokol_main(int argc, char* argv[]) {
(void)argc; (void)argv;
return (sapp_desc) {
.init_cb = init,
.frame_cb = frame,
.cleanup_cb = cleanup,
.event_cb = input,
.width = DISPLAY_TILES_X * TILE_WIDTH * 2,
.height = DISPLAY_TILES_Y * TILE_HEIGHT * 2,
.window_title = "pacman.c",
.logger.func = slog_func,
};
}
static void init(void) {
gfx_init();
snd_init();
// start into intro screen
#if DBG_SKIP_INTRO
start(&state.game.started);
#else
start(&state.intro.started);
#endif
}
static void frame(void) {
// run the game at a fixed tick rate regardless of frame rate
uint32_t frame_time_ns = (uint32_t) (sapp_frame_duration() * 1000000000.0);
// clamp max frame time (so the timing isn't messed up when stopping in the debugger)
if (frame_time_ns > 33333333) {
frame_time_ns = 33333333;
}
state.timing.tick_accum += frame_time_ns;
while (state.timing.tick_accum > -TICK_TOLERANCE_NS) {
state.timing.tick_accum -= TICK_DURATION_NS;
state.timing.tick++;
// call per-tick sound function (updates sound 'registers' with current sound effect values)
snd_tick();
// check for game state change
if (now(state.intro.started)) {
state.gamestate = GAMESTATE_INTRO;
}
if (now(state.game.started)) {
state.gamestate = GAMESTATE_GAME;
}
// call the top-level game state update function
switch (state.gamestate) {
case GAMESTATE_INTRO:
intro_tick();
break;
case GAMESTATE_GAME:
game_tick();
break;
}
}
gfx_draw();
snd_frame(frame_time_ns);
}
static void input(const sapp_event* ev) {
if (state.input.enabled) {
if ((ev->type == SAPP_EVENTTYPE_KEY_DOWN) || (ev->type == SAPP_EVENTTYPE_KEY_UP)) {
bool btn_down = ev->type == SAPP_EVENTTYPE_KEY_DOWN;
switch (ev->key_code) {
case SAPP_KEYCODE_UP:
case SAPP_KEYCODE_W:
state.input.up = state.input.anykey = btn_down;
break;
case SAPP_KEYCODE_DOWN:
case SAPP_KEYCODE_S:
state.input.down = state.input.anykey = btn_down;
break;
case SAPP_KEYCODE_LEFT:
case SAPP_KEYCODE_A:
state.input.left = state.input.anykey = btn_down;
break;
case SAPP_KEYCODE_RIGHT:
case SAPP_KEYCODE_D:
state.input.right = state.input.anykey = btn_down;
break;
case SAPP_KEYCODE_ESCAPE:
state.input.esc = state.input.anykey = btn_down;
break;
default:
state.input.anykey = btn_down;
break;
}
}
}
}
static void cleanup(void) {
snd_shutdown();
gfx_shutdown();
}
/*== GRAB BAG OF HELPER FUNCTIONS ============================================*/
// xorshift random number generator
static uint32_t xorshift32(void) {
uint32_t x = state.game.xorshift;
x ^= x<<13;
x ^= x>>17;
x ^= x<<5;
return state.game.xorshift = x;
}
// get level spec for a game round
static levelspec_t levelspec(int round) {
assert(round >= 0);
if (round >= MAX_LEVELSPEC) {
round = MAX_LEVELSPEC-1;
}
return levelspec_table[round];
}
// set time trigger to the next game tick
static void start(trigger_t* t) {
t->tick = state.timing.tick + 1;
}
// set time trigger to a future tick
static void start_after(trigger_t* t, uint32_t ticks) {
t->tick = state.timing.tick + ticks;
}
// deactivate a time trigger
static void disable(trigger_t* t) {
t->tick = DISABLED_TICKS;
}
// return a disabled time trigger
static trigger_t disabled_timer(void) {
return (trigger_t) { .tick = DISABLED_TICKS };
}
// check if a time trigger is triggered
static bool now(trigger_t t) {
return t.tick == state.timing.tick;
}
// return the number of ticks since a time trigger was triggered
static uint32_t since(trigger_t t) {
if (state.timing.tick >= t.tick) {
return state.timing.tick - t.tick;
}
else {
return DISABLED_TICKS;
}
}
// check if a time trigger is between begin and end tick
static bool between(trigger_t t, uint32_t begin, uint32_t end) {
assert(begin < end);
if (t.tick != DISABLED_TICKS) {
uint32_t ticks = since(t);
return (ticks >= begin) && (ticks < end);
}
else {
return false;
}
}
// check if a time trigger was triggered exactly N ticks ago
static bool after_once(trigger_t t, uint32_t ticks) {
return since(t) == ticks;
}
// check if a time trigger was triggered more than N ticks ago
static bool after(trigger_t t, uint32_t ticks) {
uint32_t s = since(t);
if (s != DISABLED_TICKS) {
return s >= ticks;
}
else {
return false;
}
}
// same as between(t, 0, ticks)
static bool before(trigger_t t, uint32_t ticks) {
uint32_t s = since(t);
if (s != DISABLED_TICKS) {
return s < ticks;
}
else {
return false;
}
}
// clear input state and disable input
static void input_disable(void) {
memset(&state.input, 0, sizeof(state.input));
}
// enable input again
static void input_enable(void) {
state.input.enabled = true;
}
// get the current input as dir_t
static dir_t input_dir(dir_t default_dir) {
if (state.input.up) {
return DIR_UP;
}
else if (state.input.down) {
return DIR_DOWN;
}
else if (state.input.right) {
return DIR_RIGHT;
}
else if (state.input.left) {
return DIR_LEFT;
}
else {
return default_dir;
}
}
// shortcut to create an int2_t
static int2_t i2(int16_t x, int16_t y) {
return (int2_t) { x, y };
}
// add two int2_t
static int2_t add_i2(int2_t v0, int2_t v1) {
return (int2_t) { v0.x+v1.x, v0.y+v1.y };
}
// subtract two int2_t
static int2_t sub_i2(int2_t v0, int2_t v1) {
return (int2_t) { v0.x-v1.x, v0.y-v1.y };
}
// multiply int2_t with scalar
static int2_t mul_i2(int2_t v, int16_t s) {
return (int2_t) { v.x*s, v.y*s };
}
// squared-distance between two int2_t
static int32_t squared_distance_i2(int2_t v0, int2_t v1) {
int2_t d = { v1.x - v0.x, v1.y - v0.y };
return d.x * d.x + d.y * d.y;
}
// check if two int2_t are equal
static bool equal_i2(int2_t v0, int2_t v1) {
return (v0.x == v1.x) && (v0.y == v1.y);
}
// check if two int2_t are nearly equal
static bool nearequal_i2(int2_t v0, int2_t v1, int16_t tolerance) {
return (abs(v1.x - v0.x) <= tolerance) && (abs(v1.y - v0.y) <= tolerance);
}
// convert an actor pos (origin at center) to sprite pos (origin top left)
static int2_t actor_to_sprite_pos(int2_t pos) {
return i2(pos.x - SPRITE_WIDTH/2, pos.y - SPRITE_HEIGHT/2);
}
// compute the distance of a pixel coordinate to the next tile midpoint
int2_t dist_to_tile_mid(int2_t pos) {
return i2((TILE_WIDTH/2) - pos.x % TILE_WIDTH, (TILE_HEIGHT/2) - pos.y % TILE_HEIGHT);
}
// clear tile and color buffer
static void vid_clear(uint8_t tile_code, uint8_t color_code) {
memset(&state.gfx.video_ram, tile_code, sizeof(state.gfx.video_ram));
memset(&state.gfx.color_ram, color_code, sizeof(state.gfx.color_ram));
}
// clear the playfield's rectangle in the color buffer
static void vid_color_playfield(uint8_t color_code) {
for (int y = 3; y < DISPLAY_TILES_Y-2; y++) {
for (int x = 0; x < DISPLAY_TILES_X; x++) {
state.gfx.color_ram[y][x] = color_code;
}
}
}
// check if a tile position is valid
static bool valid_tile_pos(int2_t tile_pos) {
return ((tile_pos.x >= 0) && (tile_pos.x < DISPLAY_TILES_X) && (tile_pos.y >= 0) && (tile_pos.y < DISPLAY_TILES_Y));
}
// put a color into the color buffer
static void vid_color(int2_t tile_pos, uint8_t color_code) {
assert(valid_tile_pos(tile_pos));
state.gfx.color_ram[tile_pos.y][tile_pos.x] = color_code;
}