├── .gitignore ├── menu.lst ├── screenshot.png ├── stage2_eltorito ├── linker.ld ├── entry.asm ├── config.h ├── Makefile ├── README.md └── tetris.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.elf 3 | *.iso 4 | iso 5 | -------------------------------------------------------------------------------- /menu.lst: -------------------------------------------------------------------------------- 1 | default 0 2 | timeout 0 3 | title TETRIS 4 | kernel /boot/tetris.elf 5 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programble/bare-metal-tetris/HEAD/screenshot.png -------------------------------------------------------------------------------- /stage2_eltorito: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programble/bare-metal-tetris/HEAD/stage2_eltorito -------------------------------------------------------------------------------- /linker.ld: -------------------------------------------------------------------------------- 1 | ENTRY (loader) 2 | SECTIONS 3 | { 4 | . = 0x00100000; 5 | .mbheader : { 6 | *(.mbheader) 7 | } 8 | .text : { 9 | *(.text) 10 | } 11 | .rodata ALIGN (0x1000) : { 12 | *(.rodata) 13 | } 14 | .data ALIGN (0x1000) : { 15 | *(.data) 16 | } 17 | .bss : { 18 | sbss = .; 19 | *(COMMON) 20 | *(.bss) 21 | ebss = .; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /entry.asm: -------------------------------------------------------------------------------- 1 | global loader 2 | global stack_ptr 3 | 4 | extern main 5 | 6 | MODULEALIGN equ 1<<0 7 | MEMINFO equ 1<<1 8 | FLAGS equ MODULEALIGN | MEMINFO 9 | MAGIC equ 0x1BADB002 10 | CHECKSUM equ -(MAGIC + FLAGS) 11 | 12 | section .mbheader 13 | align 4 14 | MultiBootHeader: 15 | dd MAGIC 16 | dd FLAGS 17 | dd CHECKSUM 18 | 19 | section .text 20 | 21 | STACKSIZE equ 0x4000 22 | 23 | loader: 24 | mov esp, stack+STACKSIZE 25 | push eax 26 | push ebx 27 | 28 | call main 29 | 30 | cli 31 | 32 | hang: 33 | hlt 34 | jmp hang 35 | 36 | section .bss 37 | align 4 38 | stack: 39 | resb STACKSIZE 40 | stack_ptr: 41 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | /* About data, shown on boot and when paused */ 2 | #define TETRIS_NAME "Bare Metal Tetris" 3 | #define TETRIS_VERSION "1.0.0" 4 | #define TETRIS_URL "https://github.com/programble/bare-metal-tetris" 5 | 6 | /* Tetris well dimensions */ 7 | #define WELL_WIDTH (10) 8 | #define WELL_HEIGHT (22) 9 | 10 | /* Initial interval in milliseconds at which to apply gravity */ 11 | #define INITIAL_SPEED (1000) 12 | 13 | /* Delay in milliseconds before rows are cleared */ 14 | #define CLEAR_DELAY (100) 15 | 16 | /* Scoring: score is increased by the product of the current level and a factor 17 | * corresponding to the number of rows cleared. */ 18 | #define SCORE_FACTOR_1 (100) 19 | #define SCORE_FACTOR_2 (300) 20 | #define SCORE_FACTOR_3 (500) 21 | #define SCORE_FACTOR_4 (800) 22 | 23 | /* Amount to increase the score for a soft drop */ 24 | #define SOFT_DROP_SCORE (1) 25 | 26 | /* Factor by which to multiply the number of rows dropped to increase the score 27 | * for a hard drop */ 28 | #define HARD_DROP_SCORE_FACTOR (2) 29 | 30 | /* Number of rows that need to be cleared to increase level */ 31 | #define ROWS_PER_LEVEL (10) 32 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Binaries to use 2 | TARGET = i386-elf 3 | CC = $(TARGET)-gcc 4 | ASM = nasm 5 | LD = $(TARGET)-ld 6 | 7 | # Compile and link flags 8 | CWARNS = -Wall -Wextra -Wunreachable-code -Wcast-qual -Wcast-align -Wswitch-enum -Wmissing-noreturn -Wwrite-strings -Wundef -Wpacked -Wredundant-decls -Winline -Wdisabled-optimization 9 | CFLAGS = -nostdinc -ffreestanding -fno-builtin -Os $(CWARNS) 10 | AFLAGS = -f elf 11 | LFLAGS = -nostdlib -T linker.ld 12 | 13 | # Binary build 14 | 15 | tetris.elf: entry.o tetris.o 16 | $(LD) $(LFLAGS) $^ -o $@ 17 | 18 | entry.o: entry.asm 19 | $(ASM) $(AFLAGS) $< -o $@ 20 | 21 | tetris.o: tetris.c config.h 22 | $(CC) $(CFLAGS) $< -c -o $@ 23 | 24 | # ISO build 25 | 26 | GENISOIMAGE = genisoimage 27 | GENISOFLAGS = -R -b boot/grub/stage2_eltorito -no-emul-boot -boot-load-size 4 -boot-info-table 28 | STAGE2 = stage2_eltorito 29 | 30 | tetris.iso: iso/boot/tetris.elf iso/boot/grub/stage2_eltorito iso/boot/grub/menu.lst 31 | $(GENISOIMAGE) $(GENISOFLAGS) -o $@ iso 32 | 33 | iso/boot/tetris.elf: tetris.elf 34 | @mkdir -p iso/boot 35 | cp $< $@ 36 | 37 | iso/boot/grub/stage2_eltorito: $(STAGE2) 38 | @mkdir -p iso/boot/grub 39 | cp $< $@ 40 | 41 | iso/boot/grub/menu.lst: menu.lst 42 | @mkdir -p iso/boot/grub 43 | cp $< $@ 44 | 45 | # QEMU launchers 46 | 47 | QEMU = qemu-system-i386 48 | QFLAGS = -soundhw pcspk 49 | 50 | qemu: tetris.elf 51 | $(QEMU) $(QFLAGS) -kernel $< 52 | 53 | qemu-iso: tetris.iso 54 | $(QEMU) $(QFLAGS) -cdrom $< 55 | 56 | 57 | clean: 58 | rm -rf tetris.elf entry.o tetris.o iso tetris.iso 59 | 60 | .PHONY: qemu qemu-iso clean 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bare Metal Tetris 2 | 3 | Tetris for x86. 4 | 5 | There is an improved version, in NASM, called [Tetrasm][tetrasm]. 6 | 7 | [tetrasm]: https://github.com/programble/tetrasm 8 | 9 | ![QEMU screenshot](https://raw.githubusercontent.com/programble/bare-metal-tetris/master/screenshot.png) 10 | 11 | ## Features 12 | 13 | - Color text display 14 | - PC speaker sound effects 15 | - Help screen 16 | - Tetrimino preview 17 | - Tetrimino ghost 18 | - Soft & hard drop 19 | - Pause 20 | - Score and levels 21 | - Increasing difficulty 22 | - Tetrimino statistics 23 | 24 | ## Technical Overview 25 | 26 | Bare Metal Tetris does not use processor interrupts, since initializing 27 | and using them seemed too complex for a single-file Tetris game. 28 | Instead, it uses a combination of an infinite loop, CPU tick counting, 29 | and the real-time clock (RTC) to achieve timing. 30 | 31 | The timing calibration function is called on every iteration of the main 32 | loop. This function checks the RTC to determine if a second has passed 33 | since the last calibration. If one has, the number of CPU ticks since 34 | boot is retrieved using the `rdtsc` instruction, and the number of ticks 35 | elapsed in the last second is calculated. This is then divided and set 36 | as the `tpms`, or "ticks per millisecond" value. Timing functions then 37 | use this value, along with the same `rdtsc` instruction, to determine if 38 | a number of milliseconds have elapsed since a previous call. These 39 | timing values can be seen in the debug screen, toggled using `D`. 40 | 41 | In order to properly calibrate the `tpms` before the game starts, the 42 | title screen is shown for at least one second. That is, the RTC second 43 | value must change twice before the timing is properly calibrated and the 44 | game can start. 45 | 46 | The main loop also checks for keyboard input by polling on each 47 | iteration, and takes care of updating game state and redrawing the 48 | screen if any state has changed. 49 | 50 | ## Building 51 | 52 | ``` 53 | make 54 | ``` 55 | 56 | Requires the NASM assembler, a C compiler and a linker. 57 | 58 | The build tries to use an `i386-elf` target GCC cross-compiler by 59 | default. To change the target tuple, pass a `TARGET` value to `make`. On 60 | x86 or x86_64 systems that already target ELF, such as Linux, the system 61 | compiler can be used by passing `CC=gcc LD=ld` to `make`. 62 | 63 | The build output is a multiboot ELF file `tetris.elf`. 64 | 65 | ## Binaries 66 | 67 | Binary ELF files and ISO images can be found on the 68 | [releases](https://github.com/programble/bare-metal-tetris/releases) 69 | page. 70 | 71 | ## Running 72 | 73 | ### QEMU 74 | 75 | ``` 76 | make qemu 77 | ``` 78 | 79 | The multiboot ELF file can be booted directly by the QEMU emulator. 80 | 81 | ### ISO 82 | 83 | ``` 84 | make tetris.iso 85 | ``` 86 | 87 | A bootable ISO can be created using GRUB's `stage2_eltorito` (included 88 | in this repository) to boot the multiboot ELF file. 89 | 90 | `genisoimage` is used to crate the ISO file. On systems without 91 | `genisoimage`, `mkisofs` from the `cdrtools` package can be used instead 92 | by passing `GENISOIMAGE=mkisofs` to `make`. 93 | 94 | The resulting `tetris.iso` file can then be booted with the QEMU 95 | emulator using `make qemu-iso`, attached to a virtual machine as a CD 96 | drive, or burned to a CD and booted on real hardware. 97 | 98 | ## License 99 | 100 | Copyright © 2013–2014, Curtis McEnroe 101 | 102 | Permission to use, copy, modify, and/or distribute this software for any 103 | purpose with or without fee is hereby granted, provided that the above 104 | copyright notice and this permission notice appear in all copies. 105 | 106 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 107 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 108 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 109 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 110 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 111 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 112 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 113 | -------------------------------------------------------------------------------- /tetris.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | 3 | typedef unsigned char u8; 4 | typedef signed char s8; 5 | typedef unsigned short u16; 6 | typedef signed short s16; 7 | typedef unsigned int u32; 8 | typedef signed int s32; 9 | typedef unsigned long long u64; 10 | typedef signed long long s64; 11 | 12 | #define noreturn __attribute__((noreturn)) void 13 | 14 | typedef enum bool { 15 | false, 16 | true 17 | } bool; 18 | 19 | /* Simple math */ 20 | 21 | /* A very simple and stupid exponentiation algorithm */ 22 | static inline double pow(double a, double b) 23 | { 24 | double result = 1; 25 | while (b-- > 0) 26 | result *= a; 27 | return result; 28 | } 29 | 30 | /* Port I/O */ 31 | 32 | static inline u8 inb(u16 p) 33 | { 34 | u8 r; 35 | asm("inb %1, %0" : "=a" (r) : "dN" (p)); 36 | return r; 37 | } 38 | 39 | static inline void outb(u16 p, u8 d) 40 | { 41 | asm("outb %1, %0" : : "dN" (p), "a" (d)); 42 | } 43 | 44 | /* Divide by zero (in a loop to satisfy the noreturn attribute) in order to 45 | * trigger a division by zero ISR, which is unhandled and causes a hard reset. 46 | */ 47 | noreturn reset(void) 48 | { 49 | volatile u8 one = 1, zero = 0; 50 | while (true) 51 | one /= zero; 52 | } 53 | 54 | /* Timing */ 55 | 56 | /* Return the number of CPU ticks since boot. */ 57 | static inline u64 rdtsc(void) 58 | { 59 | u32 hi, lo; 60 | asm("rdtsc" : "=a" (lo), "=d" (hi)); 61 | return ((u64) lo) | (((u64) hi) << 32); 62 | } 63 | 64 | /* Return the current second field of the real-time-clock (RTC). Note that the 65 | * value may or may not be represented in such a way that it should be 66 | * formatted in hex to display the current second (i.e. 0x30 for the 30th 67 | * second). */ 68 | u8 rtcs(void) 69 | { 70 | u8 last = 0, sec; 71 | do { /* until value is the same twice in a row */ 72 | /* wait for update not in progress */ 73 | do { outb(0x70, 0x0A); } while (inb(0x71) & 0x80); 74 | outb(0x70, 0x00); 75 | sec = inb(0x71); 76 | } while (sec != last && (last = sec)); 77 | return sec; 78 | } 79 | 80 | /* The number of CPU ticks per millisecond */ 81 | u64 tpms; 82 | 83 | /* Set tpms to the number of CPU ticks per millisecond based on the number of 84 | * ticks in the last second, if the RTC second has changed since the last call. 85 | * This gets called on every iteration of the main loop in order to provide 86 | * accurate timing. */ 87 | void tps(void) 88 | { 89 | static u64 ti = 0; 90 | static u8 last_sec = 0xFF; 91 | u8 sec = rtcs(); 92 | if (sec != last_sec) { 93 | last_sec = sec; 94 | u64 tf = rdtsc(); 95 | tpms = (u32) ((tf - ti) >> 3) / 125; /* Less chance of truncation */ 96 | ti = tf; 97 | } 98 | } 99 | 100 | /* IDs used to keep separate timing operations separate */ 101 | enum timer { 102 | TIMER_UPDATE, 103 | TIMER_CLEAR, 104 | TIMER__LENGTH 105 | }; 106 | 107 | u64 timers[TIMER__LENGTH] = {0}; 108 | 109 | /* Return true if at least ms milliseconds have elapsed since the last call 110 | * that returned true for this timer. When called on each iteration of the main 111 | * loop, has the effect of returning true once every ms milliseconds. */ 112 | bool interval(enum timer timer, u32 ms) 113 | { 114 | u64 tf = rdtsc(); 115 | if (tf - timers[timer] >= tpms * ms) { 116 | timers[timer] = tf; 117 | return true; 118 | } else return false; 119 | } 120 | 121 | /* Return true if at least ms milliseconds have elapsed since the first call 122 | * for this timer and reset the timer. */ 123 | bool wait(enum timer timer, u32 ms) 124 | { 125 | if (timers[timer]) { 126 | if (rdtsc() - timers[timer] >= tpms * ms) { 127 | timers[timer] = 0; 128 | return true; 129 | } else return false; 130 | } else { 131 | timers[timer] = rdtsc(); 132 | return false; 133 | } 134 | } 135 | 136 | /* Video Output */ 137 | 138 | /* Seven possible display colors. Bright variations can be used by bitwise OR 139 | * with BRIGHT (i.e. BRIGHT | BLUE). */ 140 | enum color { 141 | BLACK, 142 | BLUE, 143 | GREEN, 144 | CYAN, 145 | RED, 146 | MAGENTA, 147 | YELLOW, 148 | GRAY, 149 | BRIGHT 150 | }; 151 | 152 | #define COLS (80) 153 | #define ROWS (25) 154 | u16 *const video = (u16*) 0xB8000; 155 | 156 | /* Display a character at x, y in fg foreground color and bg background color. 157 | */ 158 | void putc(u8 x, u8 y, enum color fg, enum color bg, char c) 159 | { 160 | u16 z = (bg << 12) | (fg << 8) | c; 161 | video[y * COLS + x] = z; 162 | } 163 | 164 | /* Display a string starting at x, y in fg foreground color and bg background 165 | * color. Characters in the string are not interpreted (e.g \n, \b, \t, etc.). 166 | * */ 167 | void puts(u8 x, u8 y, enum color fg, enum color bg, const char *s) 168 | { 169 | for (; *s; s++, x++) 170 | putc(x, y, fg, bg, *s); 171 | } 172 | 173 | /* Clear the screen to bg backround color. */ 174 | void clear(enum color bg) 175 | { 176 | u8 x, y; 177 | for (y = 0; y < ROWS; y++) 178 | for (x = 0; x < COLS; x++) 179 | putc(x, y, bg, bg, ' '); 180 | } 181 | 182 | /* Keyboard Input */ 183 | 184 | #define KEY_D (0x20) 185 | #define KEY_H (0x23) 186 | #define KEY_P (0x19) 187 | #define KEY_R (0x13) 188 | #define KEY_S (0x1F) 189 | #define KEY_UP (0x48) 190 | #define KEY_DOWN (0x50) 191 | #define KEY_LEFT (0x4B) 192 | #define KEY_RIGHT (0x4D) 193 | #define KEY_ENTER (0x1C) 194 | #define KEY_SPACE (0x39) 195 | 196 | /* Return the scancode of the current up or down key if it has changed since 197 | * the last call, otherwise returns 0. When called on every iteration of the 198 | * main loop, returns non-zero on a key event. */ 199 | u8 scan(void) 200 | { 201 | static u8 key = 0; 202 | u8 scan = inb(0x60); 203 | if (scan != key) 204 | return key = scan; 205 | else return 0; 206 | } 207 | 208 | /* PC Speaker */ 209 | 210 | /* Set the frequency of the PC speaker through timer 2 of the programmable 211 | * interrupt timer (PIT). */ 212 | void pcspk_freq(u32 hz) 213 | { 214 | u32 div = 1193180 / hz; 215 | outb(0x43, 0xB6); 216 | outb(0x42, (u8) div); 217 | outb(0x42, (u8) (div >> 8)); 218 | } 219 | 220 | /* Enable timer 2 of the PIT to drive the PC speaker. */ 221 | void pcspk_on(void) 222 | { 223 | outb(0x61, inb(0x61) | 0x3); 224 | } 225 | 226 | /* Disable timer 2 of the PIT to drive the PC speaker. */ 227 | void pcspk_off(void) 228 | { 229 | outb(0x61, inb(0x61) & 0xFC); 230 | } 231 | 232 | /* Formatting */ 233 | 234 | /* Format n in radix r (2-16) as a w length string. */ 235 | char *itoa(u32 n, u8 r, u8 w) 236 | { 237 | static const char d[16] = "0123456789ABCDEF"; 238 | static char s[34]; 239 | s[33] = 0; 240 | u8 i = 33; 241 | do { 242 | i--; 243 | s[i] = d[n % r]; 244 | n /= r; 245 | } while (i > 33 - w); 246 | return (char *) (s + i); 247 | } 248 | 249 | /* Random */ 250 | 251 | /* Generate a random number from 0 inclusive to range exclusive from the number 252 | * of CPU ticks since boot. */ 253 | u32 rand(u32 range) 254 | { 255 | return (u32) rdtsc() % range; 256 | } 257 | 258 | /* Shuffle an array of bytes arr of length len in-place using Fisher-Yates. */ 259 | void shuffle(u8 arr[], u32 len) 260 | { 261 | u32 i, j; 262 | u8 t; 263 | for (i = len - 1; i > 0; i--) { 264 | j = rand(i + 1); 265 | t = arr[i]; 266 | arr[i] = arr[j]; 267 | arr[j] = t; 268 | } 269 | } 270 | 271 | /* Tetris */ 272 | 273 | /* The seven tetriminos in each rotation. Each tetrimino is represented as an 274 | * array of 4 rotations, each represented by a 4x4 array of color values. */ 275 | u8 TETRIS[7][4][4][4] = { 276 | { /* I */ 277 | {{0,0,0,0}, 278 | {4,4,4,4}, 279 | {0,0,0,0}, 280 | {0,0,0,0}}, 281 | {{0,4,0,0}, 282 | {0,4,0,0}, 283 | {0,4,0,0}, 284 | {0,4,0,0}}, 285 | {{0,0,0,0}, 286 | {4,4,4,4}, 287 | {0,0,0,0}, 288 | {0,0,0,0}}, 289 | {{0,4,0,0}, 290 | {0,4,0,0}, 291 | {0,4,0,0}, 292 | {0,4,0,0}} 293 | }, 294 | { /* J */ 295 | {{7,0,0,0}, 296 | {7,7,7,0}, 297 | {0,0,0,0}, 298 | {0,0,0,0}}, 299 | {{0,7,7,0}, 300 | {0,7,0,0}, 301 | {0,7,0,0}, 302 | {0,0,0,0}}, 303 | {{0,0,0,0}, 304 | {7,7,7,0}, 305 | {0,0,7,0}, 306 | {0,0,0,0}}, 307 | {{0,7,0,0}, 308 | {0,7,0,0}, 309 | {7,7,0,0}, 310 | {0,0,0,0}} 311 | }, 312 | { /* L */ 313 | {{0,0,5,0}, 314 | {5,5,5,0}, 315 | {0,0,0,0}, 316 | {0,0,0,0}}, 317 | {{0,5,0,0}, 318 | {0,5,0,0}, 319 | {0,5,5,0}, 320 | {0,0,0,0}}, 321 | {{0,0,0,0}, 322 | {5,5,5,0}, 323 | {5,0,0,0}, 324 | {0,0,0,0}}, 325 | {{5,5,0,0}, 326 | {0,5,0,0}, 327 | {0,5,0,0}, 328 | {0,0,0,0}} 329 | }, 330 | { /* O */ 331 | {{0,0,0,0}, 332 | {0,1,1,0}, 333 | {0,1,1,0}, 334 | {0,0,0,0}}, 335 | {{0,0,0,0}, 336 | {0,1,1,0}, 337 | {0,1,1,0}, 338 | {0,0,0,0}}, 339 | {{0,0,0,0}, 340 | {0,1,1,0}, 341 | {0,1,1,0}, 342 | {0,0,0,0}}, 343 | {{0,0,0,0}, 344 | {0,1,1,0}, 345 | {0,1,1,0}, 346 | {0,0,0,0}} 347 | }, 348 | { /* S */ 349 | {{0,0,0,0}, 350 | {0,2,2,0}, 351 | {2,2,0,0}, 352 | {0,0,0,0}}, 353 | {{0,2,0,0}, 354 | {0,2,2,0}, 355 | {0,0,2,0}, 356 | {0,0,0,0}}, 357 | {{0,0,0,0}, 358 | {0,2,2,0}, 359 | {2,2,0,0}, 360 | {0,0,0,0}}, 361 | {{0,2,0,0}, 362 | {0,2,2,0}, 363 | {0,0,2,0}, 364 | {0,0,0,0}} 365 | }, 366 | { /* T */ 367 | {{0,6,0,0}, 368 | {6,6,6,0}, 369 | {0,0,0,0}, 370 | {0,0,0,0}}, 371 | {{0,6,0,0}, 372 | {0,6,6,0}, 373 | {0,6,0,0}, 374 | {0,0,0,0}}, 375 | {{0,0,0,0}, 376 | {6,6,6,0}, 377 | {0,6,0,0}, 378 | {0,0,0,0}}, 379 | {{0,6,0,0}, 380 | {6,6,0,0}, 381 | {0,6,0,0}, 382 | {0,0,0,0}} 383 | }, 384 | { /* Z */ 385 | {{0,0,0,0}, 386 | {3,3,0,0}, 387 | {0,3,3,0}, 388 | {0,0,0,0}}, 389 | {{0,0,3,0}, 390 | {0,3,3,0}, 391 | {0,3,0,0}, 392 | {0,0,0,0}}, 393 | {{0,0,0,0}, 394 | {3,3,0,0}, 395 | {0,3,3,0}, 396 | {0,0,0,0}}, 397 | {{0,0,3,0}, 398 | {0,3,3,0}, 399 | {0,3,0,0}, 400 | {0,0,0,0}} 401 | } 402 | }; 403 | 404 | /* Two-dimensional array of color values */ 405 | u8 well[WELL_HEIGHT][WELL_WIDTH]; 406 | 407 | struct { 408 | u8 i, r; /* Index and rotation into the TETRIS array */ 409 | u8 p; /* Index into bag of preview tetrimino */ 410 | s8 x, y; /* Coordinates */ 411 | s8 g; /* Y-coordinate of ghost */ 412 | } current; 413 | 414 | /* Shuffled bag of next tetrimino indices */ 415 | #define BAG_SIZE (7) 416 | u8 bag[BAG_SIZE] = {0, 1, 2, 3, 4, 5, 6}; 417 | 418 | u32 score = 0, level = 1, speed = INITIAL_SPEED; 419 | 420 | bool paused = false, game_over = false; 421 | 422 | /* Return true if the tetrimino i in rotation r will collide when placed at x, 423 | * y. */ 424 | bool collide(u8 i, u8 r, s8 x, s8 y) 425 | { 426 | u8 xx, yy; 427 | for (yy = 0; yy < 4; yy++) 428 | for (xx = 0; xx < 4; xx++) 429 | if (TETRIS[i][r][yy][xx]) 430 | if (x + xx < 0 || x + xx >= WELL_WIDTH || 431 | y + yy < 0 || y + yy >= WELL_HEIGHT || 432 | well[y + yy][x + xx]) 433 | return true; 434 | return false; 435 | } 436 | 437 | u32 stats[7]; 438 | 439 | /* Set the current tetrimino to the preview tetrimino in the default rotation 440 | * and place it in the top center. Increase the stats count for the spawned 441 | * tetrimino. Set the preview tetrimino to the next one in the shuffled bag. If 442 | * the spawned tetrimino was the last in the bag, re-shuffle the bag and set 443 | * the preview to the first in the bag. */ 444 | void spawn(void) 445 | { 446 | current.i = bag[current.p]; 447 | stats[current.i]++; 448 | current.r = 0; 449 | current.x = WELL_WIDTH / 2 - 2; 450 | current.y = 0; 451 | current.p++; 452 | if (current.p == BAG_SIZE) { 453 | current.p = 0; 454 | shuffle(bag, BAG_SIZE); 455 | } 456 | } 457 | 458 | /* Set the ghost y-coordinate by moving the current tetrimino down until it 459 | * collides. */ 460 | void ghost(void) 461 | { 462 | s8 y; 463 | for (y = current.y; y < WELL_HEIGHT; y++) 464 | if (collide(current.i, current.r, current.x, y)) 465 | break; 466 | current.g = y - 1; 467 | } 468 | 469 | /* Try to move the current tetrimino by dx, dy and return true if successful. 470 | */ 471 | bool move(s8 dx, s8 dy) 472 | { 473 | if (game_over) 474 | return false; 475 | 476 | if (collide(current.i, current.r, current.x + dx, current.y + dy)) 477 | return false; 478 | current.x += dx; 479 | current.y += dy; 480 | return true; 481 | } 482 | 483 | /* Try to rotate the current tetrimino clockwise and return true if successful. 484 | */ 485 | bool rotate(void) 486 | { 487 | if (game_over) 488 | return false; 489 | 490 | u8 r = (current.r + 1) % 4; 491 | if (collide(current.i, r, current.x, current.y)) 492 | return false; 493 | current.r = r; 494 | return true; 495 | } 496 | 497 | /* Try to move the current tetrimino down one and increase the score if 498 | * successful. */ 499 | void soft_drop(void) 500 | { 501 | if (move(0, 1)) 502 | score += SOFT_DROP_SCORE; 503 | } 504 | 505 | /* Lock the current tetrimino into the well. This is done by copying the color 506 | * values from the 4x4 array of the tetrimino into the well array. */ 507 | void lock(void) 508 | { 509 | u8 x, y; 510 | for (y = 0; y < 4; y++) 511 | for (x = 0; x < 4; x++) 512 | if (TETRIS[current.i][current.r][y][x]) 513 | well[current.y + y][current.x + x] = 514 | TETRIS[current.i][current.r][y][x]; 515 | } 516 | 517 | /* The y-coordinates of the rows cleared in the last update, top down */ 518 | s8 cleared_rows[4]; 519 | 520 | /* Update the game state. Called at an interval relative to the current level. 521 | */ 522 | void update(void) 523 | { 524 | /* Gravity: move the current tetrimino down by one. If it cannot be moved 525 | * and it is still in the top row, set game over state. If it cannot be 526 | * moved down but is not in the top row, lock it in place and spawn a new 527 | * tetrimino. */ 528 | if (!move(0, 1)) { 529 | if (current.y == 0) { 530 | game_over = true; 531 | return; 532 | } 533 | lock(); 534 | spawn(); 535 | } 536 | 537 | /* Row clearing: check if any rows are full across and add them to the 538 | * cleared_rows array. */ 539 | static u8 level_rows = 0; /* Rows cleared in the current level */ 540 | 541 | u8 x, y, a, i = 0, rows = 0; 542 | for (y = 0; y < WELL_HEIGHT; y++) { 543 | for (a = 0, x = 0; x < WELL_WIDTH; x++) 544 | if (well[y][x]) 545 | a++; 546 | if (a != WELL_WIDTH) 547 | continue; 548 | 549 | rows++; 550 | cleared_rows[i++] = y; 551 | } 552 | 553 | /* Scoring */ 554 | switch (rows) { 555 | case 1: score += SCORE_FACTOR_1 * level; break; 556 | case 2: score += SCORE_FACTOR_2 * level; break; 557 | case 3: score += SCORE_FACTOR_3 * level; break; 558 | case 4: score += SCORE_FACTOR_4 * level; break; 559 | } 560 | 561 | /* Leveling: increase the level for every 10 rows cleared, increase game 562 | * speed. */ 563 | level_rows += rows; 564 | if (level_rows >= ROWS_PER_LEVEL) { 565 | level++; 566 | level_rows -= ROWS_PER_LEVEL; 567 | 568 | double speed_s = pow(0.8 - (level - 1) * 0.007, level - 1); 569 | speed = speed_s * 1000; 570 | } 571 | } 572 | 573 | /* Clear the rows in the rows_cleared array and move all rows above them down. 574 | */ 575 | void clear_rows(void) 576 | { 577 | s8 i, y, x; 578 | for (i = 0; i < 4; i++) { 579 | if (!cleared_rows[i]) 580 | break; 581 | for (y = cleared_rows[i]; y > 0; y--) 582 | for (x = 0; x < WELL_WIDTH; x++) 583 | well[y][x] = well[y - 1][x]; 584 | cleared_rows[i] = 0; 585 | } 586 | } 587 | 588 | /* Move the current tetrimino to the position of its ghost, increase the score 589 | * and trigger an update (to cause locking and clearing). */ 590 | void drop(void) 591 | { 592 | if (game_over) 593 | return; 594 | 595 | score += HARD_DROP_SCORE_FACTOR * (current.g - current.y); 596 | current.y = current.g; 597 | update(); 598 | } 599 | 600 | #define TITLE_X (COLS / 2 - 9) 601 | #define TITLE_Y (ROWS / 2 - 1) 602 | 603 | /* Draw about information in the centre. Shown on boot and pause. */ 604 | void draw_about(void) { 605 | puts(TITLE_X, TITLE_Y, BLACK, RED, " "); 606 | puts(TITLE_X + 3, TITLE_Y, BLACK, MAGENTA, " "); 607 | puts(TITLE_X + 6, TITLE_Y, BLACK, BLUE, " "); 608 | puts(TITLE_X + 9, TITLE_Y, BLACK, GREEN, " "); 609 | puts(TITLE_X + 12, TITLE_Y, BLACK, YELLOW, " "); 610 | puts(TITLE_X + 15, TITLE_Y, BLACK, CYAN, " "); 611 | puts(TITLE_X, TITLE_Y + 1, BRIGHT | RED, RED, " T "); 612 | puts(TITLE_X + 3, TITLE_Y + 1, BRIGHT | MAGENTA, MAGENTA, " E "); 613 | puts(TITLE_X + 6, TITLE_Y + 1, BRIGHT | BLUE, BLUE, " T "); 614 | puts(TITLE_X + 9, TITLE_Y + 1, BRIGHT | GREEN, GREEN, " R "); 615 | puts(TITLE_X + 12, TITLE_Y + 1, BRIGHT | YELLOW, YELLOW, " I "); 616 | puts(TITLE_X + 15, TITLE_Y + 1, BRIGHT | CYAN, CYAN, " S "); 617 | puts(TITLE_X, TITLE_Y + 2, BLACK, RED, " "); 618 | puts(TITLE_X + 3, TITLE_Y + 2, BLACK, MAGENTA, " "); 619 | puts(TITLE_X + 6, TITLE_Y + 2, BLACK, BLUE, " "); 620 | puts(TITLE_X + 9, TITLE_Y + 2, BLACK, GREEN, " "); 621 | puts(TITLE_X + 12, TITLE_Y + 2, BLACK, YELLOW, " "); 622 | puts(TITLE_X + 15, TITLE_Y + 2, BLACK, CYAN, " "); 623 | 624 | puts(0, ROWS - 1, BRIGHT | BLACK, BLACK, 625 | TETRIS_NAME " " TETRIS_VERSION " " TETRIS_URL); 626 | } 627 | 628 | #define WELL_X (COLS / 2 - WELL_WIDTH) 629 | 630 | #define PREVIEW_X (COLS * 3/4 + 1) 631 | #define PREVIEW_Y (2) 632 | 633 | #define STATUS_X (COLS * 3/4) 634 | #define STATUS_Y (ROWS / 2 - 4) 635 | 636 | #define SCORE_X STATUS_X 637 | #define SCORE_Y (ROWS / 2 - 1) 638 | 639 | #define LEVEL_X SCORE_X 640 | #define LEVEL_Y (SCORE_Y + 4) 641 | 642 | /* Draw the well, current tetrimino, its ghost, the preview tetrimino, the 643 | * status, score and level indicators. Each well/tetrimino cell is drawn one 644 | * screen-row high and two screen-columns wide. The top two rows of the well 645 | * are hidden. Rows in the cleared_rows array are drawn as white rather than 646 | * their actual colors. */ 647 | void draw(void) 648 | { 649 | u8 x, y; 650 | 651 | if (paused) { 652 | draw_about(); 653 | goto status; 654 | } 655 | 656 | /* Border */ 657 | for (y = 2; y < WELL_HEIGHT; y++) { 658 | putc(WELL_X - 1, y, BLACK, GRAY, ' '); 659 | putc(COLS / 2 + WELL_WIDTH, y, BLACK, GRAY, ' '); 660 | } 661 | for (x = 0; x < WELL_WIDTH * 2 + 2; x++) 662 | putc(WELL_X + x - 1, WELL_HEIGHT, BLACK, GRAY, ' '); 663 | 664 | /* Well */ 665 | for (y = 0; y < 2; y++) 666 | for (x = 0; x < WELL_WIDTH; x++) 667 | puts(WELL_X + x * 2, y, BLACK, BLACK, " "); 668 | for (y = 2; y < WELL_HEIGHT; y++) 669 | for (x = 0; x < WELL_WIDTH; x++) 670 | if (well[y][x]) 671 | if (cleared_rows[0] == y || cleared_rows[1] == y || 672 | cleared_rows[2] == y || cleared_rows[3] == y) 673 | puts(WELL_X + x * 2, y, BLACK, BRIGHT | GRAY, " "); 674 | else 675 | puts(WELL_X + x * 2, y, BLACK, well[y][x], " "); 676 | else 677 | puts(WELL_X + x * 2, y, BRIGHT, BLACK, "::"); 678 | 679 | /* Ghost */ 680 | if (!game_over) 681 | for (y = 0; y < 4; y++) 682 | for (x = 0; x < 4; x++) 683 | if (TETRIS[current.i][current.r][y][x]) 684 | puts(WELL_X + current.x * 2 + x * 2, current.g + y, 685 | TETRIS[current.i][current.r][y][x], BLACK, "::"); 686 | 687 | /* Current */ 688 | for (y = 0; y < 4; y++) 689 | for (x = 0; x < 4; x++) 690 | if (TETRIS[current.i][current.r][y][x]) 691 | puts(WELL_X + current.x * 2 + x * 2, current.y + y, BLACK, 692 | TETRIS[current.i][current.r][y][x], " "); 693 | 694 | /* Preview */ 695 | for (y = 0; y < 4; y++) 696 | for (x = 0; x < 4; x++) 697 | if (TETRIS[bag[current.p]][0][y][x]) 698 | puts(PREVIEW_X + x * 2, PREVIEW_Y + y, BLACK, 699 | TETRIS[bag[current.p]][0][y][x], " "); 700 | else 701 | puts(PREVIEW_X + x * 2, PREVIEW_Y + y, BLACK, BLACK, " "); 702 | 703 | status: 704 | if (paused) 705 | puts(STATUS_X + 2, STATUS_Y, BRIGHT | YELLOW, BLACK, "PAUSED"); 706 | if (game_over) 707 | puts(STATUS_X, STATUS_Y, BRIGHT | RED, BLACK, "GAME OVER"); 708 | 709 | /* Score */ 710 | puts(SCORE_X + 2, SCORE_Y, BLUE, BLACK, "SCORE"); 711 | puts(SCORE_X, SCORE_Y + 2, BRIGHT | BLUE, BLACK, itoa(score, 10, 10)); 712 | 713 | /* Level */ 714 | puts(LEVEL_X + 2, LEVEL_Y, BLUE, BLACK, "LEVEL"); 715 | puts(LEVEL_X, LEVEL_Y + 2, BRIGHT | BLUE, BLACK, itoa(level, 10, 10)); 716 | } 717 | 718 | noreturn main() 719 | { 720 | clear(BLACK); 721 | draw_about(); 722 | 723 | /* Wait a full second to calibrate timing. */ 724 | u32 itpms; 725 | tps(); 726 | itpms = tpms; while (tpms == itpms) tps(); 727 | itpms = tpms; while (tpms == itpms) tps(); 728 | 729 | /* Initialize game state. Shuffle bag of tetriminos until first tetrimino 730 | * is not S or Z. */ 731 | do { shuffle(bag, BAG_SIZE); } while (bag[0] == 4 || bag[0] == 6); 732 | spawn(); 733 | ghost(); 734 | clear(BLACK); 735 | draw(); 736 | 737 | bool debug = false, help = true, statistics = false; 738 | u8 last_key; 739 | loop: 740 | tps(); 741 | 742 | if (debug) { 743 | u32 i; 744 | puts(0, 0, BRIGHT | GREEN, BLACK, "RTC sec:"); 745 | puts(10, 0, GREEN, BLACK, itoa(rtcs(), 16, 2)); 746 | puts(0, 1, BRIGHT | GREEN, BLACK, "ticks/ms:"); 747 | puts(10, 1, GREEN, BLACK, itoa(tpms, 10, 10)); 748 | puts(0, 2, BRIGHT | GREEN, BLACK, "key:"); 749 | puts(10, 2, GREEN, BLACK, itoa(last_key, 16, 2)); 750 | puts(0, 3, BRIGHT | GREEN, BLACK, "i,r,p:"); 751 | puts(10, 3, GREEN, BLACK, itoa(current.i, 10, 1)); 752 | putc(11, 3, GREEN, BLACK, ','); 753 | puts(12, 3, GREEN, BLACK, itoa(current.r, 10, 1)); 754 | putc(13, 3, GREEN, BLACK, ','); 755 | puts(14, 3, GREEN, BLACK, itoa(current.p, 10, 1)); 756 | puts(0, 4, BRIGHT | GREEN, BLACK, "x,y,g:"); 757 | puts(10, 4, GREEN, BLACK, itoa(current.x, 10, 3)); 758 | putc(13, 4, GREEN, BLACK, ','); 759 | puts(14, 4, GREEN, BLACK, itoa(current.y, 10, 3)); 760 | putc(17, 4, GREEN, BLACK, ','); 761 | puts(18, 4, GREEN, BLACK, itoa(current.g, 10, 3)); 762 | puts(0, 5, BRIGHT | GREEN, BLACK, "bag:"); 763 | for (i = 0; i < 7; i++) 764 | puts(10 + i * 2, 5, GREEN, BLACK, itoa(bag[i], 10, 1)); 765 | puts(0, 6, BRIGHT | GREEN, BLACK, "speed:"); 766 | puts(10, 6, GREEN, BLACK, itoa(speed, 10, 10)); 767 | for (i = 0; i < TIMER__LENGTH; i++) { 768 | puts(0, 7 + i, BRIGHT | GREEN, BLACK, "timer:"); 769 | puts(10, 7 + i, GREEN, BLACK, itoa(timers[i], 10, 10)); 770 | } 771 | } 772 | 773 | if (help) { 774 | puts(1, 12, BRIGHT | BLUE, BLACK, "LEFT"); 775 | puts(7, 12, BLUE, BLACK, "- Move left"); 776 | puts(1, 13, BRIGHT | BLUE, BLACK, "RIGHT"); 777 | puts(7, 13, BLUE, BLACK, "- Move right"); 778 | puts(1, 14, BRIGHT | BLUE, BLACK, "UP"); 779 | puts(7, 14, BLUE, BLACK, "- Rotate clockwise"); 780 | puts(1, 15, BRIGHT | BLUE, BLACK, "DOWN"); 781 | puts(7, 15, BLUE, BLACK, "- Soft drop"); 782 | puts(1, 16, BRIGHT | BLUE, BLACK, "ENTER"); 783 | puts(7, 16, BLUE, BLACK, "- Hard drop"); 784 | puts(1, 17, BRIGHT | BLUE, BLACK, "P"); 785 | puts(7, 17, BLUE, BLACK, "- Pause"); 786 | puts(1, 18, BRIGHT | BLUE, BLACK, "R"); 787 | puts(7, 18, BLUE, BLACK, "- Hard reset"); 788 | puts(1, 19, BRIGHT | BLUE, BLACK, "S"); 789 | puts(7, 19, BLUE, BLACK, "- Toggle statistics"); 790 | puts(1, 20, BRIGHT | BLUE, BLACK, "D"); 791 | puts(7, 20, BLUE, BLACK, "- Toggle debug info"); 792 | puts(1, 21, BRIGHT | BLUE, BLACK, "H"); 793 | puts(7, 21, BLUE, BLACK, "- Toggle help"); 794 | } 795 | 796 | if (statistics) { 797 | u8 i, x, y; 798 | for (i = 0; i < 7; i++) { 799 | for (y = 0; y < 4; y++) 800 | for (x = 0; x < 4; x++) 801 | if (TETRIS[i][0][y][x]) 802 | puts(5 + x * 2, 1 + i * 3 + y, BLACK, 803 | TETRIS[i][0][y][x], " "); 804 | puts(14, 2 + i * 3, BLUE, BLACK, itoa(stats[i], 10, 10)); 805 | } 806 | } 807 | 808 | bool updated = false; 809 | 810 | u8 key; 811 | if ((key = scan())) { 812 | last_key = key; 813 | switch(key) { 814 | case KEY_D: 815 | debug = !debug; 816 | if (debug) 817 | help = statistics = false; 818 | clear(BLACK); 819 | break; 820 | case KEY_H: 821 | help = !help; 822 | if (help) 823 | debug = statistics = false; 824 | clear(BLACK); 825 | break; 826 | case KEY_S: 827 | statistics = !statistics; 828 | if (statistics) 829 | debug = help = false; 830 | clear(BLACK); 831 | break; 832 | case KEY_R: 833 | reset(); 834 | case KEY_LEFT: 835 | move(-1, 0); 836 | break; 837 | case KEY_RIGHT: 838 | move(1, 0); 839 | break; 840 | case KEY_DOWN: 841 | soft_drop(); 842 | break; 843 | case KEY_UP: 844 | rotate(); 845 | break; 846 | case KEY_ENTER: 847 | drop(); 848 | break; 849 | case KEY_P: 850 | if (game_over) 851 | break; 852 | clear(BLACK); 853 | paused = !paused; 854 | break; 855 | } 856 | updated = true; 857 | } 858 | 859 | if (!paused && !game_over && interval(TIMER_UPDATE, speed)) { 860 | update(); 861 | updated = true; 862 | } 863 | 864 | if (cleared_rows[0] && wait(TIMER_CLEAR, CLEAR_DELAY)) { 865 | clear_rows(); 866 | updated = true; 867 | } 868 | 869 | if (updated) { 870 | ghost(); 871 | draw(); 872 | } 873 | 874 | goto loop; 875 | } 876 | --------------------------------------------------------------------------------