diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 29a5c071..f1857378 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,6 +90,7 @@ jobs: name: Development build ${{ env.RELEASE_DATE }} files: | cpm65/images/apple2e.po + cpm65/images/apple2e_b.po cpm65/images/atari800.atr cpm65/images/atari800b.atr cpm65/images/atari800c.atr diff --git a/apps/scrntest.asm b/apps/scrntest.asm index 8c3c5bd6..8b84a224 100644 --- a/apps/scrntest.asm +++ b/apps/scrntest.asm @@ -15,8 +15,11 @@ .zp style, 1 .zp ptr1, 2 .zp ptr2, 2 +.zp kbd_blocking, 1 +.zp spinner_pos, 1 .label string_init .label string_a +.label chars_spinner .label BIOS .label SCREEN .label update_cursor @@ -54,9 +57,15 @@ lda #1 sta cur_vis + ldy #SCREEN_SHOWCURSOR + jsr SCREEN lda #0 sta style + sta spinner_pos + + lda #1 + sta kbd_blocking help: \ Clear screen and print help @@ -92,11 +101,59 @@ mainloop: sta cur_x stx cur_y +timeout: + \ Show spinnig bar to test non-blocking keyboard read + lda #39 + ldx #0 + ldy #SCREEN_SETCURSOR + jsr SCREEN + ldy spinner_pos + iny + tya + cmp #4 + .zif eq + ldy #0 + .zendif + sty spinner_pos + \ldx kbd_blocking + \.zif ne + \ lda #'*' + \.zendif + ldx kbd_blocking + \.zif eq + lda chars_spinner, y + \.zendif + ldy #SCREEN_PUTCHAR + jsr SCREEN + + lda #39 + ldx #1 + ldy #SCREEN_SETCURSOR + jsr SCREEN + lda #32 + ldy #SCREEN_PUTCHAR + jsr SCREEN + + lda cur_x + ldx cur_y + ldy #SCREEN_SETCURSOR + jsr SCREEN + \ Get and parse command - lda #10 - ldx #00 + + ldy kbd_blocking + .zif ne + lda #0xff + ldx #0x7f + .zendif + ldy kbd_blocking + .zif eq + lda #10 \ 0.1 second timeout + ldx #0x0 + .zendif ldy #SCREEN_GETCHAR jsr SCREEN + bcs timeout \ make sure we don't have consecutive key presses because of short timeouts \ Convert to uppercase cmp #0x61 @@ -248,6 +305,21 @@ case_done: jsr SCREEN jmp mainloop .zendif + + \ Toggle keyboard read mode + cmp #'B' + .zif eq + lda kbd_blocking + .zif eq + lda #1 + sta kbd_blocking + jmp mainloop + .zendif + lda #0 + sta kbd_blocking + jmp mainloop + + .zendif \ Clear screen and print help cmp #'H' @@ -398,6 +470,7 @@ string_init: .byte "J - Scroll down\r\n" .byte "L - Clear to End of Line\r\n" .byte "I - Toggle style\r\n" + .byte "B - Toggle blocking/non-blocking read\r\n" .byte "H - Clear screen and print this help\r\n" .byte "Q - Quit\r\n" .byte 0 @@ -406,3 +479,7 @@ string_a: .byte "String" .byte 0 +chars_spinner: + .byte "-\\|/" + .byte 0 + diff --git a/build.py b/build.py index 2d03dbc3..4e769384 100644 --- a/build.py +++ b/build.py @@ -20,6 +20,7 @@ "images/oric.dsk": "src/arch/oric+diskimage", "images/apple2e.po": "src/arch/apple2e+diskimage", "images/apple2e_b.po": "src/arch/apple2e+diskimage_b", + "images/apple2e_c.po": "src/arch/apple2e+diskimage_c", "images/atari800.atr": "src/arch/atari800+atari800_diskimage", "images/atari800b.atr": "src/arch/atari800+atari800b_diskimage", "images/atari800c.atr": "src/arch/atari800+atari800c_diskimage", diff --git a/config.py b/config.py index 82f862c3..0f937ef5 100644 --- a/config.py +++ b/config.py @@ -45,6 +45,8 @@ "0:life.com": "apps+life", "0:qe.com": "apps+qe", "0:scrntest.com": "apps+scrntest", + "0:tetris.com": "third_party/tetris", + "0:tetris2.com": "third_party/tetris2", "0:vt52drv.com": "apps+vt52drv", "0:vt52test.com": "apps+vt52test", "0:kbdtest.com": "apps+kbdtest", @@ -60,6 +62,7 @@ PASCAL_APPS = { "0:pint.com": "third_party/pascal-m+pint", + "0:pasc.txt": "cpmfs+pasc_txt_cpm", "0:pasc.obb": "third_party/pascal-m+pasc-obb", "0:pload.com": "third_party/pascal-m+loader", "0:hello.pas": "cpmfs+hello_pas_cpm", diff --git a/cpmfs/build.py b/cpmfs/build.py index 579f0004..d2e0f1c2 100644 --- a/cpmfs/build.py +++ b/cpmfs/build.py @@ -1,6 +1,6 @@ from tools.build import unixtocpm -for f in ["asm", "atbasic", "bedit"]: +for f in ["asm", "atbasic", "bedit", "pasc"]: unixtocpm(name="%s_txt_cpm" % f, src="./%s.txt" % f) unixtocpm(name="demo_sub_cpm", src="./demo.sub") diff --git a/cpmfs/pasc.txt b/cpmfs/pasc.txt new file mode 100644 index 00000000..3fdbb20c --- /dev/null +++ b/cpmfs/pasc.txt @@ -0,0 +1,6 @@ +Pascal-M +-------- + +pint pasc.obb hello.pas hello.obp +pload hello.obp hello.obb +pint hello.obb diff --git a/lib/screen.S b/lib/screen.S index 0f2018f4..14be1091 100644 --- a/lib/screen.S +++ b/lib/screen.S @@ -68,6 +68,15 @@ zproc screen_clear_to_eol, .text.screen_clear_to_eol jmp _call_screen zendproc +zproc screen_getchar, .text.screen_getchar + ldy #SCREEN_GETCHAR + jsr _call_screen + zif_cs + lda #0 ; is this a good way to report the timeout? + zendif + rts +zendproc + zproc screen_waitchar, .text.screen_waitchar zrepeat lda #0xff diff --git a/src/arch/apple2e/apple2e.S b/src/arch/apple2e/apple2e.S index 85296e03..40944cde 100644 --- a/src/arch/apple2e/apple2e.S +++ b/src/arch/apple2e/apple2e.S @@ -1,4 +1,4 @@ -; CP/M-65 Copyright © 2023 David Given +; CP/M-65 Copyright © 2023 David Given ; This file is licensed under the terms of the 2-clause BSD license. Please ; see the COPYING file in the root project directory for the full text. @@ -48,8 +48,31 @@ DECODE_TABLE_START = 0x96 SCREENF_CURSORSHOWN = 0x80 SCREENF_INVERSE = 0x40 - +SCREENF_CURSORENABLED = 0x20 + +;#define APPLE2 +;#define APPLE2PLUS +;#define APPLE2EUROPLUS +;#define APPLE2JPLUS +;#define APPLE2E +;#define APPLE2EENHANCED +;#define APPLE2C +;#define APPLE2GS + +#define TTY_MODE_40COLUMNS 1 +#define TTY_MODE_80COLUMNS 2 + +#if defined APPLE2C || defined APPLE2GS || defined APPLE2EENHANCED || defined APPLE2E +#define TTY_MODE TTY_MODE_80COLUMNS SCRWIDTH = 80 +; TODO: we might to want 40 columns on newer models (really?) +#elif defined APPLE2JPLUS || defined APPLE2EUROPLUS || defined APPLE2PLUS || defined APPLE2 +#define TTY_MODE TTY_MODE_40COLUMNS +SCRWIDTH = 40 +; TOPO: we might have a Videx and use that one for 80 columns mode +#else +#error Unknown Apple II model +#endif SCRHEIGHT = 24 .extern biosentry @@ -76,10 +99,12 @@ _start: cli lda MEM_BANKING+0xb ; R/W 0xe000 RAM; bank 1 in 0xd000 lda MEM_BANKING+0xb ; yes, I'm sure +#if TTY_MODE == TTY_MODE_80COLUMNS sta SCREEN_80COLON sta SCREEN_80STOREON sta SCREEN_PAGE2OFF sta SCREEN_ALTCHARSET +#endif ldx #0xff txs ; reset stack @@ -107,6 +132,7 @@ _start: jsr initdrivers lda #0 + ora #(SCREENF_CURSORENABLED) sta cursorf ; Print the startup banner. @@ -252,7 +278,7 @@ screen_jmptable_lo: jmptablo screen_putchar jmptablo screen_putstring jmptablo screen_getchar - jmptablo fail ; screen_showcursor + jmptablo screen_showcursor jmptablo screen_scrollup jmptablo screen_scrolldown jmptablo screen_cleartoeol @@ -266,7 +292,7 @@ screen_jmptable_hi: jmptabhi screen_putchar jmptabhi screen_putstring jmptabhi screen_getchar - jmptabhi fail + jmptabhi screen_showcursor jmptabhi screen_scrollup jmptabhi screen_scrolldown jmptabhi screen_cleartoeol @@ -295,6 +321,10 @@ zproc apply_style bit cursorf zif_vc ora #0x80 ; non inverted +#if TTY_MODE == TTY_MODE_40COLUMNS + zelse + and #0x3f ; inverted 40 columns +#endif zendif rts zendproc @@ -315,9 +345,11 @@ zproc screen_clear txa zrepeat dey +#if TTY_MODE == TTY_MODE_80COLUMNS sta SCREEN_PAGE2ON sta (ptr), y sta SCREEN_PAGE2OFF +#endif sta (ptr), y zuntil_eq @@ -380,7 +412,21 @@ zproc screen_putstring rts zendproc +; XA = timeout in cs (X=MSB, A=LSB) +; carry set on timeout +; carry clean when A=key + zproc screen_getchar + sta ptr + stx ptr+1 + ; this feels wrong, but how is otherwise X=0 working with DEC and underflows? + inc ptr+1 + + ; TODO: adjust here + ;~ clc + ;~ rol ptr+1 + ;~ rol ptr + ; Turn the disk motor off. ldx #DISK_SLOT @@ -392,15 +438,49 @@ zproc screen_getchar jsr draw_cursor - ; Wait for the key. + ; Wait for the key, or time out - zrepeat - lda KBD_READ - zuntil_mi +_wait_loop: + ldy #$ff ; 255 * 5 cycles +_inner_loop: + lda KBD_READ ; 4 cycles + bpl _no_key ; 3 cycles, branch usually taken + ; got a key sta KBD_STROBERESET and #0x7f clc rts +_no_key: + dey ; 2 cycles + bne _inner_loop ; 3 cycles usually (unless 0) + dec ptr ; 5 cycles + bne _wait_loop ; 3 cycles (2 if 0) + dec ptr+1 ; 5 cycles + bne _wait_loop ; 3 cycles (2 if 0) + sec + rts +zendproc + +zproc screen_showcursor + cmp #0 + zif_eq + jsr prepare_for_screen_write + lda (ptr), y + ora #0x80 + sta (ptr), y + lda cursorf + and #(~SCREENF_CURSORENABLED) & 0xff ; unset bit 5, cursor is disabled + sta cursorf + zelse + lda cursorf + ora #SCREENF_CURSORENABLED ; set bit 5, cursor is enabled + sta cursorf + jsr prepare_for_screen_write + lda (ptr), y + and #0x7f + sta (ptr), y + zendif + rts zendproc zproc screen_scrollup @@ -419,12 +499,19 @@ zproc screen_scrollup txa jsr calculate_screen_address ; ptr is source pointer +#if TTY_MODE == TTY_MODE_80COLUMNS ldy #(SCRWIDTH/2)-1 +#endif +#if TTY_MODE == TTY_MODE_40COLUMNS + ldy #(SCRWIDTH)-1 +#endif zrepeat +#if TTY_MODE == TTY_MODE_80COLUMNS sta SCREEN_PAGE2OFF lda (ptr), y sta (ptr1), y sta SCREEN_PAGE2ON +#endif lda (ptr), y sta (ptr1), y dey @@ -452,12 +539,19 @@ zproc screen_scrolldown txa jsr calculate_screen_address ; ptr is source pointer +#if TTY_MODE == TTY_MODE_80COLUMNS ldy #(SCRWIDTH/2)-1 +#endif +#if TTY_MODE == TTY_MODE_40COLUMNS + ldy #(SCRWIDTH)-1 +#endif zrepeat +#if TTY_MODE == TTY_MODE_80COLUMNS sta SCREEN_PAGE2OFF lda (ptr), y sta (ptr1), y sta SCREEN_PAGE2ON +#endif lda (ptr), y sta (ptr1), y dey @@ -468,13 +562,20 @@ zproc screen_scrolldown zendproc ; fall through zproc clear_line_at_ptr - ldy #(SCRWIDTH/2)-1 +#if TTY_MODE == TTY_MODE_80COLUMNS + ldy #(SCRWIDTH/2)-1 +#endif +#if TTY_MODE == TTY_MODE_40COLUMNS + ldy #(SCRWIDTH)-1 +#endif lda #32 jsr apply_style zrepeat +#if TTY_MODE == TTY_MODE_80COLUMNS sta SCREEN_PAGE2OFF sta (ptr), y sta SCREEN_PAGE2ON +#endif sta (ptr), y dey zuntil_mi @@ -490,6 +591,7 @@ zproc screen_cleartoeol ldx cursorx zrepeat txa +#if TTY_MODE == TTY_MODE_80COLUMNS lsr a pha rol a @@ -499,13 +601,19 @@ zproc screen_cleartoeol sta SCREEN_PAGE2OFF, y pla +#endif tay lda #32 jsr apply_style sta (ptr), y inx +#if TTY_MODE == TTY_MODE_80COLUMNS cpx #SCRWIDTH +#endif +#if TTY_MODE == TTY_MODE_40COLUMNS + cpx #40 +#endif zuntil_eq rts zendproc @@ -553,10 +661,14 @@ zproc hide_cursor zendproc zproc toggle_cursor - jsr prepare_for_screen_write - lda (ptr), y - eor #0x80 - sta (ptr), y + lda cursorf + and #(SCREENF_CURSORENABLED) ; draw cursor only if bit 5 it set + zif_ne + jsr prepare_for_screen_write + lda (ptr), y + eor #0x80 + sta (ptr), y + zendif lda cursorf eor #SCREENF_CURSORSHOWN @@ -644,13 +756,17 @@ zproc prepare_for_screen_write lda cursory jsr calculate_screen_address lda cursorx +#if TTY_MODE == TTY_MODE_80COLUMNS lsr a +#endif tay lda #0 rol eor #1 tax +#if TTY_MODE == TTY_MODE_80COLUMNS sta SCREEN_PAGE2OFF, x +#endif rts zendproc diff --git a/src/arch/apple2e/apple2e.inc b/src/arch/apple2e/apple2e.inc new file mode 100644 index 00000000..11c5df28 --- /dev/null +++ b/src/arch/apple2e/apple2e.inc @@ -0,0 +1,34 @@ +; Apple ][ OS defines + +SCREEN_80STOREOFF = 0xc000 +SCREEN_80STOREON = 0xc001 +SCREEN_80COLOFF = 0xc00c +SCREEN_80COLON = 0xc00d +SCREEN_ALTCHARSET = 0xc00f +SCREEN_PAGE2OFF = 0xc054 +SCREEN_PAGE2ON = 0xc055 + +KBD_READ = 0xc000 +KBD_STROBERESET = 0xc010 + +//#define APPLE2 +//#define APPLE2PLUS +//#define APPLE2EUROPLUS +//#define APPLE2JPLUS +//#define APPLE2E +//#define APPLE2EENHANCED +//#define APPLE2C +//#define APPLE2GS + +#define TTY_MODE_40COLUMNS 1 +#define TTY_MODE_80COLUMNS 2 +#if defined APPLE2GS || defined APPLE2EENHANCED || defined APPLE2E +#define TTY_MODE TTY_MODE_80COLUMNS +SCREEN_WIDTH = 80 +#elif defined APPLE2C || defined APPLE2JPLUS || defined APPLE2EUROPLUS || defined APPLE2PLUS || defined APPLE2 +#define TTY_MODE TTY_MODE_40COLUMNS +SCREEN_WIDTH = 40 +#else +#error Unknown Apple II model +#endif +SCREEN_HEIGHT = 24 diff --git a/src/arch/apple2e/build.py b/src/arch/apple2e/build.py index f1097f81..3ad96121 100644 --- a/src/arch/apple2e/build.py +++ b/src/arch/apple2e/build.py @@ -14,6 +14,8 @@ llvmcfile( name="bios_obj", srcs=["./apple2e.S"], +# cflags=["-DAPPLE2E"], + cflags=["-DAPPLE2PLUS"], deps=["include", "src/lib+bioslib"], ) @@ -21,6 +23,8 @@ name="bios_prelink", srcs=[".+bios_obj"], deps=["src/lib+bioslib"], +# cflags=["-DAPPLE2E"], + cflags=["-DAPPLE2PLUS"], linkscript="./apple2e-prelink.ld", ldflags=["--defsym=BIOS_SIZE=0x4000"], ) @@ -56,30 +60,45 @@ items={ "0:ccp.sys@sr": "src+ccp-tiny", "0:bdos.sys@sr": "src/bdos", - "0:scrntest.com": "apps+scrntest", + "0:cls.com": "apps+cls", + } + | MINIMAL_APPS + | MINIMAL_APPS_SRCS + | SCREEN_APPS_SRCS +) + +mkcpmfs( + name="diskimage_b", + format="appleiie", + bootimage=".+bios_shuffled", + size=143360, + items={ + "0:ccp.sys@sr": "src+ccp-tiny", + "0:bdos.sys@sr": "src/bdos", "0:cls.com": "apps+cls", "0:atbasic.com": "third_party/altirrabasic", "0:atbasic.txt": "cpmfs+atbasic_txt_cpm", "0:qe.com": "apps+qe", } | MINIMAL_APPS - | MINIMAL_APPS_SRCS, + | SCREEN_APPS ) mkcpmfs( - name="diskimage_b", + name="diskimage_c", format="appleiie", bootimage=".+bios_shuffled", size=143360, items={ + "0:ccp.sys@sr": "src+ccp-tiny", + "0:bdos.sys@sr": "src/bdos", + "0:copy.com": "apps+copy", } - | SCREEN_APPS - | SCREEN_APPS_SRCS + | MINIMAL_APPS | BIG_SCREEN_APPS - | PASCAL_APPS, + | PASCAL_APPS ) - mametest( name="mametest", target="apple2e", diff --git a/src/arch/atari800/build.py b/src/arch/atari800/build.py index 4248254b..0348087f 100644 --- a/src/arch/atari800/build.py +++ b/src/arch/atari800/build.py @@ -33,8 +33,8 @@ "1:tty80drv.com": "src/arch/atari800/utils+tty80drv", "1:olivetti.fnt": "third_party/fonts/atari/olivetti.fnt", } - | MINIMAL_APPS - | SCREEN_APPS, + | MINIMAL_APPS, +# | SCREEN_APPS, ) simplerule( diff --git a/src/bdos/filesystem.S b/src/bdos/filesystem.S index 00c6d9b0..e9d984e7 100644 --- a/src/bdos/filesystem.S +++ b/src/bdos/filesystem.S @@ -1811,6 +1811,13 @@ zproc select_active_drive zuntil_mi clc + zelse + lda #<1f + ldx #>1f + jmp harderror +1: + .ascii "BDOS: disk missing" + .byte 13, 10, 0 zendif rts zendproc diff --git a/third_party/tetris/LICENSE b/third_party/tetris/LICENSE new file mode 100644 index 00000000..db5a691e --- /dev/null +++ b/third_party/tetris/LICENSE @@ -0,0 +1,13 @@ + Copyright (c) 2022 Eugene P. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/third_party/tetris/UPSTREAM.md b/third_party/tetris/UPSTREAM.md new file mode 100644 index 00000000..931bb584 --- /dev/null +++ b/third_party/tetris/UPSTREAM.md @@ -0,0 +1,2 @@ +This was downloaded from https://codeberg.org/eu/tetris. + diff --git a/third_party/tetris/build.py b/third_party/tetris/build.py new file mode 100644 index 00000000..d67de8a2 --- /dev/null +++ b/third_party/tetris/build.py @@ -0,0 +1,8 @@ +from build.ab import Rule, Target, Targets, simplerule +from build.llvm import llvmprogram + +llvmprogram( + name="tetris", + srcs=["./tetris.c"], + deps=["lib+cpm65"] +) diff --git a/third_party/tetris/tetris.c b/third_party/tetris/tetris.c new file mode 100644 index 00000000..79bc93b1 --- /dev/null +++ b/third_party/tetris/tetris.c @@ -0,0 +1,296 @@ +/* + * Copyright (c) 2022 Eugene P. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * Adapted to CP/M-65, Andreas Baumann, 2024 + */ + +#include +#include +#include +#include +#include "lib/screen.h" + +#define WIDTH (uint8_t)12 +#define HEIGHT (uint8_t)20 + +#define W (WIDTH + 2) +#define H (HEIGHT + 1) +#define WXH (uint16_t)(W * H) + +#define E '.' +#define F '#' + +// Simple acceleration calculation (unplayable over 9000 points) +#define ACCEL (10 - score / 100) + +/* Example: figure "J" rotation 0 + * 0b0100010001100000 + * + * 0 1 0 0 + * 0 1 0 0 + * 0 1 1 0 + * 0 0 0 0 + */ +uint16_t figure[][4] = { + {0b0010001000100010, 0b0000111100000000, 0b0010001000100010, 0b0000111100000000}, // I + {0b0010001001100000, 0b0000010001110000, 0b0110010001000000, 0b0000011100010000}, // J + {0b0100010001100000, 0b0000011101000000, 0b0110001000100000, 0b0000000101110000}, // L + {0b0110011000000000, 0b0110011000000000, 0b0110011000000000, 0b0110011000000000}, // O + {0b0100011000100000, 0b0000001101100000, 0b0100011000100000, 0b0000001101100000}, // S + {0b1110010000000000, 0b0010011000100000, 0b0000010011100000, 0b1000110010000000}, // T + {0b0010011001000000, 0b0000110001100000, 0b0010011001000000, 0b0000110001100000} // Z +}; + +uint8_t field[WXH]; + +struct player { + int8_t figure_index, rot, x, y; +} gp; + +static void cr() +{ + cpm_printstring("\n\r"); +} + +static void fatal(char* msg) +{ + cpm_printstring("Error: "); + cpm_printstring(msg); + cr(); + cpm_warmboot(); +} + +int init() +{ + // Init screen + if(!screen_init()) + fatal("No SCREEN driver, exiting"); + screen_clear(); + screen_showcursor(0); // invisible cursor + + // Init random generator + // TODO: can we access the zero page from a user program? Or do we + // need a system call to get sort of a time since boot? +// uint16_t seed = (*((uint8_t *)0x004F) << 8) | *((uint8_t *)0x004E) ; + uint16_t seed = 21342; + srand( seed ); + + // Init field + memset(field, E, WXH-W); + + + /* Draw glass + * + * | | + * | | + * +--+ + */ + uint8_t i; + + memset(field + WXH-W+1, '-', W-2); + + for (i = 0; i < H - 1; i++) + field[i * W] = field[i * W + W-1] = '|'; + field[i * W] = field[i * W + W-1] = '+'; + + return 0; +} + +int figure_draw(char ch, struct player p) +{ + uint16_t mask = 0b1000000000000000; + uint8_t x, y; + + for (y = 0; y < 4; y++) { + for (x = 0; x < 4; x++) { + if (figure[p.figure_index][p.rot] & mask) { + int16_t offset = (y + p.y) * W + x + p.x; + + if (ch == F && field[offset] != E) + return 0; + + if (gp.x == p.x && gp.y == p.y && gp.rot == p.rot) + field[offset] = ch; + } + mask >>= 1; + } + } + + return 1; +} + +int remove_lines() +{ + int16_t x, y; + uint16_t c, shift, lines = 0; + + for (y = 0; y < H - 1; y++) { + for (x = 1, c = 0; x < W - 1; x++) + if (field[y * W + x] == F) + c++; + + if (c == W - 2) { + lines++; + memset(field + y*W + 1, 0, W - 2); + } + } + // Full line(s) now filled with zeros + if (!lines) + return 0; + + // Remove them and move the blocks down + for (x = 1; x < W - 1; x++) { + shift = 0; + for (y = H - 2; y >= 0; y--) { + if (!field[y * W + x]) + shift++; + if (shift) { + if (field[y * W + x] == F) + field[(y + shift) * W + x] = F; + field[y * W + x] = E; + } + } + } + + uint16_t score = 10; + + if (lines > 1) + score += 20; + if (lines > 2) + score += 40; + if (lines > 3) + score += 80; + + return score; +} + +void end(uint16_t score) +{ + screen_clear(); + printf("Final score: %d\n", score); + cpm_warmboot(); +} + +void field_print(uint16_t score) +{ + uint8_t y, x, sy, sx; + + screen_getsize(&sx, &sy); + sy = (sy - H) / 2; + sx = (sx - W) / 2; + + for (y = 0; y < H; y++) { + for (x = 0; x < W; x++) { + screen_setcursor(sx+x, sy+y); + screen_putchar(field[y * W + x]); + } + } + + screen_setcursor(sx, sy + H + 1); + printf("Score: %d", score); +} + +int main() +{ + uint16_t counter = 0; + uint8_t drop = 0, draw_next = 1; + uint8_t key = 0; + uint16_t score = 0; + struct player p; + + init(); + + do { + if (draw_next == 1) { + draw_next = 0; + + gp.figure_index = (uint8_t)rand() % 7; + gp.x = W / 2 - 2; + gp.y = 0; + gp.rot = 0; + + drop = 0; + counter = 0; + score += remove_lines(); + + if (!figure_draw(F, gp)) + break; // game over + + field_print(score); + } + + p = gp; + key = screen_getchar(10); + + switch (key) { + case 'w': + case 'W': + p.rot = gp.rot == 3 ? 0 : gp.rot + 1; + break; + case 'a': + case 'A': + p.x = gp.x - 1; + break; + case 'd': + case 'D': + p.x = gp.x + 1; + break; + case ' ': + // quickly lower + if (!drop) + drop = 1; + break; + case 's': + case 'S': + // lower slowly + counter = 254; + break; + } + + /* TODO: sleep a little while, idle looping? */ + + if (++counter > ACCEL) + counter = 0; + + if (drop == 1 || !counter) + p.y = gp.y + 1; + + if (gp.x == p.x && gp.y == p.y && gp.rot == p.rot) + continue; + + figure_draw(E, gp); + + if (!figure_draw(F, p)) { + p.x = gp.x; + p.rot = gp.rot; + if (drop == 1) { + drop = 2; + counter = ACCEL / 2; + } else if (!figure_draw(F, p) && !counter) + draw_next = 1; + } else { + gp = p; + if (drop == 2) + drop = 0; // Allow use drop key again + } + + figure_draw(F, gp); + field_print(score); + } while (key != 27); + + screen_showcursor(1); // visible cursor + + end(score); +} diff --git a/third_party/tetris2/build.py b/third_party/tetris2/build.py new file mode 100644 index 00000000..c574b03d --- /dev/null +++ b/third_party/tetris2/build.py @@ -0,0 +1,8 @@ +from build.ab import Rule, Target, Targets, simplerule +from build.llvm import llvmprogram + +llvmprogram( + name="tetris2", + srcs=["./tetris2.c"], + deps=["lib+cpm65"] +) diff --git a/third_party/tetris2/tetris2.c b/third_party/tetris2/tetris2.c new file mode 100644 index 00000000..7ce06e2d --- /dev/null +++ b/third_party/tetris2/tetris2.c @@ -0,0 +1,306 @@ +/* Simple Tetris in the terminal, adapted for CP/M-65, */ +/* generated by using p2c with tetris.pas for CP/M 2.2 Apple ][ */ +/* A.Baumann, 14.12.2024, 0BSD clause */ + +#include +#include +#include +#include +#include +#include +#include +#include "lib/screen.h" + +#define Delay 200 +#define BoardHeight 20 +#define BoardWidth 10 +#define NofShapes 7 + +static uint8_t Shapes[NofShapes][4][4] = { + /* I */ + { { 0, 0, 0, 0 }, + { 1, 1, 1, 1 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 } }, + /* O */ + { { 0, 1, 1, 0 }, + { 0, 1, 1, 0 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 } }, + /* T */ + { { 0, 1, 0, 0 }, + { 1, 1, 1, 0 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 } }, + /* J */ + { { 1, 0, 0, 0 }, + { 1, 1, 1, 0 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 } }, + /* L */ + { { 0, 0, 1, 0 }, + { 1, 1, 1, 0 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 } }, + /* S */ + { { 0, 1, 1, 0 }, + { 1, 1, 0, 0 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 } }, + /* Z */ + { { 1, 1, 0, 0 }, + { 0, 1, 1, 0 }, + { 0, 0, 0, 0 }, + { 0, 0, 0, 0 } } +}; +static uint8_t ShapeChars[NofShapes] = {'I', 'O', 'T', 'J', 'L', 'S', 'Z' }; + +uint8_t Board[BoardHeight][BoardWidth]; +bool GameOver; +uint8_t CurrentPiece, NextPiece; +int8_t PosX, PosY; + +static void InitializeBoard(void) +{ + memset(Board, 0xFF, sizeof(Board)); +} + +static void DrawBoard(void) +{ + uint8_t x, y; + + for (y = 1; y <= BoardHeight; y++) { + for (x = 1; x <= BoardWidth; x++) { + screen_setcursor(x,y); + if (Board[y-1][x-1] != 0xFF) { + screen_setstyle(1); + screen_putchar(ShapeChars[Board[y-1][x-1]]); + } else { + screen_setstyle(0); + screen_putchar('.'); + } + } + } + screen_setstyle(0); +} + + +static void DrawPiece(uint8_t piece, int8_t x, int8_t y,bool Erase) +{ + uint8_t nx, ny; + + screen_setstyle(!Erase); + for (ny = 0; ny <= 3; ny++) { + for (nx = 0; nx <= 3; nx++) { + if (Shapes[piece][ny][nx] == 1) { + screen_setcursor(x + nx, y + ny); + if (Erase) + screen_putchar('.'); + else + screen_putchar(ShapeChars[piece]); + } + } + } + screen_setstyle(0); +} + +static bool CanMove(int8_t dx, int8_t dy) +{ + int8_t x, y; + + for (y = 0; y <= 3; y++) { + for (x = 0; x <= 3; x++) { + if (Shapes[CurrentPiece][y][x] == 1) { + if (PosX + x + dx < 1 || PosX + x + dx > BoardWidth || + PosY + y + dy > BoardHeight || + Board[PosY + y + dy - 1][PosX + x + dx - 1] != 0xFF) + return false; + } + } + } + return true; +} + +static void DrawNextPiece(void) +{ + int8_t x, y; + + for (y = 10; y <= 13; y++) { + for (x = 12; x <= 15; x++) { + screen_setcursor(x, y); + screen_putchar('.'); + } + } + DrawPiece(NextPiece, 12, 10, false); +} + +static void NewPiece(void) +{ + CurrentPiece = NextPiece; + NextPiece = (uint8_t)rand() % NofShapes; + DrawNextPiece(); + PosX = BoardWidth / 2 - 2; + PosY = 1; + if (!CanMove(0, 0)) + GameOver = true; +} + +static void RotatePiece(void) +{ + uint8_t Temp[4][4]; + uint8_t Save[4][4]; + uint8_t x, y; + + for (y = 0; y <= 3; y++) + for (x = 0; x <= 3; x++) + Save[y][x] = Shapes[CurrentPiece][y][x]; + for (y = 0; y <= 3; y++) + for (x = 0; x <= 3; x++) + Temp[y][x] = Shapes[CurrentPiece][3 - x][y]; + for (y = 0; y <= 3; y++) + for (x = 0; x <= 3; x++) + Shapes[CurrentPiece][y][x] = Temp[y][x]; + if (!CanMove(0, 0)) + for (y = 0; y <= 3; y++) + for (x = 0; x <= 3; x++) + Shapes[CurrentPiece][y][x] = Save[y][x]; +} + +static void PlacePiece(void) +{ + uint8_t x, y; + + for (y = 0; y <= 3; y++) { + for (x = 0; x <= 3; x++) { + if (Shapes[CurrentPiece][y][x] == 1) + Board[PosY + y - 1][PosX + x - 1] = CurrentPiece; + } + } +} + +static bool ClearLines(void) +{ + uint8_t x, y, ny; + bool Full; + bool Redraw = false; + + for (y = 1; y <= BoardHeight; y++) { + Full = true; + for (x = 0; x < BoardWidth; x++) { + if (Board[y-1][x] == 0xFF) + Full = false; + } + if (Full) { + for (ny = y; ny >= 2; ny--) { + for (x = 0; x < BoardWidth; x++) + Board[ny-1][x] = Board[ny-2][x]; + } + for (x = 0; x < BoardWidth; x++) + Board[0][x] = 0xFF; + Redraw = true; + } + } + return Redraw; +} + +static void HandleInput(void) +{ + uint8_t c; + + c = screen_getchar(Delay); + if (c > 0) { + switch (c) { + case 'A': case 'a': case SCREEN_KEY_LEFT: + if (CanMove(-1, 0)) { + DrawPiece(CurrentPiece, PosX, PosY, true); + PosX--; + DrawPiece(CurrentPiece, PosX, PosY, false); + } + break; + + case 'D': case 'd': case SCREEN_KEY_RIGHT: + if (CanMove(1, 0)) { + DrawPiece(CurrentPiece, PosX, PosY, true); + PosX++; + DrawPiece(CurrentPiece, PosX, PosY, false); + } + break; + + case 'S': case 's': case SCREEN_KEY_DOWN: + if (CanMove(0, 1)) { + DrawPiece(CurrentPiece, PosX, PosY, true); + PosY++; + DrawPiece(CurrentPiece, PosX, PosY, false); + } + break; + + case ' ': + while (CanMove(0, 1)) { + DrawPiece(CurrentPiece, PosX, PosY, true); + PosY++; + DrawPiece(CurrentPiece, PosX, PosY, false); + } + break; + + case 'W': case 'w': case SCREEN_KEY_UP: + DrawPiece(CurrentPiece, PosX, PosY, true); + RotatePiece(); + DrawPiece(CurrentPiece, PosX, PosY, false); + break; + + case 'P': case 'p': + screen_setcursor(12,7); + cpm_printstring("Game paused."); + screen_setcursor(12,8); + cpm_printstring("Press any key to continue."); + screen_waitchar(); + screen_clear(); + DrawBoard(); + DrawNextPiece(); + break; + + case 'Q': case 'q': + GameOver = true; + break; + } + } +} + +int main(void) +{ + if(!screen_init()) { + cpm_printstring("No SCREEN driver, exiting\n\r"); + cpm_warmboot(); + } + screen_clear(); + screen_showcursor(0); + + srand( 21342 ); + InitializeBoard(); + DrawBoard(); + GameOver = false; + NextPiece = (uint8_t)rand() % NofShapes; + NewPiece(); + while (!GameOver) { + DrawPiece(CurrentPiece, PosX, PosY, false); + HandleInput(); + if (CanMove(0, 1)) { + DrawPiece(CurrentPiece, PosX, PosY, true); + PosY++; + DrawPiece(CurrentPiece, PosX, PosY, false); + } else { + PlacePiece(); + while (ClearLines()) { + DrawBoard(); + } + NewPiece(); + } + if (PosY >= BoardHeight) + GameOver = true; + } + screen_clear(); + printf("Thanks for playing.\n"); + screen_showcursor(1); + + cpm_warmboot(); +} diff --git a/third_party/tetris2/tetris2.pas b/third_party/tetris2/tetris2.pas new file mode 100644 index 00000000..370c071d --- /dev/null +++ b/third_party/tetris2/tetris2.pas @@ -0,0 +1,272 @@ +(* Simple Tetris in the terminal, for CP/M 2.2 Apple ][ *) +(* A.Baumann, 13.12.2024, 0BSD clause *) + +PROGRAM Tetris; + +CONST + DelayTicks = 40; + BoardHeight = 20; + BoardWidth = 10; + NofShapes = 7; + Shapes : ARRAY[1..7,0..3,0..3] OF Byte = + (((0,0,0,0),(1,1,1,1),(0,0,0,0),(0,0,0,0)), (* I *) + ((0,1,1,0),(0,1,1,0),(0,0,0,0),(0,0,0,0)), (* O *) + ((0,1,0,0),(1,1,1,0),(0,0,0,0),(0,0,0,0)), (* T *) + ((1,0,0,0),(1,1,1,0),(0,0,0,0),(0,0,0,0)), (* J *) + ((0,0,1,0),(1,1,1,0),(0,0,0,0),(0,0,0,0)), (* L *) + ((0,1,1,0),(1,1,0,0),(0,0,0,0),(0,0,0,0)), (* S *) + ((1,1,0,0),(0,1,1,0),(0,0,0,0),(0,0,0,0))); (* Z *) + ShapeChars : ARRAY[0..7] OF Char = + (' ','I','O','T','J','L','S','Z'); + +VAR + Board : ARRAY[1..BoardHeight,1..BoardWidth] OF Byte; + GameOver : Boolean; + CurrentPiece, NextPiece : Integer; + PosX, PosY : Integer; + +PROCEDURE InitializeBoard; +BEGIN + FillChar(Board,SizeOf(Board),0) +END; + +PROCEDURE DrawBoard; +VAR + x, y : Integer; +BEGIN + FOR y := 1 TO BoardHeight DO + BEGIN + FOR x := 1 TO BoardWidth DO + BEGIN + GotoXY(x,y); + IF Board[y,x] >= 1 THEN + Write(ShapeChars[Board[y,x]]) + ELSE + Write('.') + END + END +END; + +PROCEDURE DrawPiece(Piece : Integer; x, y : Integer; Erase : Boolean); +VAR + nx, ny : Integer; +BEGIN + FOR ny := 0 TO 3 DO + FOR nx := 0 TO 3 DO + IF Shapes[Piece,ny,nx] = 1 THEN + BEGIN + GotoXY(nx+x,ny+y); + IF Erase THEN + Write('.') + ELSE + Write(ShapeChars[Piece]) + END +END; + +FUNCTION ReadKey : Char; +BEGIN + ReadKey := Chr(BDOS(6,$FF)) +END; + +FUNCTION CanMove(dx, dy : Integer) : Boolean; +VAR + x, y : Integer; +BEGIN + CanMove := True; + FOR y := 0 TO 3 DO + FOR x := 0 TO 3 DO + IF Shapes[CurrentPiece,y,x] = 1 THEN + BEGIN + IF (PosX+x+dx<1) OR (PosX+x+dx>BoardWidth) OR + (PosY+y+dy>BoardHeight) THEN + BEGIN + CanMove := False; + Exit + END; + IF (Board[PosY+y+dy,PosX+x+dx]>=1) THEN + BEGIN + CanMove := False; + Exit + END + END +END; + +PROCEDURE DrawNextPiece; +VAR + x, y : Integer; +BEGIN + FOR y := 10 TO 13 DO + FOR x := 12 TO 15 DO + BEGIN + GotoXY(x,y); + Write('.') + END; + DrawPiece(NextPiece,12,10,False); +END; + +PROCEDURE NewPiece; +BEGIN + CurrentPiece := NextPiece; + NextPiece := 1+Random(NofShapes); + DrawNextPiece; + PosX := (BoardWidth div 2)-2; + PosY := 1; + IF NOT CanMove(0,0) THEN + GameOver := True +END; + +PROCEDURE RotatePiece; +VAR + Temp, Save : ARRAY[0..3,0..3] OF Byte; + x, y : Integer; +BEGIN + FOR y := 0 TO 3 DO + FOR x := 0 TO 3 DO + Save[y,x] := Shapes[CurrentPiece,y,x]; + FOR y := 0 TO 3 DO + FOR x := 0 TO 3 DO + Temp[y,x] := Shapes[CurrentPiece,3-x,y]; + FOR y := 0 TO 3 DO + FOR x := 0 TO 3 DO + Shapes[CurrentPiece,y,x] := Temp[y,x]; + IF NOT CanMove(0,0) THEN + FOR y := 0 TO 3 DO + FOR x := 0 TO 3 DO + Shapes[CurrentPiece,y,x] := Save[y,x]; +END; + +PROCEDURE PlacePiece; +VAR + x, y : Integer; +BEGIN + FOR y := 0 TO 3 DO + FOR x := 0 TO 3 DO + IF Shapes[CurrentPiece,y,x] = 1 THEN + Board[PosY+y,PosX+x] := CurrentPiece +END; + +FUNCTION ClearLines : Boolean; +VAR + x, y, ny : Integer; + Full : Boolean; +BEGIN + ClearLines := False; + FOR y := 1 TO BoardHeight DO + BEGIN + Full := True; + FOR x := 1 TO BoardWidth DO + IF Board[y,x] = 0 THEN + BEGIN + Full := False + END; + IF Full THEN + BEGIN + FOR ny := y DOWNTO 2 DO + FOR x := 1 TO BoardWidth DO + Board[ny,x] := Board[ny-1,x]; + FOR x := 1 TO BoardWidth DO + Board[1,x] := 0; + ClearLines := True + END + END; +END; + +PROCEDURE HandleInput; +VAR + c : Char; + i : Integer; +BEGIN + i := 0; + WHILE i