eris2010

Documentation: http://frombelow.net/projects/eris2010/
Clone: git clone https://git.frombelow.net/eris2010.git
Log | Files | Refs | Submodules | README | LICENSE

commit 3fceb94389d2d61507b1aeadb13b5a7763772f3f
parent c2522876d58e4fda99b87644fca9f362dad13db4
Author: Gerd Beuster <gerd@frombelow.net>
Date:   Sun, 25 Oct 2020 12:49:24 +0100

Tic-Tac-Toe

Diffstat:
Mdoc/Documentation.txt | 3+++
Mroms/boot/Makefile | 3++-
Mroms/boot/boot.h | 68+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mroms/boot/boot.s | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Msw/10print/10print.s | 4++--
Asw/ttt/Makefile | 33+++++++++++++++++++++++++++++++++
Asw/ttt/README.txt | 26++++++++++++++++++++++++++
Asw/ttt/board.s | 589+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asw/ttt/board_test.s | 143+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asw/ttt/computer_player.s | 755+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asw/ttt/computer_player_test.s | 582++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asw/ttt/human_player.s | 53+++++++++++++++++++++++++++++++++++++++++++++++++++++
Asw/ttt/ttt.s | 159+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
13 files changed, 2491 insertions(+), 23 deletions(-)

diff --git a/doc/Documentation.txt b/doc/Documentation.txt @@ -56,6 +56,9 @@ transmission is the number of $100 byte blocks to be transmitted. Run rom/boot/boot.py on the host PC to upload a program. This python script triggers a reset by setting DTR to high; see section Reset Logic. +If no initial byte is recevied, the boot ROM starts to execute a +standard application from ROM. + ** Tools Directory misc/ contains some tools and unfinished parts: diff --git a/roms/boot/Makefile b/roms/boot/Makefile @@ -1,6 +1,7 @@ TARGET=boot.bin TARGET_BASENAME=$(basename $(TARGET)) SYMON=java -jar ../../emulator/symon-1.3.1.jar -cpu 65c02 +INC=-I../../sw/ttt/ all: $(TARGET) $(TARGET_BASENAME)_symon.bin liba.h @@ -8,7 +9,7 @@ liba.h: $(TARGET_BASENAME).l $(TARGET_BASENAME)_symon.l $(TARGET_BASENAME).s ./export_symbols.py %.bin: %.s - xa -l "$(basename $@).l" -r -o "$@" "$<" + xa $(INC) -l "$(basename $@).l" -r -o "$@" "$<" %_symon.bin: %.s xa -DSYMON -l "$(basename $@).l" -r -o "$@" "$<" diff --git a/roms/boot/boot.h b/roms/boot/boot.h @@ -16,7 +16,7 @@ gets_str = $11 ; 16 bit address ;;; Output: ;;; - ;;; Changes: -;;; a, acai-registers +;;; a, acai registers #ifdef SYMON acia_base = $8800 ; Symon @@ -28,6 +28,29 @@ gets_str = $11 ; 16 bit address acia_cmd_reg = acia_base + 2 acia_ctrl_reg = acia_base + 3 +;;; getc +;;; Read character from acia. Blocks until character is read +;;; Input: +;;; - +;;; Output: +;;; a: +;;; Character read +;;; Changes: +;;; a, acai registers + +;;; getc_seed_rng +;;; Like getc, but also updates lsfr +;;; This is our only source of randomness right now: +;;; While we are busy waiting for a character, lsfr is +;;; updated. +;;; Input: +;;; - +;;; Output: +;;; a: +;;; Character read +;;; Changes: +;;; a, acai registers, lsfr_state + ;;; puts ;;; Send up to 256 characters terminated by null via acia ;;; Input: @@ -47,7 +70,7 @@ gets_str = $11 ; 16 bit address ;;; Output: ;;; - ;;; Changes: -;;; x, acai-registers +;;; x, acai registers ;;; puth ;;; Convert byte to hex and send via acia @@ -57,7 +80,7 @@ gets_str = $11 ; 16 bit address ;;; Output: ;;; - ;;; Changes: -;;; a, x, acai-registers, c-flag +;;; a, x, acai registers, c-flag ;;; puth_nibble ;;; Convert lower half of byte to hex and send via acia @@ -67,7 +90,7 @@ gets_str = $11 ; 16 bit address ;;; Output: ;;; - ;;; Changes: -;;; a, x, acai-registers, c-flag +;;; a, x, acai registers, c-flag ;;; putnl ;;; Send newline via acia @@ -97,7 +120,25 @@ gets_str = $11 ; 16 bit address ;;; Changes: ;;; a, y, ACAI registers +;;; lfsr_init +;;; Initialize lfsr +;;; Input: +;;; - +;;; Output: +;;; - +;;; Changes: +;;; lfsr + +;;; lfsr_step +;;; update lfsr +;;; Input: +;;; - +;;; Output: +;;; - +;;; Changes: +;;; lfsr + ;;; Macros ;;; PRINT(addr) @@ -156,7 +197,16 @@ gets_str = $11 ; 16 bit address ;;; Output: ;;; - ;;; Changes: -;;; a, x, acai-registers +;;; a, x, acai registers + +;;; STORE_WORD(word,dst) +;;; Store word at dst +;;; Input: +;;; word +;;; Output: +;;; - +;;; Changes: +;;; a, dst #define PRINT(addr) \ .(: \ @@ -200,5 +250,13 @@ cont: \ lda #$0a: \ jsr putc +#define STORE_WORD(word,dst) \ + .(: \ + lda #<word: \ + sta dst: \ + lda #>word: \ + sta dst+1: \ + .) + ;;; The lines below are automatically generated by export_symbols.py diff --git a/roms/boot/boot.s b/roms/boot/boot.s @@ -8,6 +8,11 @@ * = $e000 #endif + +;;; We have enough space left to +;;; fit in a Tic-Tac-Toe engine! + start_ttt = * + $0c00 + ;;; ---------------------------------------------------------- ;;; RESET ;;; @@ -18,16 +23,39 @@ init: cld jsr init_acia + jsr check_for_program_download + bcs no_program_download jmp download_program +no_program_download: + ;; No program download? + ;; Then let's play some + ;; Tic-Tac-Toe! + jmp start_ttt -;;; Downalod program to RAM and start executing it. -;;; The first byte transmitted is the number of $100 byte -;;; blocks to receive. Then the program is received. -;;; Finally, a two byte checksum is send back. The first -;;; byte of the checksum is the addition of all bytes received. -;;; The second byte of the checksum is the xor of all bytes -;;; received. - +;;; If we receive a byte right after a reset, this +;;; triggers a program download via serial line. +;;; The value of the byte is the number of $100 byte +;;; blocks to receive. +check_for_program_download: + .( + PRINTSNL("READY!") + ldy #$20 ; Outer loop counter + ldx #$00 ; Inner counter +loop: + ;; Try to get number of 0x100 byte blocks to be read + jsr getc_nonblocking + bcc got_byte + dex + bne loop + dey + bne loop + PRINTSNL("Nothing to download.") + sec +got_byte: + rts + .) + + ;; Program is loaded to this memory address start_addr = $0300 ;; Location of temporary variables @@ -36,11 +64,10 @@ init: add_checksum = $15 xor_checksum = $16 download_program: - PRINTSNL("READY!") - ;; Get number of 0x100 byte blocks to be read - jsr getc + ;; We expect the number of $100 blocks to be + ;; downloaded in A. ;; The msb of the last block to be written is the msb - ;; of the start address plus the number of blocks + ;; of the start address plus the number of blocks. clc adc #>start_addr sta msb_last_addr @@ -168,6 +195,34 @@ getc: ; EXPORT lda acia_data_reg rts +getc_seed_rng: ; EXPORT + ;; Read character from acia + ;; We also use the time between keystrokes + ;; as entropy source for the RNG + jsr lfsr_step + lda acia_status_reg + and #%00001000 + beq getc_seed_rng + lda acia_data_reg + rts + +getc_nonblocking: ; EXPORT + .( + ;; Non-blocking read: If + ;; character has been read, + ;; it is returned in A and + ;; C is cleared. If not, + ;; C is set + sec + lda acia_status_reg + and #%00001000 + beq nothing_read + clc + lda acia_data_reg +nothing_read: + rts + .) + gets: ; EXPORT .( ;; Read string terminated by CR @@ -193,14 +248,14 @@ terminate_string: ;;; bit from the state yields random numbers with reasonable ;;; statistical properties. lfsr_seed = $beef -init_lfsr: ; EXPORT +lfsr_init: ; EXPORT .( lda #<lfsr_seed sta lfsr_state lda #>lfsr_seed sta lfsr_state+1 .) -lfsr: ; EXPORT +lfsr_step: ; EXPORT .( asl lfsr_state lda lfsr_state+1 @@ -231,7 +286,18 @@ loop: .) #endif // TESTS - +;;; Include Tic-Tac-Toe binary +;;; This creates some circular dependencies, +;;; because ttt.s includes liba.h, which +;;; is generated from boot.h upon successful +;;; compiliation of boot.s. + .dsb start_ttt-*, $ff +#ifdef SYMON + .bin 0,0,"../../sw/ttt/ttt_boot_symon.bin" +#else + .bin 0,0,"../../sw/ttt/ttt_boot.bin" +#endif + ;;; Vectors .dsb $fffa-*, $ff .word $0000 ; nmi diff --git a/sw/10print/10print.s b/sw/10print/10print.s @@ -4,7 +4,7 @@ init: cld jsr init_acia - jsr init_lfsr + jsr lfsr_init jmp ten_print ten_print: @@ -17,7 +17,7 @@ l0: bne l0 dex bne l1 - jsr lfsr + jsr lfsr_step and #$01 bne slash PRINTS('╲') diff --git a/sw/ttt/Makefile b/sw/ttt/Makefile @@ -0,0 +1,33 @@ +TARGET=ttt.bin +TARGET_BASENAME=$(basename $(TARGET)) + +all: $(TARGET) $(TARGET_BASENAME)_boot.bin \ + $(TARGET_BASENAME)_symon.bin $(TARGET_BASENAME)_boot_symon.bin \ + $(TARGET_BASENAME)_test.bin $(TARGET_BASENAME)_symon_test.bin + +%.bin: %.s + xa -l "$(basename $@).l" -r -o "$@" "$<" + +%_boot.bin: %.s + xa -DBOOT -l "$(basename $@).l" -r -o "$@" "$<" + +%_test.bin: %.s + xa -DRUN_TESTS -l "$(basename $@).l" -r -o "$@" "$<" + +%_symon.bin: %.s + xa -DSYMON -l "$(basename $@).l" -r -o "$@" "$<" + +%_boot_symon.bin: %.s + xa -DBOOT -DSYMON -l "$(basename $@).l" -r -o "$@" "$<" + +%_symon_test.bin: %.s + xa -DRUN_TESTS -DSYMON -l "$(basename $@).l" -r -o "$@" "$<" + +upload: $(TARGET) + ../../roms/boot/boot.py $(TARGET) + +upload_test: $(TARGET_BASENAME)_test.bin + ../../roms/boot/boot.py $(TARGET_BASENAME)_test.bin + +clean: + rm -f *.bin *.l diff --git a/sw/ttt/README.txt b/sw/ttt/README.txt @@ -0,0 +1,26 @@ +* Targets + +ttt.bin + is the binary to be uploaded to address $300 on the GATE 2010 +ttt_symon.bin + is the binary for Symon (ROM & ACIA at different memory addresses) +*_boot.bin + are binaries to be included into the boot ROM of GATE 2010 and Symon +*_test.bin + are binaries running self-tests on GATE 2010 and Symon + +* Source Code + +ttt.s + Contains the main routine +board.s + Subroutines for managing the board +board_test.s + Tests for board.s +computer_player.s + Implementations of random and perfect computer players +computer_player_test.s + Tests for computer_player.s +human_player.s + CLI for human players + diff --git a/sw/ttt/board.s b/sw/ttt/board.s @@ -0,0 +1,589 @@ +;;; This file contains the definitions and subroutines +;;; related to the game boards, like printing the +;;; game board and placing pieces. It also contains +;;; the subroutines to check for a win constallation +;;; on the board, and for playing the game. + +;;; Orchester a game between two players +;;; Each player has an init subroutine, called +;;; once at the beginning, and a ply subroutine, +;;; called on every turn of the player. +;;; player_x_init_ptr - Init function of player X +;;; player_x_ply_ptr - Ply function of player X +;;; player_o_init_ptr - Init function of player O +;;; player_o_ply_ptr - Ply function of player O +play_game: + .( + jsr init_board + ;; jsr does not allow indirect addressing, + ;; threfore we have to wrap the indirect + ;; calls this way. + jsr player_x_init + jsr player_o_init +play_game_loop: + lda #piece_x + jsr next_ply + bne game_finished + lda #piece_o + jsr next_ply + bne game_finished + jmp play_game_loop +game_finished: + pha + jsr print_board + pla + jsr print_game_state + PRINTNL + rts +next_ply: + cmp #piece_x + bne ply_player_o + ;; Player X's ply + ;; jsr does not allow indirect addressing, + ;; threfore we have to wrap the indirect + ;; calls this way. + jsr player_x_ply + jmp cont +ply_player_o: + jsr player_o_ply +cont: + jsr get_game_state + cmp #$ff + rts +player_x_init: + jmp (player_x_init_ptr) +player_o_init: + jmp (player_o_init_ptr) +player_x_ply: + jmp (player_x_ply_ptr) +player_o_ply: +before_o: + jmp (player_o_ply_ptr) +after_o: + .) + +;;; We store the Tic-Tac-Toe board in three +;;; bytes, one for each row. When passing +;;; a board on the stack, the first row is +;;; pushed first, followed by the second, +;;; and third. +;;; +;;; Each field in a row is represented by +;;; two bits: +;;; %00 - Field unoccupied +;;; %01 - Field occupied by player "X" +;;; %10 - Field occupied by player "O" + +piece_none = %00 +piece_x = %01 +piece_o = %10 +;;; +;;; In the byte for a row, the first +;;; two bits represent the leftmost field. +;;; The last two bits are not used. +;;; +;;; A contains the piece +;;; X contains the position (0-8) +;;; The location of the board is read +;;; as a 16 bit address from address +;;; board. + + +;;; Initialize and clear the board. +;;; When init_board is called, the +;;; "standard" board location as given +;;; in variable board is transferred +;;; to board_ptr before clearing the +;;; board. +;;; If clear_board is called, the +;;; board at board_ptr is cleared +;;; without changing board_ptr. +init_board: + lda #<main_board + sta board_ptr + lda #>main_board + sta board_ptr+1 +clear_board: + // Clear board + lda #$00 + ldy #$00 + sta (board_ptr),y + iny + sta (board_ptr),y + iny + sta (board_ptr),y + rts + +mirror_board: + .( + ;; Mirror board on the diagonal axis + ;; such that we can + ;; use the win_row subroutine. + ;; First row to column + ldy #$00 + lda (board_ptr),y + and #%00000011 + sta (other_board_ptr),y + lda (board_ptr),y + and #%00001100 + lsr + lsr + ldy #$01 + sta (other_board_ptr),y + ldy #$00 + lda (board_ptr),y + and #%00110000 + lsr + lsr + lsr + lsr + ldy #$02 + sta (other_board_ptr),y + ;; Second row to column + ldy #$01 + lda (board_ptr),y + and #%00000011 + asl + asl + ldy #$00 + ora (other_board_ptr),y + sta (other_board_ptr),y + ldy #$01 + lda (board_ptr),y + and #%00001100 + ora (other_board_ptr),y + sta (other_board_ptr),y + ldy #$01 + lda (board_ptr),y + and #%00110000 + lsr + lsr + ldy #$02 + ora (other_board_ptr),y + sta (other_board_ptr),y + ;; Third column to row + lda (board_ptr),y + and #%00000011 + asl + asl + asl + asl + ldy #$00 + ora (other_board_ptr),y + sta (other_board_ptr),y + ldy #$02 + lda (board_ptr),y + and #%00001100 + asl + asl + ldy #$01 + ora (other_board_ptr),y + sta (other_board_ptr),y + ldy #$02 + lda (board_ptr),y + and #%00110000 + ora (other_board_ptr),y + sta (other_board_ptr),y + rts + .) + +;;; Set a field on the board. +;;; A is the field, X the position. +;;; This subroutine changes A, X, and Y. +set_field: + ;; Convert position on X + ;; to column/row in X/Y. + jsr pos_to_column_row + ;; Set field based on X/Y coordinates +set_field_x_y: + .( + cpx #$00 + beq change_board + asl + asl + dex + jmp set_field_x_y +change_board: + ora (board_ptr),y + sta (board_ptr),y + rts + .) + +;;; Get value of field on the board. +;;; X is the position. Field value is +;;; returned in A. +get_field: + .( + ;; Convert position on X + ;; to column/row in X/Y. + jsr pos_to_column_row + lda (board_ptr),y +field_loop: + cpx #$00 + beq got_field + lsr + lsr + dex + jmp field_loop +got_field: + and #%11 + rts + .) + +;;; Convert position in X +;;; register to row/column +;;; coordinates. +;;; X becomes the column, +;;; Y the row +pos_to_column_row: + .( + ldy #$00 +pos_loop: + cpx #$03 + bcc pos_found + iny + dex + dex + dex + jmp pos_loop +pos_found: + rts + .) + + +copy_field: + ;; Copy field X from board_ptr + ;; to field Y of other_board_ptr + ;; Save variables + lda board_ptr + pha + lda board_ptr+1 + pha + phy + ;; Get value & save ot + jsr get_field + pha + ;; Write value + lda other_board_ptr + sta board_ptr + lda other_board_ptr+1 + sta board_ptr+1 + pla + plx + jsr set_field + ;; Restore variables + pla + sta board_ptr+1 + pla + sta board_ptr + rts + +#define COPY_FIELD(from,to) \ + ldx #from: \ + ldy #to: \ + jsr copy_field + +#define SET_FIELD(piece,pos) \ + lda #piece: \ + ldx #pos: \ + jsr set_field + +#define GET_FIELD(ptr,pos) \ + STORE_WORD(ptr,board_ptr): \ + ldx #pos: \ + jsr get_field + +;;; Print Tic-Tac-Toe board. +;;; Note that the printed +;;; numbers start with 1, while +;;; the internal counting of +;;; fields starts with 0. +print_board: + .( + ldx #$ff + phx + PRINTSNL('+---+---+---+') +print_field: + lda #$03 + sta tmp ; Print each line 3 times +print_line: + lda #'|' + jsr putc + plx + inx + phx + jsr get_field + jsr piece_to_ascii + jsr putc + plx + phx + jsr put_field_number_if_empty_and_middle + jsr putc + plx + phx + cpx #$02 + beq print_row_seperator + cpx #$05 + beq print_row_seperator + cpx #$08 + beq print_row_seperator +check_if_all_rows_printed: + plx + phx + cpx #$08 + bne print_line + plx ; Clean-up stack + rts +print_row_seperator: + PRINTSNL('|') + dec tmp ; Check if each line has been printed 3 times + lda tmp + beq print_seperator + pla ; If not, + sec ; print + sbc #$03 ; line + pha + jmp print_line ;again. +print_seperator: + PRINTSNL('+---+---+---+') + lda #$03 + sta tmp + jmp check_if_all_rows_printed +put_field_number_if_empty_and_middle: + pha + cmp #' ' + bne print_verbatim + ;; Check if we are in the + ;; middle line + lda tmp + cmp #$02 + bne print_verbatim + ;; Middle of empty field; + ;; print field number + txa + clc + adc #'1' + jsr putc + pla + rts +print_verbatim: + pla + jmp putc + .) + + + +piece_to_ascii: + .( + cmp #piece_x + bne not_x + lda #'#' + rts +not_x: + cmp #piece_o + bne not_o + lda #':' + rts +not_o: + lda #' ' + rts + .) + +;;; Check if a player won. +;;; Returns piece_x or piece_y +;;; if one of the player won, +;;; piece_none in case of a draw, +;;; and #$ff in case the game +;;; is not finished yet. + piece_x_row = (piece_x << 4) + (piece_x << 2) + piece_x + piece_o_row = (piece_o << 4) + (piece_o << 2) + piece_o +get_game_state: + .( + lda #piece_x_row + jsr check_player_won + bne done + lda #piece_o_row + jsr check_player_won + bne done + jsr check_draw +done: + rts + .) + +;;; Check if a certain player one. +;;; This subroutine expects a complete +;;; row of pieces of the player to be +;;; checked for winning in A as input. +check_player_won: + .( + sta tmp ; Store pattern for following checks + jsr check_win_row + bne done + lda tmp + jsr check_win_column + bne done + lda tmp + jsr check_win_first_diagonal + bne done + lda tmp + jsr check_win_second_diagonal +done: + rts + .) + +;;; Check for three in a row. +;;; Expects the row pattern of +;;; three identical pieces in A. +check_win_row: + .( + ldy #$00 +loop: + cmp (board_ptr),y + beq player_won + iny + cpy #$03 + bne loop + ;; No winning rows + lda #$00 + rts +player_won: + lda tmp + and #%00000011 + rts + .) + +;;; Check for three in a column. +;;; Expects the row(!) pattern of +;;; three identical pieces in A. +check_win_column: + .( + and #%00110000 ; Field (column) pattern + tax ; We use X as temporary memory + ldy #$00 +loop: + and (board_ptr),y ; Piece in column? + beq check_next_column ; If not, check next column + iny ; If yes, advance to next row + cpy #$03 ; Until all three rows + bne loop ; have been checked +player_won: + lda tmp + and #%00000011 + rts +check_next_column: + ldy #$00 ; Restore row counter + txa ; Old field (column) pattern + lsr ; Change to + lsr ; next column + tax ; and save it in temporary memory + bne loop + ;; No winning columns + lda #$00 + rts + .) + +check_win_first_diagonal: + .( + and #%00110000 ; Start with last field (column) + tax ; Store pattern in x + ldy #$00 ; Row number stored in y +loop: + and (board_ptr),y + beq no_win_first_diagonal + txa ; Move + lsr ; field + lsr ; (column) + tax ; pattern + iny ; and row by one + cpy #$03 + bne loop + ;; Player won + ;; Return player piece + lda tmp + and #%00000011 + rts +no_win_first_diagonal: + lda #$00 + rts + .) + +check_win_second_diagonal: + .( + and #%00000011 ; Start with first field (column) + tax ; Store pattern in x + ldy #$00 ; Row number stored in y +loop: + and (board_ptr),y + beq no_win_second_diagonal + txa ; Move + asl ; field + asl ; (column) + tax ; pattern + iny ; and row by one + cpy #$03 + bne loop + ;; Player won + ;; Return player piece + lda tmp + and #%00000011 + rts +no_win_second_diagonal: + lda #$00 + rts + .) + +check_draw: + .( + ldy #$00 ; Row +row_loop: + ldx #%00110000 ; Field (column) pattern + txa ; Store pattern in x +col_loop: + and (board_ptr),y ; Check + beq no_draw ; all + txa ; fields + lsr ; for + lsr ; emptiness + tax + bne col_loop + iny + cpy #$03 + bne row_loop + ;; Draw + lda #piece_none + rts +no_draw: + lda #$ff + rts + .) + + + +;;; This subroutine can be called +;;; called after get_game_state +;;; in order to print the state +;;; of the game in human-friendly +;;; format to the acai. +print_game_state: + .( + cmp #piece_x + beq win + cmp #piece_o + beq win + cmp #piece_none + bne no_draw + PRINTSNL("Draw!") + rts +no_draw: + PRINTSNL("Let the game continue!") + rts +win: + pha + PRINTS("Player ") + pla + clc + adc #'0' + jsr putc + PRINTSNL(" wins!") + rts + .) diff --git a/sw/ttt/board_test.s b/sw/ttt/board_test.s @@ -0,0 +1,143 @@ +;;; Expected value is given in res. +;;; A is compared to this value. +#define CHECK_ERROR(res) \ + .(: \ + cmp #res: \ + beq no_error: \ + PRINTSNL("Error"): \ +stop_loop: \ + jmp stop_loop: \ +no_error: \ + .) + +#define PLAYER_WIN_ROW(player,row_start) \ + .(: \ + jsr init_board: \ + ldx #row_start: \ + lda #player: \ + jsr set_field: \ + ldx #(row_start+1): \ + lda #player: \ + jsr set_field: \ + ldx #(row_start+2): \ + lda #player: \ + jsr set_field: \ + jsr print_board: \ + jsr get_game_state: \ + CHECK_ERROR(player): \ + jsr print_game_state: \ + .) + +#define PLAYER_WIN_COLUMN(player,col) \ + .(: \ + jsr init_board: \ + lda #player: \ + ldx #col: \ + jsr set_field: \ + lda #player: \ + ldx #(col+3): \ + jsr set_field: \ + lda #player: \ + ldx #(col+6): \ + jsr set_field: \ + jsr print_board: \ + jsr get_game_state: \ + CHECK_ERROR(player): \ + jsr print_game_state: \ + .) + +#define PLAYER_WIN_SECOND_DIAGONAL(player) \ + jsr init_board: \ + lda #player: \ + ldx #$00: \ + jsr set_field: \ + lda #player: \ + ldx #$04: \ + jsr set_field: \ + lda #player: \ + ldx #$08: \ + jsr set_field: \ + jsr print_board: \ + jsr get_game_state: \ + CHECK_ERROR(player): \ + jsr print_game_state + +#define PLAYER_WIN_FIRST_DIAGONAL(player) \ + jsr init_board: \ + lda #player: \ + ldx #$02: \ + jsr set_field: \ + lda #player: \ + ldx #$04: \ + jsr set_field: \ + lda #player: \ + ldx #$06: \ + jsr set_field: \ + jsr print_board: \ + jsr get_game_state: \ + CHECK_ERROR(player): \ + jsr print_game_state + +;;; Place some pieces +;;; and print the field. +game_board_test: + .( + ;; + ;; Unfinished game. + ;; + jsr init_board + // Place "X" pieces + SET_FIELD(piece_x,$00) + SET_FIELD(piece_x,$01) + SET_FIELD(piece_x,$08) + // Place "O" pieces + SET_FIELD(piece_o,$04) + SET_FIELD(piece_o,$05) + SET_FIELD(piece_o,$06) + ;; Print board + jsr print_board + jsr get_game_state + CHECK_ERROR($ff) + jsr print_game_state + ;; + ;; Finished game. + ;; + jsr init_board + SET_FIELD(piece_x,$00) + SET_FIELD(piece_x,$01) + SET_FIELD(piece_o,$02) + SET_FIELD(piece_o,$03) + SET_FIELD(piece_o,$04) + SET_FIELD(piece_x,$05) + SET_FIELD(piece_x,$06) + SET_FIELD(piece_x,$07) + SET_FIELD(piece_o,$08) + jsr print_board + jsr get_game_state + CHECK_ERROR(piece_none) + jsr print_game_state + ;; Let each player win in + ;; each row + PLAYER_WIN_ROW(piece_x,$00) + PLAYER_WIN_ROW(piece_x,$03) + PLAYER_WIN_ROW(piece_x,$06) + PLAYER_WIN_ROW(piece_o,$00) + PLAYER_WIN_ROW(piece_o,$03) + PLAYER_WIN_ROW(piece_o,$06) + ;; Let each player win in + ;; each column + PLAYER_WIN_COLUMN(piece_x,$00) + PLAYER_WIN_COLUMN(piece_x,$01) + PLAYER_WIN_COLUMN(piece_x,$02) + PLAYER_WIN_COLUMN(piece_o,$00) + PLAYER_WIN_COLUMN(piece_o,$01) + PLAYER_WIN_COLUMN(piece_o,$02) + ;; Let each player win in + ;; each diagonal + PLAYER_WIN_SECOND_DIAGONAL(piece_x) + PLAYER_WIN_FIRST_DIAGONAL(piece_x) + PLAYER_WIN_SECOND_DIAGONAL(piece_o) + PLAYER_WIN_FIRST_DIAGONAL(piece_o) + PRINTSNL("Board tests completed. No errors found!") + rts + .) diff --git a/sw/ttt/computer_player.s b/sw/ttt/computer_player.s @@ -0,0 +1,755 @@ +;;; ************************************************ +;;; +;;; Random Computer Player +;;; +;;; ************************************************ +computer_random_init: + rts +computer_random_ply: + .( + pha ; Remember who we are + ;; We need a random number + ;; in the range 0..8. For + ;; this, we update the lfsr + ;; and take the lowest + ;; nibble until it is <= 8. +get_random: + jsr lfsr_step + lda lfsr_state + and #$0f + cmp #$09 + bcs get_random + ;; Check if field is occupied + sta tmp + tax + jsr get_field + cmp #piece_none + bne get_random + ;; Occupy field + ldx tmp + pla +#ifdef RUN_TESTS + jsr print_random_ply +#endif + jsr set_field + clc + rts + .) + +;;; ************************************************ +;;; +;;; Perfect Computer Player +;;; +;;; ************************************************ + +;;; See http://csjarchive.cogsci.rpi.edu/1993v17/i04/p0531p0561/MAIN.PDF +computer_perfect_init: + rts +computer_perfect_ply: + .( + ;; Random First + ;; Added by gb: Start with a random move on an empty field in + ;; order to create different games + pha + jsr random_first + pla + bcc done + ;; Win + ;; If there is a row, column, or diagonal with two of my pieces + ;; and a blank space, + ;; Then play the blank space (thus winning the game). + pha + jsr win + pla + bcc execute_ply + ;; Block + ;; If there Is a row, column, or diagonal with two of my + ;; opponent's pieces and a blank space, + ;; Then play the blank space (thus blocking a potential win for + ;; my opponent). + pha + jsr block + pla + bcc execute_ply + ;; Fork + ;; If there are two intersecting rows, columns, or diagonals + ;; with one of my pieces and two blanks, and + ;; If the intersecting space Is empty, + ;; Then move to the intersecting space (thus creating two + ;; ways to win on my next turn). + pha + jsr fork + pla + bcc execute_ply + ;; Block Fork + ;; If there are two intersecting rows, columns, or diagonals + ;; with one of my opponent’s pieces ond two blanks, and + ;; If the intersecting space is empty, + ;; Then + ;; If there is an empty location that creates a + ;; two-in-a-row for me (thus forcing my opponent to + ;; block rather than fork), + ;; Then move to the location. + ;; Else move to the Intersection space (thus occupying + ;; the location that my opponent could use to fork). + pha + jsr block_fork + pla + bcc execute_ply + ;; Play Center + ;; If the center is blank, + ;; Then play the center. + pha + jsr play_center + pla + bcc execute_ply + ;; Play Opposite Corner + ;; If my opponent is in a corner, and + ;; If the opposite corner is empty, + ;; Then play the opposite corner. + pha + jsr play_opposite_corner + pla + bcc execute_ply + ;; Play Empty Corner + ;; If there is an empty corner, + ;; Then move to an empty corner. + pha + jsr play_empty_corner + pla + bcc execute_ply + ;; Play Empty Side + ;; If there is an empty side, + ;; Then move to an empty side. + pha + jsr play_empty_side + pla + bcc execute_ply + ;; No rule left; this should not happen. + PRINTSNL('Error') +error: + jmp error +execute_ply: +#ifdef RUN_TESTS + jsr print_ply +#endif + jsr set_field_x_y +done: + rts + .) + +random_first: + .( + sta tmp ; Remember who I am + cmp #piece_x ; If we are x + bne not_first ; + ldy #$00 +row_loop: + lda main_board,y ; Check if + bne not_first ; all + iny ; rows + cpy #$03 ; are + bne row_loop ; empty + lda tmp + jmp computer_random_ply +not_first: + sec + rts + .) + +win: + .( + pha + jsr win_row + pla + bcc done + pha + jsr win_column + pla + bcc done + pha + jsr win_first_diagonal + pla + bcc done + pha + jsr win_second_diagonal + pla +done: + rts + .) + +win_row: + .( + ;; Load complete winning row + cmp #piece_x + bne this_is_o + lda #piece_x_row + jmp win_row_set +this_is_o: + lda #piece_o_row +win_row_set: + sta tmp ; Store win row + ldy #$00 ; Row counter +row_loop: + ldx #$00 ; Column counter + lda tmp + and #%00111100 + cmp (board_ptr),y + beq win_ply + inx + lda tmp + and #%00110011 + cmp (board_ptr),y + beq win_ply + inx + lda tmp + and #%00001111 + cmp (board_ptr),y + beq win_ply + ;; No win here. Try next row + iny + cpy #$03 + bne row_loop + ;; No win row + ;; Carry set indicates failure + sec + rts +win_ply: + ;; Found a winning move + ;; return piece in A, + ;; and position in X/Y. + ;; Clear carry to indicate success. + lda tmp + and #%00000011 + clc + rts + .) + +win_column: + .( + sta tmp ; Remember who I am + ;; We mirror the board in order to use + ;; win_column to check for a winning + ;; position. + STORE_WORD(board_mirrored,other_board_ptr) + jsr mirror_board + STORE_WORD(board_mirrored,board_ptr) + lda tmp ; Who am I? + jsr win_row + STORE_WORD(main_board,board_ptr) + bcs no_win + ;; We found a winning position + ;; since we found the winning + ;; position on the mirrored board, + ;; we have to mirror it back. + phx + tya + tax + ply + clc +no_win: + rts + .) + +win_first_diagonal: + .( + sta tmp ; Remember who I am + lda #$ff ; No empty field + sta empty_field ; found so far + lda #$00 ; Upper left field + jsr check_if_occupied_and_update_empty_field + bcs no_win + lda #$04 ; Middle field + jsr check_if_occupied_and_update_empty_field + bcs no_win + lda #$08 ; Lower right field + jsr check_if_occupied_and_update_empty_field + bcs no_win + ;; We can win! + clc + lda tmp + ldx empty_field + jsr pos_to_column_row + rts +no_win: + sec + rts + .) +win_second_diagonal: + .( + sta tmp ; Remember who I am + lda #$ff ; No empty field + sta empty_field ; found so far + lda #$02 ; Upper right field + jsr check_if_occupied_and_update_empty_field + bcs no_win + lda #$04 ; Middle field + jsr check_if_occupied_and_update_empty_field + bcs no_win + lda #$06 ; Lower left field + jsr check_if_occupied_and_update_empty_field + bcs no_win + ;; We can win! + clc + lda tmp + ldx empty_field + jsr pos_to_column_row + rts +no_win: + sec + rts + .) + + ;; We can complete the first diagonal if + ;; we occupy two of the field already, + ;; and the third field is empty. + ;; This subroutine checks if a field + ;; is occupied by us or if it is empty. + ;; In the latter case, empty_field is + ;; updated unless it was set already + ;; (i.e. this is the second empty field, + ;; thus no win ply available (yet)). +check_if_occupied_and_update_empty_field: + .( + pha ; Field number + tax ; stored in X, + jsr get_field ; field piece + plx ; stored in A. + cmp tmp ; Field occupied + beq cont ; by me? + cmp #$00 ; Field not occupied? + bne no_win ; Occupied by opponent. No win. + lda empty_field ; Have we seen an empty + cmp #$ff ; field before? + bne no_win ; Then we have too many empty fields. + stx empty_field ; Otherwise this is the empty field. +cont: + clc + rts +no_win: + sec + rts + .) + + ;; We use subroutine win to check + ;; if the other player could win +block: + .( + cmp #piece_o + beq we_are_o + lda #piece_o + jmp win +we_are_o: + lda #piece_x + jmp win + .) + +fork: + .( + ;; Forks require two intersecting rows, columns, or diagonals + ;; which both contain only one piece of the player and no + ;; other pieces. If the intersection is empty, then this + ;; is the fork field. + ;; To find these points, we create three additional boards: + ;; On the first board, all rows satisfying the critera + ;; are maintained, while all other rows are filled with $ff. + ;; On the second board, we do the same for columns, + ;; on the third board for the diagonals. + ;; Then we OR the boards pairwise. If there is + ;; an empty field after pairing, then this is the + ;; fork field. + pha ; Remember who I am + STORE_WORD(fork_board_rows,other_board_ptr) + pla ; Store my piece + pha ; in A + jsr find_rows_for_fork + ;; Generate second board (potential fork columns) + ;; For this, we mirror the board and use + ;; find_rows_for_fork again. + ;; We use board_mirrored as an auxillary variable + ;; here. + STORE_WORD(main_board,board_ptr) + STORE_WORD(fork_board_columns,other_board_ptr) + jsr mirror_board + ;; Check for fork points on mirrored + ;; board and store the result in + ;; fork_board_columns + STORE_WORD(fork_board_columns,board_ptr) + STORE_WORD(board_mirrored,other_board_ptr) + pla ; Store my piece + pha ; in A + jsr find_rows_for_fork + ;; Reverse mirroring + STORE_WORD(board_mirrored,board_ptr) + STORE_WORD(fork_board_columns,other_board_ptr) + jsr mirror_board + pla + pha + STORE_WORD(fork_board_rows,board_ptr) + STORE_WORD(fork_board_columns,other_board_ptr) + jsr check_fork + bcs cont + jmp done +cont: + ;; We have generated the fork_boards + ;; for row and column, but there was + ;; no fork on rows and columns. + ;; Now we create the fork board for + ;; diagonals. + ;; In order to use our subroutine + ;; find_rows_for_fork + ;; again, we map the first diagonal + ;; to the first row of a new board, + ;; and the second diagonal to the + ;; second row. + ;; We use board_mirrored as + ;; temporary storage. + ;; First diagonal to first row + lda #$00 + sta fork_board_diagonals + sta fork_board_diagonals+1 + sta fork_board_diagonals+2 + STORE_WORD(main_board,board_ptr) + STORE_WORD(fork_board_diagonals,other_board_ptr) + COPY_FIELD(0,0) + COPY_FIELD(4,1) + COPY_FIELD(8,2) + ;; Second diagonal to second row + COPY_FIELD(2,3) + COPY_FIELD(4,4) + COPY_FIELD(6,5) + STORE_WORD(fork_board_diagonals,board_ptr) + STORE_WORD(board_mirrored,other_board_ptr) + pla ; Store my piece + pha ; in A + jsr find_rows_for_fork + ;; Store result in fork_board_diagonals + ;; Invalidate all non-diagonal fields + lda #%00001100 + sta fork_board_diagonals + sta fork_board_diagonals+2 + lda #%00110011 + sta fork_board_diagonals+1 + ;; First diagonal + STORE_WORD(board_mirrored,board_ptr) + STORE_WORD(fork_board_diagonals,other_board_ptr) + COPY_FIELD(0,0) + COPY_FIELD(1,4) + COPY_FIELD(2,8) + ;; Second diagonal + COPY_FIELD(3,2) + COPY_FIELD(5,6) + ;; Middle field can be used in either diagonal + ;; therefore we have to treat it seperately + GET_FIELD(board_mirrored,4) + cmp #$00 + bne middle_field_not_part_of_potential_fork + STORE_WORD(fork_board_diagonals,board_ptr) + SET_FIELD($00,4) +middle_field_not_part_of_potential_fork: + ;; No check if there is a fork between + ;; rows and diagonals ... + STORE_WORD(fork_board_diagonals,board_ptr) + STORE_WORD(fork_board_rows,other_board_ptr) + jsr check_fork + bcc done + ;; ... and columns and diagonals. + STORE_WORD(fork_board_columns,other_board_ptr) + jsr check_fork +done: + ;; Some restore operatons + STORE_WORD(main_board,board_ptr) + pla + rts + .) + +;;; Check if we can create a fork with a +;;; suitable row and column. If this is +;;; the case, we set the carry flag +;;; and return the fork position. +check_fork: + .( + ;; Since empty fields in + ;; fork_boards_rows and + ;; fork roads_columns indicate potential + ;; fork fields, we OR these two + ;; boards and check if there are + ;; empty fields. + ;; We store the ORed boards in + ;; board_mirrored, because this + ;; board is not used here. + clc + ldy #$00 +copy_and_or_boards_loop: + lda (board_ptr),y + ora (other_board_ptr),y + sta board_mirrored,y + iny + cpy #$03 + bne copy_and_or_boards_loop + ;; No check for empty fields + ldy #$00 ; Row counter +row_loop: + lda #%00000011 ; Field pattern + sta tmp + ldx #$00 ; Column counter +pattern_loop: + lda board_mirrored,y + and tmp + beq found_empty_field + lda tmp ; Move + asl ; to + asl ; next + sta tmp ; pattern + inx + cpx #$03 + bne pattern_loop + iny + cpy #$03 + bne row_loop + sec +found_empty_field: + rts + .) + + +;;; Check for all rows if they may be part +;;; of a fork. This is the case when the +;;; row contains one of our pieces and +;;; no other piece. In this case, the row +;;; stays intact. In all other cases, +;;; we overwrite the row with $ff. +;;; board_ptr points to the input board, +;;; other_board_ptr_to_the output board +find_rows_for_fork: + .( + pha + ldy #$00 ; Row counter +row_loop: + pla ; Get pattern we are looking for + pha + sta tmp + ldx #$00 ; Pattern counter +pattern_loop: + lda (board_ptr),y ; Load row + cmp tmp ; Compare row to pattern + beq potential_fork_row + inx ; Is there + cpx #$03 ; a next pattern? + beq no_potential_fork_row + lda tmp ; Generate next pattern + asl + asl + sta tmp + jmp pattern_loop ; Test for this pattern +no_potential_fork_row: + lda #$ff +potential_fork_row: + sta (other_board_ptr),y + iny + cpy #$03 + bne row_loop + pla ; Clean stack + rts + .) + +block_fork: + .( + pha ; Remember who I am + cmp #piece_o + beq we_are_o + lda #piece_o + jmp check_for_fork +we_are_o: + lda #piece_x +check_for_fork: + jsr fork + bcs done + ;; We found a potential fork for the opponent. + ;; Before occupying it, we check if we could + ;; create a two-in-a-row + ;; situation for me. + pla + pha + jsr two_in_a_row +done: + pla ;Clean up stack + rts + .) +two_in_a_row: + .( + pha ; Remember who I am + ldy #$00 ; Row counter +row_loop: + pla ; Set + pha ; start + sta tmp ; pattern. + ldx #$00 ; Pattern counter +pattern_loop: + lda (board_ptr),y + cmp tmp + beq found_row + lda tmp ; Next pattern + asl + asl + sta tmp + inx + cpx #$03 ; All patterns + bne pattern_loop ; checked for row + iny + cpy #$03 + bne row_loop +done: + pla ; Clean-up stack + sec + rts +found_row: + ;; Found empty spot in row + ;; It is important to place the next piece + ;; on the empty corner field, not the middle field. + pla ; Clean-up stack + clc + lda (board_ptr),y + and #%00000011 + beq first_spot_empty + ;; Since first spot is occupied, + ;; third spot must be empty + ldx #$02 + rts +first_spot_empty: + ldx #$00 + rts + .) + + + +play_center: + .( + GET_FIELD(main_board,4) + sec + cmp #piece_none + bne done + ldx #$01 + ldy #$01 + clc +done: + rts + .) + +play_opposite_corner: + .( + pha ; Remember who I am + ldx #$00 + ldy #$08 + pla + pha + jsr check_if_occupied_and_empty + bcc done + ldx #$02 + ldy #$06 + pla + pha + jsr check_if_occupied_and_empty + bcc done + ldx #$06 + ldy #$02 + pla + pha + jsr check_if_occupied_and_empty + bcc done + ldx #$08 + ldy #$00 + pla + pha + jsr check_if_occupied_and_empty + sec +done: + pla + rts + .) + ;; Check if field in X is + ;; occupied by opponent and + ;; field in Y is empty. + ;; In this case, return + ;; coordinates of empty + ;; field in X/Y +check_if_occupied_and_empty: + .( + phy + sta tmp ; This me + jsr get_field + cmp tmp ; Occupied + beq not_applicable ; by me + cmp #$00 + beq not_applicable ; Field emtpy + ;; Check field (originally) in Y + plx + phx + jsr get_field + cmp #$00 + bne not_applicable ; Not empty + ;; Field is empty. Occupy it + plx + jsr pos_to_column_row + clc + rts +not_applicable: + pla ; Clear stack + sec + rts + .) + +play_empty_corner: + .( + sta tmp + ldx #$00 + jsr occupy_if_empty + bcc done + ldx #$02 + jsr occupy_if_empty + bcc done + ldx #$06 + jsr occupy_if_empty + bcc done + ldx #$08 + jsr occupy_if_empty + bcc done +done: + rts + .) + +occupy_if_empty: + .( + phx + jsr get_field + cmp #$00 + bne already_occupied + plx + jsr pos_to_column_row + clc + rts +already_occupied: + plx ; Clear stack + sec + rts + .) + +play_empty_side: + .( + sta tmp + ldx #$01 + jsr occupy_if_empty + bcc done + ldx #$03 + jsr occupy_if_empty + bcc done + ldx #$05 + jsr occupy_if_empty + bcc done + ldx #$07 + jsr occupy_if_empty + bcc done +done: + rts + .) + diff --git a/sw/ttt/computer_player_test.s b/sw/ttt/computer_player_test.s @@ -0,0 +1,582 @@ +computer_player_test: + // jsr mirror_board_test + // jsr win_by_column + // jsr win_by_diagonal + // jsr block_by_column + // jsr fork_row_column + // jsr test_block_fork + // jsr test_play_opposite_corner + // jsr test_play_empty_corner + // jsr test_perfect_vs_random_computer_player + // jsr test_random_vs_perfect_computer_player + jsr test_perfect_vs_perfect_computer_player +loop: + jmp loop + +#define SET_BOARD(first_row,second_row,third_row) \ + lda #first_row: \ + sta main_board: \ + lda #second_row: \ + sta main_board+1: \ + lda #third_row: \ + sta main_board+2 + +#define CHECK_BOARD(first_row,second_row,third_row) \ + .(: \ + lda board_mirrored: \ + cmp #first_row: \ + bne check_board_err: \ + lda board_mirrored+1: \ + cmp #second_row: \ + bne check_board_err: \ + lda board_mirrored+2: \ + cmp #third_row: \ + beq check_board_cont: \ +check_board_err: \ + jmp error: \ +check_board_cont: \ + .) + +mirror_board_test: + .( + jsr init_board + STORE_WORD(board_mirrored,other_board_ptr) + ;; Test rotation of board + ;; One piece on field 0. + ;; Rotation should change nothing + SET_BOARD(%00000011,%00000000,%00000000) + jsr mirror_board + CHECK_BOARD(%00000011,%00000000,%00000000) + ;; First row to first column + SET_BOARD(%00111001,%00000000,%00000000) + jsr mirror_board + CHECK_BOARD(%00000001,%00000010,%00000011) + ;; Second row to Second column + SET_BOARD(%00000000,%00111001,%00000000) + jsr mirror_board + CHECK_BOARD(%00000100,%00001000,%00001100) + ;; Third row to third column + SET_BOARD(%00000000,%00000000,%00111001) + jsr mirror_board + CHECK_BOARD(%00010000,%00100000,%00110000) + ;; Complex board pattern + SET_BOARD(%00100100,%00110011,%00011001) + jsr mirror_board + CHECK_BOARD(%011100,%100001,%011110) +no_error: + PRINTSNL("OK") + rts +error: + PRINTSNL("ERROR") + rts + .) + + ;; Computer finishes game + ;; by completing column +win_by_column: + ;; First column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 0) + SET_FIELD(piece_x, 3) + jsr play_win_by_column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 0) + SET_FIELD(piece_x, 6) + jsr play_win_by_column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 3) + SET_FIELD(piece_x, 6) + jsr play_win_by_column + ;; Second column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 1) + SET_FIELD(piece_x, 4) + jsr play_win_by_column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 1) + SET_FIELD(piece_x, 7) + jsr play_win_by_column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 4) + SET_FIELD(piece_x, 7) + jsr play_win_by_column + ;; Third column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 2) + SET_FIELD(piece_x, 5) + jsr play_win_by_column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 2) + SET_FIELD(piece_x, 8) + jsr play_win_by_column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 5) + SET_FIELD(piece_x, 8) + jsr play_win_by_column + rts +play_win_by_column: + jsr print_board + PRINTNL + lda #piece_x + jsr computer_perfect_ply + pha + jsr print_board + pla + jsr get_game_state + cmp #piece_x + bne win_by_column_error + PRINTSNL("OK") + rts +win_by_column_error: + PRINTSNL("ERROR") + rts + +win_by_diagonal: + ;; + ;; First diagonal + ;; + ;; Lower right field empty + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 0) + SET_FIELD(piece_x, 4) + jsr play_win_by_diagonal + ;; Middle field empty + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 0) + SET_FIELD(piece_x, 8) + jsr play_win_by_diagonal + ;; Upper left field empty + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 4) + SET_FIELD(piece_x, 8) + jsr play_win_by_diagonal + ;; + ;; Second diagonal + ;; + ;; Lower left field empty + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 2) + SET_FIELD(piece_x, 4) + jsr play_win_by_diagonal + ;; Middle field empty + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 2) + SET_FIELD(piece_x, 6) + jsr play_win_by_diagonal + ;; Upper right field empty + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 4) + SET_FIELD(piece_x, 6) + jsr play_win_by_diagonal + rts +play_win_by_diagonal: + jsr print_board + PRINTNL + lda #piece_x + jsr computer_perfect_ply + pha + jsr print_board + pla + jsr get_game_state + cmp #piece_x + bne win_by_diagonal_error + PRINTSNL("OK") + rts +win_by_diagonal_error: + PRINTSNL("ERROR") + rts + +block_by_column: + ;; First column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 0) + SET_FIELD(piece_x, 3) + jsr play_block_by_column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 0) + SET_FIELD(piece_x, 6) + jsr play_block_by_column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 3) + SET_FIELD(piece_x, 6) + jsr play_block_by_column + ;; Second column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 1) + SET_FIELD(piece_x, 4) + jsr play_block_by_column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 1) + SET_FIELD(piece_x, 7) + jsr play_block_by_column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 4) + SET_FIELD(piece_x, 7) + jsr play_block_by_column + ;; Third column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 2) + SET_FIELD(piece_x, 5) + jsr play_block_by_column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 2) + SET_FIELD(piece_x, 8) + jsr play_block_by_column + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 5) + SET_FIELD(piece_x, 8) + jsr play_block_by_column + rts +play_block_by_column: + jsr print_board + PRINTNL + lda #piece_o + jsr computer_perfect_ply + pha + jsr print_board + pla + jsr getc + rts + +fork_row_column: + .( + ;; Row/column fork + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 0) + SET_FIELD(piece_x, 1) + SET_FIELD(piece_o, 2) + SET_FIELD(piece_o, 3) + SET_FIELD(piece_x, 6) + pha + jsr print_board + pla + PRINTNL + lda #piece_o + jsr computer_perfect_ply + pha + jsr print_board + pla + ;; Check for correct ply + GET_FIELD(main_board,5) + CHECK_ERROR(piece_o) + jsr getc + ;; Row/diagonal fork + jsr init_board + // lda #$ac ; Test runs perfectly with this initialization of the lfsr + // sta lfsr_state + // sta lfsr_state+1 + jsr computer_perfect_init + SET_FIELD(piece_x, 0) + SET_FIELD(piece_o, 3) + SET_FIELD(piece_x, 6) + PRINTNL + pha + jsr print_board + pla + lda #piece_x + jsr computer_perfect_ply + pha + PRINTNL + jsr print_board + pla + ;; Check for correct ply + GET_FIELD(main_board,2) + CHECK_ERROR(piece_x) + jsr getc + ;; Another Row/diagonal fork + jsr init_board + jsr computer_perfect_init + SET_FIELD(piece_x, 0) + SET_FIELD(piece_x, 3) + SET_FIELD(piece_o, 6) + SET_FIELD(piece_o, 7) + SET_FIELD(piece_x, 8) + pha + jsr print_board + pla + PRINTNL + lda #piece_o + jsr computer_perfect_ply + pha + jsr print_board + pla + ;; Check for correct ply + GET_FIELD(main_board,4) + CHECK_ERROR(piece_o) + jsr getc + rts + .) + +test_block_fork: + .( + jsr computer_perfect_init + STORE_WORD(main_board,board_ptr) + SET_FIELD(piece_x, 0) + SET_FIELD(piece_o, 3) + SET_FIELD(piece_x, 4) + SET_FIELD(piece_o, 8) + PRINTNL + pha + jsr print_board + pla + lda #piece_o + jsr computer_perfect_ply + pha + PRINTNL + jsr print_board + pla + ;; Check for correct ply + GET_FIELD(main_board,1) + CHECK_ERROR(piece_o) + jsr getc + ;; Check if computer creates + ;; two-in-a-row/column/diagonal + ;; when when a fork by the opponet is + ;; possible. + ;; The following scenario + ;; can be created by the plys + ;; X -> 5 + ;; O -> 1 + ;; X -> 9 + jsr computer_perfect_init + STORE_WORD(main_board,board_ptr) + SET_FIELD(piece_o, 0) + SET_FIELD(piece_x, 4) + SET_FIELD(piece_x, 8) + PRINTNL + pha + jsr print_board + pla + lda #piece_o + jsr computer_perfect_ply + pha + PRINTNL + jsr print_board + pla + ;; Check for correct ply + GET_FIELD(main_board,1) + CHECK_ERROR(piece_o) + jsr getc + ;; The following scenario + ;; can be created by the plys + ;; X -> 1 + ;; O -> 5 + ;; X -> 9 + jsr computer_perfect_init + STORE_WORD(main_board,board_ptr) + SET_FIELD(piece_x, 0) + SET_FIELD(piece_o, 4) + SET_FIELD(piece_x, 8) + PRINTNL + pha + jsr print_board + pla + lda #piece_o + jsr computer_perfect_ply + pha + PRINTNL + jsr print_board + pla + ;; Check for correct ply + GET_FIELD(main_board,3) + CHECK_ERROR(piece_o) + jsr getc + rts + ;; The following scenario + ;; can be created by the plys + ;; X -> 5 + ;; O -> 1 + ;; X -> 9 + jsr computer_perfect_init + STORE_WORD(main_board,board_ptr) + SET_FIELD(piece_x, 4) + SET_FIELD(piece_o, 0) + SET_FIELD(piece_x, 8) + PRINTNL + pha + jsr print_board + pla + lda #piece_o + jsr computer_perfect_ply + pha + PRINTNL + jsr print_board + pla + ;; Check for correct ply + GET_FIELD(main_board,2) + CHECK_ERROR(piece_o) + jsr getc + rts + .) + +test_play_opposite_corner: + .( + jsr computer_perfect_init + STORE_WORD(main_board,board_ptr) + SET_FIELD(piece_x, 0) + SET_FIELD(piece_o, 4) + PRINTNL + pha + jsr print_board + pla + lda #piece_o + jsr computer_perfect_ply + pha + PRINTNL + jsr print_board + pla + ;; Check for correct ply + GET_FIELD(main_board,8) + CHECK_ERROR(piece_o) + jsr getc + rts + .) + +test_play_empty_corner: + .( + jsr computer_perfect_init + STORE_WORD(main_board,board_ptr) + SET_FIELD(piece_x, 4) + SET_FIELD(piece_o, 0) + PRINTNL + pha + jsr print_board + pla + lda #piece_o + jsr computer_perfect_ply + pha + PRINTNL + jsr print_board + pla + ;; Check for correct ply + GET_FIELD(main_board,2) + CHECK_ERROR(piece_o) + jsr getc + rts + .) + +test_perfect_vs_random_computer_player: + .( +game_loop: + ;; Set player X + STORE_WORD(computer_perfect_init, player_x_init_ptr) + STORE_WORD(computer_perfect_ply, player_x_ply_ptr) + ;; Set player O + STORE_WORD(computer_random_init, player_o_init_ptr) + STORE_WORD(computer_random_ply, player_o_ply_ptr) + jsr play_game + jsr get_game_state + cmp #piece_o ; Random player + bne game_loop ; Should never win + PRINTSNL('Error') +error: + jmp error + .) +test_random_vs_perfect_computer_player: + .( +game_loop: + ;; Set player X + STORE_WORD(computer_random_init, player_x_init_ptr) + STORE_WORD(computer_random_ply, player_x_ply_ptr) + ;; Set player O + STORE_WORD(computer_perfect_init, player_o_init_ptr) + STORE_WORD(computer_perfect_ply, player_o_ply_ptr) + jsr play_game + jsr get_game_state + cmp #piece_x ; Random player + bne game_loop ; Should never win + PRINTSNL('Error') +error: + jmp error + .) +test_perfect_vs_perfect_computer_player: + .( +game_loop: + ;; Set player X + STORE_WORD(computer_perfect_init, player_x_init_ptr) + STORE_WORD(computer_perfect_ply, player_x_ply_ptr) + ;; Set player O + STORE_WORD(computer_perfect_init, player_o_init_ptr) + STORE_WORD(computer_perfect_ply, player_o_ply_ptr) + jsr play_game + jsr get_game_state + cmp #piece_none ; All games should end in + beq game_loop ; a draw. + PRINTSNL('Error') +error: + jmp error + .) + +print_random_ply: + .( + ;; Debug output - Computer ply + pha + phx + clc + adc #'0' + jsr putc + PRINTS(' plays ') + pla + pha + adc #'0' + jsr putc + PRINTNL + plx + pla + rts + .) + +print_ply: + .( + ;; Debug output - Computer ply + pha + phy + phx + clc + adc #'0' + jsr putc + PRINTS(' plays ') + pla + pha + adc #'0' + jsr putc + lda #'/' + jsr putc + plx + pla + pha + phx + adc #'0' + jsr putc + PRINTNL + plx + ply + pla + rts + .) + diff --git a/sw/ttt/human_player.s b/sw/ttt/human_player.s @@ -0,0 +1,53 @@ +;;; No initialization necessary +human_player_init: + rts + +;;; Ask human player for his/her next +;;; move. A contains the piece_x or +;;; piece_o, depending which +;;; piece the human plays +human_player_ply: + .( + ;; Print board + pha + jsr print_board + ;; Print question for move + PRINTS("Player ") + pla + pha + clc + adc #'0' + jsr putc + PRINTS(": ") + ;; Get move and check + ;; if move is valid. + jsr getc + jsr putc + ;; Check if user input + ;; is in range [1..9] + cmp #"1" + bcc invalid + cmp #":" ; Follows 9 in ASCII table + bcs invalid + ;; Check if field is already + ;; occuped + sec + sbc #"1" + sta tmp ; Field number + tax + jsr get_field + cmp #piece_none + bne invalid + PRINTNL + PRINTNL + ;; Set the piece + ldx tmp ; Field number + pla ; Piece + jsr set_field + rts +invalid: + PRINTNL + PRINTSNL("Not a valid move.") + pla + jmp human_player_ply + .) diff --git a/sw/ttt/ttt.s b/sw/ttt/ttt.s @@ -0,0 +1,159 @@ +#include "../../roms/boot/liba.h" + +;;; Variables + ;; This temporary variable is used all over the place + tmp = $30 + ;; Used by win_first_diagonal and win_first_diagonal + empty_field = $3e + ;; Used to check for forking opportunities + fork_board_rows = $4006 + fork_board_columns = $4009 + fork_board_diagonals = $400c + ;; Used to pass location of board + ;; to subroutines. + board_ptr = $32 + ;; Sometimes we copy stuff from one board + ;; to the other. For this we have to + ;; point to the other board. + other_board_ptr = $34 + ;; Default board location in memory + main_board = $4000 + ;; Mirrored board location in memory + board_mirrored = $4003 + ;; Pointer to init subroutine of + ;; first player (2 bytes) + player_x_init_ptr = $36 + ;; Pointer to ply subroutine of + ;; first player (2 bytes) + player_x_ply_ptr = $38 + ;; Pointer to init subroutine of + ;; second player (2 bytes) + player_o_init_ptr = $3a + ;; Pointer to ply subroutine of + ;; first player (2 bytes) + player_o_ply_ptr = $3c + ;; Bord rendered as ASCII art + board_ascii = $5000 + + +#ifdef SYMON + rom_start = $c000 +#else + rom_start = $e000 +#endif + +#ifdef BOOT + ;; Program to be backed into + ;; boot ROM + * = rom_start + $0c00 +#else + ;; Program to be uploaded to + ;; RAM + * = $0300 +#endif + +start_ttt: + .( + cld + jsr init_acia + jsr lfsr_init +#ifdef RUN_TESTS + jsr run_tests +#endif +game_loop: + PRINTSNL('** Tic-Tac-Toe **') + PRINTNL + lda #piece_x + jsr choose_player + lda #piece_o + jsr choose_player + jsr play_game + jmp game_loop + .) + + +choose_player: + .( + pha + PRINTS("Choose player ") + pla + pha + clc + adc #'0' + jsr putc + PRINTSNL(':') + PRINTSNL('1 - Human') + PRINTSNL('2 - Computer (perfect)') + PRINTSNL('3 - Computer (random)') + jsr getc_seed_rng + cmp #'1' + beq set_human_player + cmp #'2' + beq set_computer_perfect_player + cmp #'3' + beq set_computer_random_player + pla + jmp choose_player + ;; Board is not initialized at + ;; this point. Therefore we can use + ;; board_ptr to store a temporary + ;; variable. +set_human_player: + STORE_WORD(human_player_init, tmp) + STORE_WORD(human_player_ply, board_ptr) + jmp set_player +set_computer_perfect_player: + STORE_WORD(computer_perfect_init, tmp) + STORE_WORD(computer_perfect_ply, board_ptr) + jmp set_player +set_computer_random_player: + STORE_WORD(computer_random_init, tmp) + STORE_WORD(computer_random_ply, board_ptr) +set_player: + pla + cmp #piece_x + bne set_second_player + lda tmp + sta player_x_init_ptr + lda tmp+1 + sta player_x_init_ptr+1 + lda board_ptr + sta player_x_ply_ptr + lda board_ptr+1 + sta player_x_ply_ptr+1 + rts +set_second_player: + lda tmp + sta player_o_init_ptr + lda tmp+1 + sta player_o_init_ptr+1 + lda board_ptr + sta player_o_ply_ptr + lda board_ptr+1 + sta player_o_ply_ptr+1 + rts + .) + + + + + +#include "board.s" +#include "human_player.s" +#include "computer_player.s" + +#ifdef RUN_TESTS +#include "board_test.s" +#include "computer_player_test.s" +run_tests: + // jsr game_board_test + // PRINTNL + // jsr getc + // jsr mirror_board_test + jsr computer_player_test + PRINTNL + jsr getc + jmp run_tests +#endif + +