commit 82c002891fdf49c73cc081b2215c29d25baf0531 Author: StewBC Date: Fri Feb 14 16:57:00 2014 +0100 cc65 Chess V1.0 Initial commit diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..49ab60b --- /dev/null +++ b/Makefile @@ -0,0 +1,345 @@ +############################################################################### +### Generic Makefile for cc65 projects - full version with abstract options ### +### V1.3.0(w) 2010 - 2013 Oliver Schmidt & Patryk "Silver Dream !" Łogiewa ### +############################################################################### + +############################################################################### +### In order to override defaults - values can be assigned to the variables ### +############################################################################### + +# Space or comma separated list of cc65 supported target platforms to build for. +# Default: c64 (lowercase!) +TARGETS := + +# Name of the final, single-file executable. +# Default: name of the current dir with target name appended +PROGRAM := + +# Path(s) to additional libraries required for linking the program +# Use only if you don't want to place copies of the libraries in SRCDIR +# Default: none +LIBS := + +# Custom linker configuration file +# Use only if you don't want to place it in SRCDIR +# Default: none +CONFIG := + +# Additional C compiler flags and options. +# Default: none +CFLAGS = + +# Additional assembler flags and options. +# Default: none +ASFLAGS = + +# Additional linker flags and options. +# Default: none +LDFLAGS = + +# Path to the directory containing C and ASM sources. +# Default: src +SRCDIR := + +# Path to the directory where object files are to be stored (inside respective target subdirectories). +# Default: obj +OBJDIR := + +# Command used to run the emulator. +# Default: depending on target platform. For default (c64) target: x64 -kernal kernal -VICIIdsize -autoload +EMUCMD := + +# Optional commands used before starting the emulation process, and after finishing it. +# Default: none +#PREEMUCMD := osascript -e "tell application \"System Events\" to set isRunning to (name of processes) contains \"X11.bin\"" -e "if isRunning is true then tell application \"X11\" to activate" +#PREEMUCMD := osascript -e "tell application \"X11\" to activate" +#POSTEMUCMD := osascript -e "tell application \"System Events\" to tell process \"X11\" to set visible to false" +#POSTEMUCMD := osascript -e "tell application \"Terminal\" to activate" +PREEMUCMD := +POSTEMUCMD := + +# On Windows machines VICE emulators may not be available in the PATH by default. +# In such case, please set the variable below to point to directory containing +# VICE emulators. +#VICE_HOME := "C:\Program Files\WinVICE-2.2-x86\" +VICE_HOME := + +# Options state file name. You should not need to change this, but for those +# rare cases when you feel you really need to name it differently - here you are +STATEFILE := Makefile.options + +################################################################################### +#### DO NOT EDIT BELOW THIS LINE, UNLESS YOU REALLY KNOW WHAT YOU ARE DOING! #### +################################################################################### + +################################################################################### +### Mapping abstract options to the actual compiler, assembler and linker flags ### +### Predefined compiler, assembler and linker flags, used with abstract options ### +### valid for 2.14.x. Consult the documentation of your cc65 version before use ### +################################################################################### + +# Compiler flags used to tell the compiler to optimise for SPEED +define _optspeed_ + CFLAGS += -Oris +endef + +# Compiler flags used to tell the compiler to optimise for SIZE +define _optsize_ + CFLAGS += -Or +endef + +# Compiler and assembler flags for generating listings +define _listing_ + CFLAGS += --listing $$(@:.o=.lst) + ASFLAGS += --listing $$(@:.o=.lst) + REMOVES += $(addsuffix .lst,$(basename $(OBJECTS))) +endef + +# Linker flags for generating map file +define _mapfile_ + LDFLAGS += --mapfile $$@.map + REMOVES += $(PROGRAM).map +endef + +# Linker flags for generating VICE label file +define _labelfile_ + LDFLAGS += -Ln $$@.lbl + REMOVES += $(PROGRAM).lbl +endef + +# Linker flags for generating a debug file +define _debugfile_ + LDFLAGS += -Wl --dbgfile,$$@.dbg + REMOVES += $(PROGRAM).dbg +endef + +############################################################################### +### Defaults to be used if nothing defined in the editable sections above ### +############################################################################### + +# Presume the C64 target like the cl65 compile & link utility does. +# Set TARGETS to override. +ifeq ($(TARGETS),) + TARGETS := c64 +endif + +# Presume we're in a project directory so name the program like the current +# directory. Set PROGRAM to override. +ifeq ($(PROGRAM),) + PROGRAM := $(notdir $(CURDIR)) +endif + +# Presume the C and asm source files to be located in the subdirectory 'src'. +# Set SRCDIR to override. +ifeq ($(SRCDIR),) + SRCDIR := src +endif + +# Presume the object and dependency files to be located in the subdirectory +# 'obj' (which will be created). Set OBJDIR to override. +ifeq ($(OBJDIR),) + OBJDIR := obj +endif +TARGETOBJDIR := $(OBJDIR)/$(TARGETS) + +# On Windows it is mandatory to have CC65_HOME set. So do not unnecessarily +# rely on cl65 being added to the PATH in this scenario. +ifdef CC65_HOME + CC := $(CC65_HOME)/bin/cl65 +else + CC := cl65 +endif + +# Default emulator commands and options for particular targets. +# Set EMUCMD to override. +c64_EMUCMD := $(VICE_HOME)x64 -kernal kernal -VICIIdsize -autoload +c128_EMUCMD := $(VICE_HOME)x128 -kernal kernal -VICIIdsize -autoload +vic20_EMUCMD := $(VICE_HOME)xvic -kernal kernal -VICdsize -autoload +pet_EMUCMD := $(VICE_HOME)xpet -Crtcdsize -autoload +plus4_EMUCMD := $(VICE_HOME)xplus4 -TEDdsize -autoload +# So far there is no x16 emulator in VICE (why??) so we have to use xplus4 with -memsize option +c16_EMUCMD := $(VICE_HOME)xplus4 -ramsize 16 -TEDdsize -autoload +cbm510_EMUCMD := $(VICE_HOME)xcbm2 -model 510 -VICIIdsize -autoload +cbm610_EMUCMD := $(VICE_HOME)xcbm2 -model 610 -Crtcdsize -autoload +atari_EMUCMD := atari800 -windowed -xl -pal -nopatchall -run + +ifeq ($(EMUCMD),) + EMUCMD = $($(CC65TARGET)_EMUCMD) +endif + +############################################################################### +### The magic begins ### +############################################################################### + +# The "Native Win32" GNU Make contains quite some workarounds to get along with +# cmd.exe as shell. However it does not provide means to determine that it does +# actually activate those workarounds. Especially does $(SHELL) NOT contain the +# value 'cmd.exe'. So the usual way to determine if cmd.exe is being used is to +# execute the command 'echo' without any parameters. Only cmd.exe will return a +# non-empy string - saying 'ECHO is on/off'. +# +# Many "Native Win32" programs accept '/' as directory delimiter just fine. How- +# ever the internal commands of cmd.exe generally require '\' to be used. +# +# cmd.exe has an internal command 'mkdir' that doesn't understand nor require a +# '-p' to create parent directories as needed. +# +# cmd.exe has an internal command 'del' that reports a syntax error if executed +# without any file so make sure to call it only if there's an actual argument. +ifeq ($(shell echo),) + MKDIR = mkdir -p $1 + RMDIR = rmdir $1 + RMFILES = $(RM) $1 +else + MKDIR = mkdir $(subst /,\,$1) + RMDIR = rmdir $(subst /,\,$1) + RMFILES = $(if $1,del /f $(subst /,\,$1)) +endif +COMMA := , +SPACE := $(N/A) $(N/A) +define NEWLINE + + +endef +# Note: Do not remove any of the two empty lines above ! + +TARGETLIST := $(subst $(COMMA),$(SPACE),$(TARGETS)) + +ifeq ($(words $(TARGETLIST)),1) + +# Set PROGRAM to something like 'myprog.c64'. +override PROGRAM := $(PROGRAM).$(TARGETLIST) + +# Set SOURCES to something like 'src/foo.c src/bar.s'. +# Use of assembler files with names ending differently than .s is deprecated! +SOURCES := $(wildcard $(SRCDIR)/*.c) +SOURCES += $(wildcard $(SRCDIR)/*.s) +SOURCES += $(wildcard $(SRCDIR)/*.asm) +SOURCES += $(wildcard $(SRCDIR)/*.a65) + +# Add to SOURCES something like 'src/c64/me.c src/c64/too.s'. +# Use of assembler files with names ending differently than .s is deprecated! +SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.c) +SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.s) +SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.asm) +SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.a65) + +# Set OBJECTS to something like 'obj/c64/foo.o obj/c64/bar.o'. +OBJECTS := $(addsuffix .o,$(basename $(addprefix $(TARGETOBJDIR)/,$(notdir $(SOURCES))))) + +# Set DEPENDS to something like 'obj/c64/foo.d obj/c64/bar.d'. +DEPENDS := $(OBJECTS:.o=.d) + +# Add to LIBS something like 'src/foo.lib src/c64/bar.lib'. +LIBS += $(wildcard $(SRCDIR)/*.lib) +LIBS += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.lib) + +# Add to CONFIG something like 'src/c64/bar.cfg src/foo.cfg'. +CONFIG += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.cfg) +CONFIG += $(wildcard $(SRCDIR)/*.cfg) + +# Select CONFIG file to use. Target specific configs have higher priority. +ifneq ($(word 2,$(CONFIG)),) + CONFIG := $(firstword $(CONFIG)) + $(info Using config file $(CONFIG) for linking) +endif + +.SUFFIXES: +.PHONY: all test clean zap love + +all: $(PROGRAM) + +-include $(DEPENDS) +-include $(STATEFILE) + +# If OPTIONS are given on the command line then save them to STATEFILE +# if (and only if) they have actually changed. But if OPTIONS are not +# given on the command line then load them from STATEFILE. Have object +# files depend on STATEFILE only if it actually exists. +ifeq ($(origin OPTIONS),command line) + ifneq ($(OPTIONS),$(_OPTIONS_)) + ifeq ($(OPTIONS),) + $(info Removing OPTIONS) + $(shell $(RM) $(STATEFILE)) + $(eval $(STATEFILE):) + else + $(info Saving OPTIONS=$(OPTIONS)) + $(shell echo _OPTIONS_=$(OPTIONS) > $(STATEFILE)) + endif + $(eval $(OBJECTS): $(STATEFILE)) + endif +else + ifeq ($(origin _OPTIONS_),file) + $(info Using saved OPTIONS=$(_OPTIONS_)) + OPTIONS = $(_OPTIONS_) + $(eval $(OBJECTS): $(STATEFILE)) + endif +endif + +# Transform the abstract OPTIONS to the actual cc65 options. +$(foreach o,$(subst $(COMMA),$(SPACE),$(OPTIONS)),$(eval $(_$o_))) + +# Strip potential variant suffix from the actual cc65 target. +CC65TARGET := $(firstword $(subst .,$(SPACE),$(TARGETLIST))) + +# The remaining targets. +$(TARGETOBJDIR): + $(call MKDIR,$@) + +vpath %.c $(SRCDIR)/$(TARGETLIST) $(SRCDIR) + +$(TARGETOBJDIR)/%.o: %.c | $(TARGETOBJDIR) + $(CC) -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(CFLAGS) -o $@ $< + +vpath %.s $(SRCDIR)/$(TARGETLIST) $(SRCDIR) + +$(TARGETOBJDIR)/%.o: %.s | $(TARGETOBJDIR) + $(CC) -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(ASFLAGS) -o $@ $< + +vpath %.asm $(SRCDIR)/$(TARGETLIST) $(SRCDIR) + +$(TARGETOBJDIR)/%.o: %.asm | $(TARGETOBJDIR) + $(CC) -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(ASFLAGS) -o $@ $< + +vpath %.a65 $(SRCDIR)/$(TARGETLIST) $(SRCDIR) + +$(TARGETOBJDIR)/%.o: %.a65 | $(TARGETOBJDIR) + $(CC) -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(ASFLAGS) -o $@ $< + +$(PROGRAM): $(CONFIG) $(OBJECTS) $(LIBS) + $(CC) -t $(CC65TARGET) $(LDFLAGS) -o $@ $(patsubst %.cfg,-C %.cfg,$^) + +test: $(PROGRAM) + $(PREEMUCMD) + $(EMUCMD) $< + $(POSTEMUCMD) + +clean: + $(call RMFILES,$(OBJECTS)) + $(call RMFILES,$(DEPENDS)) + $(call RMFILES,$(REMOVES)) + $(call RMFILES,$(PROGRAM)) + +else # $(words $(TARGETLIST)),1 + +all test clean: + $(foreach t,$(TARGETLIST),$(MAKE) TARGETS=$t $@$(NEWLINE)) + +endif # $(words $(TARGETLIST)),1 + +OBJDIRLIST := $(wildcard $(OBJDIR)/*) + +zap: + $(foreach o,$(OBJDIRLIST),-$(call RMFILES,$o/*.o $o/*.d $o/*.lst)$(NEWLINE)) + $(foreach o,$(OBJDIRLIST),-$(call RMDIR,$o)$(NEWLINE)) + -$(call RMDIR,$(OBJDIR)) + -$(call RMFILES,$(basename $(PROGRAM)).* $(STATEFILE)) + +love: + @echo "Not war, eh?" + +################################################################### +### Place your additional targets in the additional Makefiles ### +### in the same directory - their names have to end with ".mk"! ### +################################################################### +-include *.mk diff --git a/readme.txt b/readme.txt new file mode 100644 index 0000000..15902d5 --- /dev/null +++ b/readme.txt @@ -0,0 +1,189 @@ + +I. Introduction + +I started playing chess about 3 months ago and this got me wondering how +difficult it would be to make a computer chess game. I decided to try and +since it's just a "for the fun of it" project, I decided to make it for the +Commodore 64, still my all-time favorite computer. Using the excellent cc65 +tools I could do it all in C and thus make it portable to other systems also. + +I learnt that making the game isn't hard, but getting the computer to play +something that resembles a reasonable game is hard. I don't know enough about +chess to get it right, but in this version of today, 14 Feb 2014, the AI is +not very good. + +The game was developed on OS X using cc65 and the VICE emulator. + +There is a video of the game here: http://youtu.be/bkA4vtwxaJg + + +II. Use and keys + +The colors here refer to the C64 version. The terminal version has a minimal +working display but does try to somewhat match the colors of the C64. + +The user controls an on-screen cursor. The cursor changes color to indicate +a state. The colors for selection are: + Green - the piece can be selected + Red - The piece cannot be selected as it doesn't have valid moves + Purple - Empty tile or piece on the other side + Blue - The currently selected piece + Cyan - A valid destination for the currently selected piece + +To move the cursor, use the cursor keys. To select a piece, press the RETURN +key while the piece is selected. To deselect the piece, press RETURN on the +same piece again, or press RUN/STOP. + +To bring up the menu, press the M key, or the RUN/STOP key when no piece is +selected. Pressing RUN/STOP in a menu backs out of the menu, to the previous +menu or back to the game. Press RETURN to select a menu item and use the up +and down cursor keys to change the selection. + +While a side is under human control, there are a few more options. Press B to +toggle on/off a state showing on every tile how many of both black and white's +pieces can attack that tile. Pressing A will toggle a highlight of all of the +pieces on the opposing side that attack the selected tile. Pressing D will +toggle a highlight of all the pieces on the side currently playing's side that +can defend the selected tile. All three of these options basically give a +visual representation of the Attack DB. The colors are: For attackers Cyan +and for defenders Red. + + +III. Distribution + +This version has code for a C64, using multi-colored text mode, and also code +for a terminal version using the curses library. The terminal version was +only tested on OS X but I suspect it will run under Linux and Windows. + + +IV. Building from source + +a) For the C64 (and other cc65 supported platforms): +Using a properly installed cc65 distribution, the C64 version should build +using make in the folder with the Makefile. I suggest compiling for speed +but optimizing for size does save a bit of memory (1K at present): +make OPTIONS=optspeed + +b) For a terminal version: +I built it on OS X using the following command line from the src folder: +cc -I. -lcurses -funsigned-char globals.c undo.c board.c cpu.c human.c \ +frontend.c main.c term/platTerm.c -o chess + + +V. Porting + +The code in the src folder should compile on any system (cc65 has type char as +unsigned by default - the char type is almost the only type really used in the +code). A new system will need platform specific implementations of the +functions in plat.h. When I created the terminal port, it literally worked in +under an hour as it was mostly replacing cursor positioning and printing +function names, along with initialization and color management. I had to redo +the log update and timer completely but still, the only file that needed +changes was the platTerm.c copy I made from plat64.c. + +In the C64 specific folder under src is a data.c file which contains the +graphics characters to draw the pieces. The layout and chosen bit-pattern of +that data is also explained in that file. + + +VI. The AI & other thoughts on the code + +The game is a fine 2-player chess game, but the computer is not a great chess +player. My approach for the AI is this: + +For each piece, calculate a score (see later) for the tile the piece currently +occupies. Then look at all available moves for the piece, and score each +destination tile separately. If the computer is fast, I would now "effect" +each move and run the same algorithm on the opponent, getting a "retaliation +score". Effect the opponent move and run the algorithm again, getting a +subsequent score. The accumulated "score - retaliation score + this side +next score - other side next retaliation score" sum, up to as many levels +deep as desired, would be the final score for that piece and destination. + +Since the C64 isn't fast enough for all that, I have it calculate the score +for the piece where it stands and for all destinations. The highest scoring +move, if valid, becomes the move for the piece. The scores for all pieces +are then stack-ranked. Some number of these are then chosen to pursue. I set +it to 16 (gWidth), making it pursues all pieces. + +Pursuit of the best moves means doing the depth search for opponent moves and +back to own moves. This is set to go to a level controlled by a variable +named gMaxLevel. + +There is another variable, gDeepThoughts, that affects difficulty and speed. +This variable, when set to 1, ensure the moves chosen when evaluating best +moves, are valid. It also, when set, causes the AttackDB to be updated. +Both of these are slow operations. Not doing the work makes things a lot +faster, but obviously less accurate. Especially the further away the +thinking gets from the current, accurate, state. + +All three these variables are set from the difficulty selection if there's +an AI player. + +Scoring a piece means this: Positive points encourage the piece to move, +negative points discourage making a move. +A) For where the piece stands the score is calculated by looking at: + If this piece is under attack increase chance to move, else decrease + If this piece is being defended, decrease chance to move, else increase + Providing support to a piece on own team, decrease + if supported piece is under attack + if only defender, decrease else increase + if supported piece is more valuable, decrease + not supporting a piece, increase + +B) For every destination the piece can move to, score like this: + If this piece will be under attack there, decrease + If it will be defended there, increase else decrease + If a piece is taken at dest, increase + If providing support to a piece on same side from there, increase + if that piece is under attack, increase + if this will be the only defender of that piece, increase + if the supported piece is more valuable than this piece, increase + If attacking a piece on the other side from there, increase + if the attacked piece is more valuable, increase + if the attacked piece has no defenders, increase + +The values for increase and decrease aren't always 1. Some situations I +deemed more important so the value may be 2, or the value of the piece itself +for which I use: 1 PAWN, 3 KNIGHT, 3 BISHOP, 5 ROOK, 9 QUEEN, 10 KING, but +modified to 2+(3*value). The +2 compensates for the +/-1's that encourage or +discourage a move, and the 3*value makes the value really meaningful. + +There is another scoring opportunity that happens before any other. It is +meant to take a holistic view of the board. Currently, all it does is see +if the king has no moves then all of its neighboring pieces on the same side +are encouraged to move and; it encourages pawns to move so they can get to +promote. The closer they get to the opposite side, the stronger the +encouragement. + +I have no real plans to keep working on this project. As stated, I wanted +to see how hard it would be, and now I know. I rushed this V1.0 release so, +sadly, I am sure there will be bugs. There's also lots of room to experiment +with scores, values and relative importance of things like being under attack +vs. supporting another piece. + +The code is reasonably clean but I really didn't design this as a game. It +all evolved from the writing of the functions to build an array of valid moves +into a game. The en passant and castling is somewhat hacked in, for example +and may be hard to make sense of. + + +VII. Credits + +The Makefile has the following notice: +############################################################################### +### Generic Makefile for cc65 projects - full version with abstract options ### +### V1.3.0(w) 2010 - 2013 Oliver Schmidt & Patryk "Silver Dream !" Łogiewa ### +############################################################################### + +cl65 --version prints the following in my installation: +cl65 V2.13.9 - (C) Copyright 1998-2011 Ullrich von Bassewitz + +VIII. Contact + +Feel free to send me an email if you have any comments. If you do make a port +or something else, I would love to hear about it! + +swessels@email.com + +Thank you! \ No newline at end of file diff --git a/src/board.c b/src/board.c new file mode 100644 index 0000000..ef3007c --- /dev/null +++ b/src/board.c @@ -0,0 +1,765 @@ +/* + * board.c + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#include +#include +#include "types.h" +#include "globals.h" +#include "board.h" +#include "frontend.h" +#include "plat.h" + +/*-----------------------------------------------------------------------*/ +void board_LoadMoves(char x, char y, char dx, char dy, char n, char addDefenceMove); +void board_GenPawnMoves(char position, char color, char addDefenceMove); +void board_GenRookMoves(char position, char addDefenceMove); +void board_GenKnightMoves(char position, char addDefenceMove); +void board_GenBishopMoves(char position, char addDefenceMove); +void board_GenKingMoves(char position, char addDefenceMove); +void board_GenQueenMoves(char position, char addDefenceMove); +char board_CheckLineAttack(char t1, char t2, char side); +void board_UpdateAttackGrid(int offset, char side); + +/*-----------------------------------------------------------------------*/ +// board_UpdateAttackGrid may add entries (behind the king in a line +// attack, for example) and these variables track those changes so they +// can easily be undone +static int si_fixupTable[(8*2)+7+6]; +static char sc_numFixes; + +/*-----------------------------------------------------------------------*/ +void board_Init() +{ + char i; + + memset(gpChessBoard,NONE,8*8*sizeof(char)); + + gChessBoard[0][0] = gChessBoard[7][0] = ROOK; + gChessBoard[0][1] = gChessBoard[7][1] = KNIGHT; + gChessBoard[0][2] = gChessBoard[7][2] = BISHOP; + gChessBoard[0][3] = gChessBoard[7][3] = QUEEN; + gChessBoard[0][4] = gChessBoard[7][4] = KING; + gChessBoard[0][5] = gChessBoard[7][5] = BISHOP; + gChessBoard[0][6] = gChessBoard[7][6] = KNIGHT; + gChessBoard[0][7] = gChessBoard[7][7] = ROOK; + + for(i=0; i < 8; ++i) + { + gChessBoard[1][i] = PAWN; + gChessBoard[6][i] = PAWN | PIECE_WHITE; + gChessBoard[7][i] |= PIECE_WHITE; + } + + gKingData[0] = MK_POS(0,4); + gKingData[1] = MK_POS(7,4); + + // Not part of the board but does need resetting for every game + gCursorPos[SIDE_BLACK][1] = gCursorPos[SIDE_WHITE][1] = 4; + gCursorPos[SIDE_BLACK][0] = 0; + gCursorPos[SIDE_WHITE][0] = 7; + + board_PlacePieceAttacks(); +} + +/*-----------------------------------------------------------------------*/ +// For every piece on the board, make entries in the attack db of what it +// is attacking or defending +void board_PlacePieceAttacks() +{ + char i, j, piece; + + memset(gpAttackBoard,0,8*8*ATTACK_WIDTH); + + for(i=0; i<64; ++i) + { + piece = gpChessBoard[i]; + if(piece != NONE) + { + char color = (piece & PIECE_WHITE) >> 7; + board_GeneratePossibleMoves(i, 1); + if(gNumMoves) + { + for(j=0; j 7 || y > 7) + break; + + piece = gChessBoard[y][x]; + // color = piece & PIECE_WHITE; + // piece &= PIECE_DATA; + + if(NONE == piece || addDefenceMove || my_color != (piece & PIECE_WHITE)) + gPossibleMoves[gNumMoves++] = MK_POS(y,x); + + // any non-emty tiles ends the scan + if(NONE != piece) + break; + + } while(n); +} + +/*-----------------------------------------------------------------------*/ +// position is a tile number 0..63. If there's a piece on the tile, +// it's available moves will be placed in gPossibleMoves. if +// addDefenceMove is non-zero, moves onto friendly pieces and moves +// by PAWNS to capture are allso added (defence and pawn-take) +void board_GeneratePossibleMoves(char position, char addDefenceMove) +{ + char piece, color; + + // Reset the global counter - used as an index into the + // global buffer for tracking moves for a piece + gNumMoves = 0; + + piece = gpChessBoard[position]; + color = (piece & PIECE_WHITE) >> 7; + piece &= PIECE_DATA; + + // Call the piece-specific function to generate the list + // for the piece at tile "position" + switch(piece) + { + case PAWN: + board_GenPawnMoves(position, color, addDefenceMove); + break; + + case ROOK: + board_GenRookMoves(position, addDefenceMove); + break; + + case KNIGHT: + board_GenKnightMoves(position, addDefenceMove); + break; + + case BISHOP: + board_GenBishopMoves(position, addDefenceMove); + break; + + case KING: + board_GenKingMoves(position, addDefenceMove); + break; + + case QUEEN: + board_GenQueenMoves(position, addDefenceMove); + break; + } +} + +/*-----------------------------------------------------------------------*/ +void board_GenPawnMoves(char position, char sideColor, char addDefenceMove) +{ + const signed char cap_x[2] = {-1, 1}; + char i, nx, piece, color, y = position / 8, x = position & 7; + signed char d = sideColor ? -1 : 1; + + if(y == 0 || y == 7) + return; + + // if addDefenceMove is true, the move forward of a pawn isn't added + // becase a pawn doesn't "defend" the next square in that it can't + // take the piece there + if(!addDefenceMove && NONE == gChessBoard[y+d][x]) + { + gPossibleMoves[gNumMoves++] = MK_POS(y+d,x); + if(((sideColor && y == 6) || (!sideColor && y == 1)) && NONE == gChessBoard[y+(2*d)][x]) + gPossibleMoves[gNumMoves++] = MK_POS(y+(2*d),x); + } + + for(i=0; i<2; ++i) + { + nx = x + cap_x[i]; + if(nx > 7) + continue; + + position = MK_POS(y+d,nx); + + if(gEPPawn != position) + { + // Read the piece from the chessboard + piece = gChessBoard[y+d][nx]; + color = (piece & PIECE_WHITE) >> 7; + piece &= PIECE_DATA; + } + else + { + // Create a fake piece to represent the en passant pawn + piece = PAWN; + color = 1 - sideColor; + } + + if(addDefenceMove || (NONE != piece && color != sideColor)) + gPossibleMoves[gNumMoves++] = position; + } +} + +/*-----------------------------------------------------------------------*/ +void board_GenRookMoves(char position, char addDefenceMove) +{ + char y = position / 8, x = position & 7; + + board_LoadMoves(x, y, -1, 0, 8, addDefenceMove); + board_LoadMoves(x, y, 0, -1, 8, addDefenceMove); + board_LoadMoves(x, y, 1, 0, 8, addDefenceMove); + board_LoadMoves(x, y, 0, 1, 8, addDefenceMove); +} + +/*-----------------------------------------------------------------------*/ +void board_GenKnightMoves(char position, char addDefenceMove) +{ + char y = position / 8, x = position & 7; + + board_LoadMoves(x, y, -2, -1, 1, addDefenceMove); + board_LoadMoves(x, y, -1, -2, 1, addDefenceMove); + board_LoadMoves(x, y, 1, -2, 1, addDefenceMove); + board_LoadMoves(x, y, 2, -1, 1, addDefenceMove); + board_LoadMoves(x, y, 2, 1, 1, addDefenceMove); + board_LoadMoves(x, y, 1, 2, 1, addDefenceMove); + board_LoadMoves(x, y, -1, 2, 1, addDefenceMove); + board_LoadMoves(x, y, -2, 1, 1, addDefenceMove); +} + +/*-----------------------------------------------------------------------*/ +void board_GenBishopMoves(char position, char addDefenceMove) +{ + char y = position / 8, x = position & 7; + + board_LoadMoves(x, y, -1, -1, 8, addDefenceMove); + board_LoadMoves(x, y, 1, -1, 8, addDefenceMove); + board_LoadMoves(x, y, 1, 1, 8, addDefenceMove); + board_LoadMoves(x, y, -1, 1, 8, addDefenceMove); +} + +/*-----------------------------------------------------------------------*/ +void board_GenKingMoves(char position, char addDefenceMove) +{ + char okayToCastle[2] = {0,0}, y = position / 8, x = position & 7; + + // If the king hasn't moved (| PIECE_MOVED == 0) + if(KING == (gpChessBoard[position] & (PIECE_DATA | PIECE_MOVED))) + { + // and the rook hasn't moved + if(ROOK == (gpChessBoard[position-4] & (PIECE_DATA | PIECE_MOVED))) + { + // and there are 3 open spaces between them, then + // casteling is possible + board_LoadMoves(x, y, -1, 0, 3, addDefenceMove); + if(3 == gNumMoves) + okayToCastle[0] = 1; + gNumMoves = 0; + } + + if(ROOK == (gpChessBoard[position+3] & (PIECE_DATA | PIECE_MOVED))) + { + // on this side, 2 open spaces is all that's needed + board_LoadMoves(x, y, 1, 0, 2, addDefenceMove); + if(2 == gNumMoves) + okayToCastle[1] = 1; + gNumMoves = 0; + } + } + + if(okayToCastle[0]) + board_LoadMoves(x, y, -2, 0, 1, addDefenceMove); + + if(okayToCastle[1]) + board_LoadMoves(x, y, 2, 0, 1, addDefenceMove); + + board_LoadMoves(x, y, -1, 0, 1, addDefenceMove); + board_LoadMoves(x, y, -1, -1, 1, addDefenceMove); + board_LoadMoves(x, y, 0, -1, 1, addDefenceMove); + board_LoadMoves(x, y, 1, -1, 1, addDefenceMove); + board_LoadMoves(x, y, 1, 0, 1, addDefenceMove); + board_LoadMoves(x, y, 1, 1, 1, addDefenceMove); + board_LoadMoves(x, y, 0, 1, 1, addDefenceMove); + board_LoadMoves(x, y, -1, 1, 1, addDefenceMove); +} + +/*-----------------------------------------------------------------------*/ +void board_GenQueenMoves(char position, char addDefenceMove) +{ + char y = position / 8, x = position & 7; + + board_LoadMoves(x, y, -1, 0, 8, addDefenceMove); + board_LoadMoves(x, y, -1, -1, 8, addDefenceMove); + board_LoadMoves(x, y, 0, -1, 8, addDefenceMove); + board_LoadMoves(x, y, 1, -1, 8, addDefenceMove); + board_LoadMoves(x, y, 1, 0, 8, addDefenceMove); + board_LoadMoves(x, y, 1, 1, 8, addDefenceMove); + board_LoadMoves(x, y, 0, 1, 8, addDefenceMove); + board_LoadMoves(x, y, -1, 1, 8, addDefenceMove); +} + +/*-----------------------------------------------------------------------*/ +// This is the function that will move pieces from gTile[0] to gTile[1] +// if the move is valid. It will make sure pieces dont' do illegal moves +char board_ProcessAction(void) +{ + // Get the king's tile and an offset to the king's attackers + char kingTile = gKingData[gColor[0]]; + + // If the king is moving onto a tile under attack the move is invalid + // Can't move into check + if(KING == gPiece[0] && gpAttackBoard[giAttackBoardOffset[gTile[1]][1-gColor[0]]]) + return OUTCOME_INVALID; + + if(KING == gPiece[0]) + { + kingTile = gTile[1]; + // See if the King is trying to castle + board_ProcessCastling(3, 2); + } + else if(PAWN == gPiece[0]) + { + if(gEPPawn == gTile[1]) + board_ProcessEnPassant(ENPASSANT_TAKE); + else if(gTile[1] < 8 || gTile[1] > 55) + { + if(!gAI) + gpChessBoard[gTile[0]] = frontend_GetPromotion() | (gpChessBoard[gTile[0]] & PIECE_EXTRA_DATA); + else + gpChessBoard[gTile[0]] = QUEEN | (gpChessBoard[gTile[0]] & PIECE_EXTRA_DATA); + } + } + + // Make the move effective + gpChessBoard[gTile[1]] = gpChessBoard[gTile[0]]; + gpChessBoard[gTile[0]] = NONE; + + // Recalculate the attack DB for the post-move board + board_PlacePieceAttacks(); + + // If the king is (still) under attack, the move isn't valid. Can't move/leave king in check + if(gpAttackBoard[giAttackBoardOffset[kingTile][1-gColor[0]]]) + { + // restore the board, incl. reversing a casteling move or unpromoting a pawn, etc. + gpChessBoard[gTile[0]] = gpChessBoard[gTile[1]]; + gpChessBoard[gTile[1]] = gPiece[1] | (gColor[1] << 7) | gMove[1]; + if(PAWN == gPiece[0]) + { + if(gEPPawn == gTile[1]) + { + gpChessBoard[gTile[2]] = PAWN | ((1-gColor[0]) ? PIECE_WHITE : 0); + gTile[2] = NULL_TILE; + } + else if(gTile[1] < 8 || gTile[1] > 55) + gpChessBoard[gTile[0]] = PAWN | (gpChessBoard[gTile[0]] & PIECE_EXTRA_DATA); + } + else if(KING == gPiece[0]) + board_ProcessCastling(2, 3); + + // Set the attack DB back to how it was before the move was put into effect + board_PlacePieceAttacks(); + // Regenerate the possible moves for the selected piece as it was destroyed by PlacePieceAttacks + board_GeneratePossibleMoves(gTile[0], 0); + + return OUTCOME_INVALID; + } + + // Reset the en passant decoy at every move + gEPPawn = NULL_TILE; + + // Update the special king-tile-tracker + if(PAWN == gPiece[0]) + { + if(!gMove[0]) + board_ProcessEnPassant(ENPASSANT_MAYBE); + } + else if(KING == gPiece[0]) + gKingData[gColor[0]] = gTile[1]; + + // The move succeeded so mark the piece as having moved + gpChessBoard[gTile[1]] |= PIECE_MOVED; + + // If the opposing king is in check + if(gpAttackBoard[giAttackBoardOffset[gKingData[1-gColor[0]]][gColor[0]]]) + { + sc_numFixes = 0; + // Check if that king is in check-mate + return board_CheckForMate(1-gColor[0]); + } + + return OUTCOME_OK; +} + +/*-----------------------------------------------------------------------*/ +// Use Bresenham's algorithm to see what tiles lie +// between the attacker and the king +// not for use with a knight attacker +char board_CheckLineAttack(char t1, char t2, char side) +{ + int offset; + signed char y1, x1, y2, x2, dx, dy, sx, sy, err, e2; + char piece, defenders; + + y1 = t1 / 8; + x1 = t1 & 7; + y2 = t2 / 8; + x2 = t2 & 7; + + dx = abs(x2-x1); + dy = abs(y2-y1); + + if(x1 < x2) + sx = 1; + else + sx = -1; + + if(y1 < y2) + sy = 1; + else + sy = -1; + + err = dx - dy; + + // for every block from the attacker to the king, consider + while(1) + { + if(x1 == x2 && y1 == y2) + break; + + offset = giAttackBoardOffset[t1][side]; + + // How many of the king's pieces attack this block, and what is on the block + defenders = gpAttackBoard[offset]; + piece = gpChessBoard[t1] & PIECE_DATA; + + if(defenders) + { + char i; + + // For every defender of the king, see if they can break the check-mate + for(i=1; i<=defenders; ++i) + { + // Who is defending and what tile is the defender on + char defTile = gpAttackBoard[offset+i]; + char defPiece = gpChessBoard[defTile] & PIECE_DATA; + + if(PAWN == defPiece) + { + // see if the pawn is in a move or attack position relative to the + // path of the attacker + char d1 = (defTile+8), d2 = (defTile-8); + + // If it's the path, not the attacker self + if(NONE == piece) + { + // if the pawn can move onto the path then it's not check-mate + if(d1 == t1 || d2 == t1) + return OUTCOME_CHECK; + } + else + { + // if it's the attacker itself and the pawn can take it, then + // it's not check-mate + if(d1 != t1 && d2 != t1) + return OUTCOME_CHECK; + } + } + // if the defender is the king and the code is here, it means + // the attacker has backup (or the code won't get here) and the + // king can't take the attacker (because he would still be under attack). + // For any other defender, the defender can take the attacker and + // it's not check-mate + else if(KING != defPiece) + return OUTCOME_CHECK; + } + } + + e2 = err << 1; + if(e2 > -dy) + { + err = err - dy; + x1 = x1 + sx; + } + if(e2 < dx) + { + err = err + dx; + y1 = y1 + sy; + } + t1 = MK_POS(y1, x1); + } + + return OUTCOME_CHECKMATE; +} + +/*-----------------------------------------------------------------------*/ +// Make sure gAttackBoard has all the moves in it that's needed to determine a check-mate +// Offset is in the attack DB for the attackers of the king on "side" +void board_UpdateAttackGrid(int offset, char side) +{ + char i, j, piece, color, tile, king, attackers, numAttackers, *attackPieces; + + // Get a handle to the pieces that are attacking the king, causing check + numAttackers = gpAttackBoard[offset]; + attackPieces = &gpAttackBoard[offset+1]; + + // Backup the king and remove him from the board + tile = gKingData[side]; + king = gpChessBoard[tile]; + gpChessBoard[tile] = NONE; + + for(i=0; i<64; ++i) + { + piece = gpChessBoard[i]; + if(piece != NONE) + { + color = (piece & PIECE_WHITE) >> 7; + piece &= PIECE_DATA; + + // if the piece is one of the kings' pawns, now a move forward + // is also an "attack" (or a defensive move) that can cut the + // path from the attacker to the king, so add these moves + if(PAWN == piece && color == side) + { + signed char di, d = side ? -8 : 8; + + di = i+d; + if(di < 0 || di > 63) + continue; + + // Check the square in front of the pawn + if(NONE == gpChessBoard[di]) + { + offset = giAttackBoardOffset[di][color]; + attackers = gpAttackBoard[offset] + 1; + gpAttackBoard[offset+attackers] = i; + // Keep a log of all modifications so this can be "unwound" at the end + // to return gAttackBoard to the in-game state without re-generating from + // scratch + ++gpAttackBoard[offset]; + si_fixupTable[sc_numFixes++] = offset; + + // and also 2 in front of, if the pawn hasn't moved yet + // if(((side && (i >= 48 && i <= 55)) || (!side && (i >= 8 && i <= 15))) && NONE == gpChessBoard[i+(2*d)]) + if(!(gpChessBoard[i] & PIECE_MOVED) && NONE == gpChessBoard[i+(2*d)]) + { + offset += (d*ATTACK_WIDTH); + attackers = gpAttackBoard[offset] + 1; + gpAttackBoard[offset+attackers] = i; + ++gpAttackBoard[offset]; + si_fixupTable[sc_numFixes++] = offset; + } + } + } + // If the piece is an enemy piece, and not a knight + else if(color != side && piece != KNIGHT) + { + // Is the piece directly attacking the king + if(board_findInList(attackPieces, numAttackers, i)) + { + // Get a list of all blocks this piece is attacking - + // this list will include blocks "behind" the king, since + // the king has been removed + board_GeneratePossibleMoves(i, 1); + j = 0; + while(j < gNumMoves) + { + offset = giAttackBoardOffset[gPossibleMoves[j++]][color]; + attackers = gpAttackBoard[offset]; + // See if this tile has already been marked as being attacked by this piece + if(!board_findInList(&gpAttackBoard[offset+1], attackers, i)) + { + // If not, mark it as being under attack from this piece - + // these are the tiles "behind" the king + si_fixupTable[sc_numFixes++] = offset; + ++gpAttackBoard[offset++]; + gpAttackBoard[offset+attackers] = i; + } + } + } + } + } + } + + // Put the king back on the board + gpChessBoard[tile] = king; +} + +/*-----------------------------------------------------------------------*/ +void board_ProcessEnPassant(char state) +{ + switch(state) + { + case ENPASSANT_TAKE: // Take the pawn + if((gTile[0] & 7) < (gTile[1] & 7)) + gTile[2] = gTile[0] + 1; + else + gTile[2] = gTile[0] - 1; + + gpChessBoard[gTile[2]] = NONE; + break; + + case ENPASSANT_UNTAKE: // Reverse 1; Restore the pawn + if((gTile[0] & 7) < (gTile[1] & 7)) + gTile[2] = gTile[0] + 1; + else + gTile[2] = gTile[0] - 1; + + gpChessBoard[gTile[2]] = PAWN | ((1-gColor[0]) ? PIECE_WHITE : 0); + gEPPawn = gTile[1]; + break; + + case ENPASSANT_MAYBE: // See if the move forwards creates an en passant opportunity + { + char x0 = (gTile[0] / 8), x1 = (gTile[1] / 8); + + if(x0 < x1) + { + if(x0 == x1 - 2) + gEPPawn = gTile[0] + 8; + } + else + { + if(x0 == x1 + 2) + gEPPawn = gTile[0] - 8; + } + } + break; + } +} + +/*-----------------------------------------------------------------------*/ +// This moves the rook and marks its move bit if the king is moving +// to cause a "casteling" move. +// Can be called with the parameters reversed to undo the casteling, +// whcih also clears the move bit on the rook. +void board_ProcessCastling(char a, char b) +{ + gTile[2] = gTile[3] = NULL_TILE; + + // If the king hasn't moved and is now moving + // 2 spaces, it's already known that the rook hasn't moved + // because that's the only way this shows up as + // a valid move, so no need to check the rook. The reason + // to check the king is to make sure this is the + // first move for the king, otherwise this code should + // not do anything. + if(gTile[1] == gKingMovingTo[gColor[0]][0] && !gMove[0]) + { + gTile[2] = gTile[1] - 2; + gTile[3] = gTile[1] + 1; + } + else if(gTile[1] == gKingMovingTo[gColor[0]][1] && !gMove[0]) + { + gTile[2] = gTile[1] + 1; + gTile[3] = gTile[1] - 1; + } + + if(NULL_TILE != gTile[2]) + { + gpChessBoard[gTile[a]] = gpChessBoard[gTile[b]]; + if(a > b) + gpChessBoard[gTile[a]] |= PIECE_MOVED; + else + gpChessBoard[gTile[a]] &= ~PIECE_MOVED; + + gpChessBoard[gTile[b]] = NONE; + } +} + +/*-----------------------------------------------------------------------*/ +// This is called to see if the king on "side" is in check-mate +char board_CheckForMate(char side) +{ + int offset; + char i, tile, other = 1-side; + + // Look at the king's attackers + tile = gKingData[side]; + offset = giAttackBoardOffset[tile][other]; + + // Make gAttackBoard contain all needed attacks and defences + // to determine a check mate state + board_UpdateAttackGrid(offset, side); + + // See where the king might hide + board_GeneratePossibleMoves(tile, 0); + + // This also triggers if the attacker has no backup and the + // king can take it + for(i=0; i 1) + return OUTCOME_CHECKMATE; + + // There's only 1 attacker, so work with it + tile = gpAttackBoard[offset+1]; + + // Deal with a knight attacker here as there is no "interruptable path" + // between a horse attacker and the king + if(KNIGHT == (gpChessBoard[tile] & PIECE_DATA)) + { + // Get an offset to the defenders ("attackers" on same side as king) + offset = giAttackBoardOffset[tile][side]; + + // See if the Horse can be taken by any defenders + if(gpAttackBoard[offset]) + return OUTCOME_CHECK; + + // If not then it's check-mate + return OUTCOME_CHECKMATE; + } + + // See if the attacker can be eliminated or the path between the attacker + // and the king can be blocked + i = board_CheckLineAttack(tile, gKingData[side], side); + + // Clean up changes it made to the attack DB in board_UpdateAttackGrid + while(sc_numFixes) + { + --sc_numFixes; + --gpAttackBoard[si_fixupTable[sc_numFixes]]; + } + + return i; +} + +/*-----------------------------------------------------------------------*/ +// Utility function to find a number in a series of numbers +char board_findInList(char *list, char numElements, char number) +{ + while(numElements--) + { + if(number == list[numElements]) + return 1; + } + return 0; +} diff --git a/src/board.h b/src/board.h new file mode 100644 index 0000000..5e7bce8 --- /dev/null +++ b/src/board.h @@ -0,0 +1,21 @@ +/* + * board.h + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#ifndef _BOARD_H_ +#define _BOARD_H_ + +void board_Init(); +void board_PlacePieceAttacks(); +void board_GeneratePossibleMoves(char position, char addDefenceMove); +char board_ProcessAction(void); +void board_ProcessEnPassant(char state); +void board_ProcessCastling(char a, char b); +char board_CheckForMate(char side); +char board_findInList(char *list, char numElements, char number); + +#endif //_BOARD_H_ \ No newline at end of file diff --git a/src/c64/chess.cfg b/src/c64/chess.cfg new file mode 100644 index 0000000..7556eb1 --- /dev/null +++ b/src/c64/chess.cfg @@ -0,0 +1,39 @@ +SYMBOLS { + __LOADADDR__: type = import; + __EXEHDR__: type = import; + __STACKSIZE__: type = weak, value = $0800; # 2k stack +} +MEMORY { + ZP: file = "", define = yes, start = $0002, size = $001A; + LOADADDR: file = %O, start = $07FF, size = $0002; + HEADER: file = %O, start = $0801, size = $000C; + RAM1: file = %O, define = yes, start = $080D, size = $7BF3; + RAM: file = %O, define = yes, start = $9000, size = $3C00 - __STACKSIZE__; +} +SEGMENTS { + LOADADDR: load = LOADADDR, type = ro; + EXEHDR: load = HEADER, type = ro; + STARTUP: load = RAM1, type = ro; + LOWCODE: load = RAM1, type = ro, optional = yes; + INIT: load = RAM1, type = ro, define = yes, optional = yes; + CODE: load = RAM1, type = ro; + RODATA: load = RAM1, type = ro; + DATA: load = RAM1, type = rw; + ZPSAVE: load = RAM, type = bss, define = yes; + BSS: load = RAM, type = bss, define = yes; + ZEROPAGE: load = ZP, type = zp; +} +FEATURES { + CONDES: segment = INIT, + type = constructor, + label = __CONSTRUCTOR_TABLE__, + count = __CONSTRUCTOR_COUNT__; + CONDES: segment = RODATA, + type = destructor, + label = __DESTRUCTOR_TABLE__, + count = __DESTRUCTOR_COUNT__; + CONDES: segment = RODATA, + type = interruptor, + label = __INTERRUPTOR_TABLE__, + count = __INTERRUPTOR_COUNT__; +} diff --git a/src/c64/data.c b/src/c64/data.c new file mode 100644 index 0000000..992eae7 --- /dev/null +++ b/src/c64/data.c @@ -0,0 +1,326 @@ +/* + * data.c + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#include "../types.h" +#include "data.h" + +/*-----------------------------------------------------------------------*/ +// C64 specific graphics for the chess pieces + +// The comment shows an icon 16x24 +// The data is 24 chars & 24 chars (2 * 8x24). The second set of 24 +// is the right-hand side 8-bits of the icon + +// The pieces use bit pattern 11 as the transparent color, which uses +// the color from the color memory. Bit pattern 01 is the piece color +// and uses bgcolor1 for black pieces. In code, 01 is switched to 10 +// for white pieces. 10 uses bgcolor2 for its color +const char gfxTiles[PAWN][2*24] = +{ + { + 0xff, /* XXXXXXXXXXXXXXXX */ // 0 + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xdd, /* XX-XXX-X-XXX-XXX */ + 0xdd, /* XX-XXX-X-XXX-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ /* 48 */ + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0x77, + 0x77, + 0x57, + 0x57, + 0x57, + 0x5f, + 0x5f, + 0x5f, + 0x5f, + 0x5f, + 0x5f, + 0x5f, + 0x5f, + 0x5f, + 0x57, + 0x57, + }, + { + 0xff, /* XXXXXXXXXXXXXXXX */ // 1 + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXX-XXXXX */ + 0xff, /* XXXXXXXX-X-XXXXX */ + 0xfd, /* XXXXXX-X-X-X-XXX */ + 0xf5, /* XXXX-X-X-X-X-XXX */ + 0xf7, /* XXXX-XXX-X-X-XXX */ + 0xd7, /* XX-X-XXX-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xdd, /* XX-XXX-X-X-XXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ /* 96 */ + 0xff, + 0xff, + 0xff, + 0xff, + 0xdf, + 0x5f, + 0x57, + 0x57, + 0x57, + 0x57, + 0x57, + 0x57, + 0x57, + 0x57, + 0x5f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x5f, + 0x5f, + 0x5f, + 0x57, + 0x57, + }, + { + 0xff, /* XXXXXXXXXXXXXXXX */ // 2 + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xf5, /* XXXX-X-XXXXXXXXX */ + 0xf7, /* XXXX-XXXXX-XXXXX */ + 0xf7, /* XXXX-XXX-X-XXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ /* 144 */ + 0xff, + 0xff, + 0xff, + 0x7f, + 0xff, + 0xdf, + 0x5f, + 0x7f, + 0x7f, + 0x7f, + 0x5f, + 0x5f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x5f, + 0x5f, + 0x57, + 0x57, + 0x57, + }, + { + 0xff, /* XXXXXXXXXXXXXXXX */ // 3 + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ /* 192 */ + 0xff, + 0xff, + 0x7f, + 0x5f, + 0x57, + 0x57, + 0x57, + 0x7f, + 0x5f, + 0x57, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x5f, + 0x57, + 0x57, + 0x57, + }, + { + 0xff, /* XXXXXXXXXXXXXXXX */ // 4 + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ /* 240 */ + 0xff, + 0x7f, + 0x7f, + 0x7f, + 0x57, + 0x57, + 0x7f, + 0x7f, + 0x7f, + 0x5f, + 0x5f, + 0x5f, + 0x5f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x5f, + 0x5f, + 0x57, + 0x57, + 0x57, + }, + { + 0xff, /* XXXXXXXXXXXXXXXX */ // 5 + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xff, /* XXXXXXXXXXXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xfd, /* XXXXXX-X-XXXXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xf5, /* XXXX-X-X-X-XXXXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ + 0xd5, /* XX-X-X-X-X-X-XXX */ /* 288 */ + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0xff, + 0x7f, + 0x5f, + 0x5f, + 0x5f, + 0x7f, + 0x7f, + 0x5f, + 0x5f, + 0x7f, + 0x7f, + 0x7f, + 0x7f, + 0x5f, + 0x5f, + 0x57, + 0x57, + } +}; +const int gfxTileSetSize = sizeof(gfxTiles) / sizeof(gfxTiles[0]); diff --git a/src/c64/data.h b/src/c64/data.h new file mode 100644 index 0000000..a98fddf --- /dev/null +++ b/src/c64/data.h @@ -0,0 +1,15 @@ +/* + * data.h + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#ifndef _DATA_H_ +#define _DATA_H_ + +extern const char gfxTiles[PAWN][2*24]; +extern const int gfxTileSetSize; + +#endif //_DATA_H_ \ No newline at end of file diff --git a/src/c64/plat64.c b/src/c64/plat64.c new file mode 100644 index 0000000..a0d8ddd --- /dev/null +++ b/src/c64/plat64.c @@ -0,0 +1,567 @@ +/* + * plat64.c + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#include +#include +#include +#include "../types.h" +#include "../globals.h" +#include "../undo.h" +#include "../frontend.h" +#include "../plat.h" +#include "data.h" + +/*-----------------------------------------------------------------------*/ +// System locations +#define VIC_BASE_RAM (0x8000) +#define SCREEN_RAM ((char*)VIC_BASE_RAM+0x0400) +#define CHARMAP_RAM ((char*)VIC_BASE_RAM+0x0800) +#define COLOUR_RAM ((char*)0xd800) +#define MEM_KRNL_PRNT ((char*)0x288) + +/*-----------------------------------------------------------------------*/ +#define BOARD_PIECE_WIDTH 4 +#define BOARD_PIECE_HEIGHT 3 +#define SCREEN_WIDTH 40 +#define SCREEN_HEIGHT 25 +#define COLOR_OFFSET (int)(COLOUR_RAM - SCREEN_RAM) +#define FONT_INVERSE 0x80 +#define FONT_SPACE 0x20 +#define BOARD_BLOCK (FONT_INVERSE + FONT_SPACE) +#define SCROLL_SPEED 0xf5 +#define LOG_WINDOW_HEIGHT SCREEN_HEIGHT-2 +#define CHARMAP_DEST (BOARD_BLOCK + 1) + +/*-----------------------------------------------------------------------*/ +// Internal function Prototype +char plat_TimeExpired(unsigned int aTime, char *timerInit); + +/*-----------------------------------------------------------------------*/ +// The charactres to make the [ and ] around the highlighted pieces +static char brackets[4] = {240,237,238,253}; + +/*-----------------------------------------------------------------------*/ +// These variables hold copies of system info that needs to be restored +// before returning back to DOS +static char sc_ddra, sc_pra, sc_vad, sc_ct2, sc_vbc, sc_vbg0, sc_vbg1, sc_vbg2, sc_mcp, sc_txt, sc_x, sc_y; +static char gColor_ram[SCREEN_HEIGHT*SCREEN_WIDTH]; + +/*-----------------------------------------------------------------------*/ +// Called one-time to set up the platform (or computer or whatever) +void plat_Init() +{ + int i; + char c; + + // Setting this to 0 will not show the "Quit" option in the main menu + gReturnToOS = 1; + + if(gReturnToOS) + { + // Save these values so they can be restored + memcpy(gColor_ram, COLOUR_RAM, SCREEN_HEIGHT*SCREEN_WIDTH); + sc_ddra = CIA2.ddra; + sc_pra = CIA2.pra; + sc_vad = VIC.addr; + sc_ct2 = VIC.ctrl2; + sc_vbc = VIC.bordercolor; + sc_vbg0 = VIC.bgcolor0; + sc_vbg1 = VIC.bgcolor1; + sc_vbg2 = VIC.bgcolor2; + sc_mcp = *MEM_KRNL_PRNT; + sc_x = wherex(); + sc_y = wherey(); + } + + // Set up a user defined font, and move the screen to the appropriate position + CIA2.ddra |= 0x03; + CIA2.pra = (CIA2.pra & 0xfc) | (3-(VIC_BASE_RAM / 0x4000)); + VIC.addr = ((((int)(SCREEN_RAM - VIC_BASE_RAM) / 0x0400) << 4) + (((int)(CHARMAP_RAM - VIC_BASE_RAM) / 0x0800) << 1)); + VIC.ctrl2 |= 16; + VIC.bordercolor = VIC.bgcolor0 = COLOR_LIGHTBLUE; + VIC.bgcolor1 = COLOR_RED; + VIC.bgcolor2 = COLOR_GRAY3; + + *MEM_KRNL_PRNT = (int)SCREEN_RAM / 256; + + // Save and set the text color + sc_txt = textcolor(COLOR_BLUE); + clrscr(); + + // Copy the standard font to where the redefined char font will live + CIA1.cra = (CIA1.cra & 0xfe); + *(char*)0x01 = *(char*)0x01 & 0xfb; + memcpy(CHARMAP_RAM,COLOUR_RAM,256*8); + *(char*)0x01 = *(char*)0x01 | 0x04; + CIA1.cra = (CIA1.cra | 0x01); + + // copy the charactes to make the chess pieces font. + memcpy(&CHARMAP_RAM[8*CHARMAP_DEST], &gfxTiles[0][0], sizeof(gfxTiles)); + + // For the second (white) set, set all 01 bits to 10 so it uses bgcolor2 + for(i=0; i maxLen) + maxLen = len; + } + + // Centre on the screen + sy = MAX_SIZE(0, (SCREEN_HEIGHT / 2) - (height / 2) - 1); + sx = MAX_SIZE(0, (SCREEN_WIDTH / 2) - (maxLen / 2) - 1); + maxLen = MIN_SIZE(SCREEN_WIDTH-2, maxLen); + + // Show the title + textcolor(COLOR_GREEN); + gotoxy(sx, sy); + cprintf(" %.*s ",maxLen, menuItems[0]); + + // Leave a blank line + textcolor(COLOR_BLACK); + gotoxy(sx, ++sy); + for(j=0; j%.*s<",maxLen, menuItems[i]); + textcolor(COLOR_BLACK); + + // Look for user input + keyMask = plat_ReadKeys(0); + + if(keyMask & INPUT_MOTION) + { + // selection changes so de-highlight the selected item + gotoxy(sx, sy+i); + cprintf(" %.*s ",maxLen, menuItems[i]); + + // see if the selection goes up or down + switch(keyMask & INPUT_MOTION) + { + case INPUT_UP: + if(!--i) + i = numMenuItems-1; + break; + + case INPUT_DOWN: + if(numMenuItems == ++i) + i = 1; + break; + } + } + keyMask &= (INPUT_SELECT | INPUT_BACKUP); + + // Show the scroller + gotoxy(sx,sy+height); + textcolor(COLOR_CYAN); + cprintf(" %.*s ",maxLen, pScroller); + + // Wrap the message if needed + if((pEnd - pScroller) < maxLen-1) + { + gotoxy(sx+(pEnd-pScroller)+1,sy+height); + cprintf(" %.*s ",maxLen-(pEnd - pScroller)-1, scroller); + } + + // Only update the scrolling when needed + if(plat_TimeExpired(SCROLL_SPEED, &timerInit)) + { + ++pScroller; + if(!*pScroller) + pScroller = scroller; + } + } while(keyMask != INPUT_SELECT && keyMask != INPUT_BACKUP); + + // if backing out of the menu, return 0 + if(keyMask & INPUT_BACKUP) + return 0; + + // return the selection + return i; +} + +/*-----------------------------------------------------------------------*/ +// Draw the chess board and possibly clear the log section +void plat_DrawBoard(char clearLog) +{ + char i; + + if(clearLog) + { + for(i=0; i<25; ++i) + memset(SCREEN_RAM+1+(i*SCREEN_WIDTH)+(8*BOARD_PIECE_WIDTH), FONT_SPACE, SCREEN_WIDTH-1-(8*BOARD_PIECE_WIDTH)); + } + + // redraw all tiles + for(i=0; i<64; ++i) + plat_DrawSquare(i); + + // Add the A..H and 1..8 tile-keys + textcolor(COLOR_GREEN); + for(i=0; i<8; ++i) + { + gotoxy(3+i*BOARD_PIECE_WIDTH,0); + cprintf("%c",'A'+i); + gotoxy(0,2+i*BOARD_PIECE_HEIGHT); + cprintf("%d",8-i); + } +} + +/*-----------------------------------------------------------------------*/ +// Draw a tile with background and piece on it for positions 0..63 +void plat_DrawSquare(char position) +{ + char piece, color, dx, dy; + char y = position / 8, x = position & 7; + char* drawDest = (char*)SCREEN_RAM+SCREEN_WIDTH+(y*SCREEN_WIDTH*BOARD_PIECE_HEIGHT)+(x*BOARD_PIECE_WIDTH)+1; + char blackWhite = !((x & 1) ^ (y & 1)); + + if(blackWhite) + color = COLOR_WHITE; + else + color = COLOR_BLACK; + + // Unsofisticated draw + for(dy=0; dy 8 are multi-color mode colors + drawDest += 1; + } + drawDest += (SCREEN_WIDTH - BOARD_PIECE_WIDTH); + } + + // Show the attack numbers + if(gShowAttackBoard) + { + textcolor(COLOR_RED); + gotoxy(1+x*BOARD_PIECE_WIDTH,(y+1)*BOARD_PIECE_HEIGHT); + cprintf("%d",(gpAttackBoard[giAttackBoardOffset[position][0]])); + textcolor(COLOR_YELLOW); + gotoxy(1+x*BOARD_PIECE_WIDTH+3,(y+1)*BOARD_PIECE_HEIGHT); + cprintf("%d",(gpAttackBoard[giAttackBoardOffset[position][1]])); + gotoxy(1+x*BOARD_PIECE_WIDTH,1+y*BOARD_PIECE_HEIGHT); + cprintf("%02X",gChessBoard[y][x]); + gotoxy(1+x*BOARD_PIECE_WIDTH+3,1+y*BOARD_PIECE_HEIGHT); + cprintf("%d",(gChessBoard[y][x]&PIECE_WHITE)>>7); + } + + drawDest -= ((BOARD_PIECE_HEIGHT * SCREEN_WIDTH)-1); + + // Get the piece data to draw the piece over the tile + piece = gChessBoard[y][x]; + color = piece & PIECE_WHITE; + piece &= PIECE_DATA; + + if(piece) + { + // The white pieces are 36 characters after the start of the black pieces + // (6 pieces at 6 characters each to draw a piece) + if(color) + color = 36; + + color += CHARMAP_DEST+(piece-1)*6; + + drawDest[0] = color; + drawDest[1] = color+3; + drawDest[SCREEN_WIDTH] = color+1; + drawDest[SCREEN_WIDTH+1] = color+4; + drawDest[2*SCREEN_WIDTH] = color+2; + drawDest[2*SCREEN_WIDTH+1] = color+5; + } +} + +/*-----------------------------------------------------------------------*/ +void plat_ShowSideToGoLabel(char side) +{ + gotoxy(2+8*BOARD_PIECE_WIDTH,0); + textcolor(side); + cprintf("%s",gszSideLabel[side]); +} + +/*-----------------------------------------------------------------------*/ +void plat_Highlight(char position, char color) +{ + char y = position / 8, x = position & 7; + + char *drawDest = (char*)SCREEN_RAM+SCREEN_WIDTH+(y*SCREEN_WIDTH*BOARD_PIECE_HEIGHT)+(x*BOARD_PIECE_WIDTH)+1; + + drawDest[COLOR_OFFSET] = color; + drawDest[COLOR_OFFSET+SCREEN_WIDTH] = color; + drawDest[COLOR_OFFSET+2*SCREEN_WIDTH] = color; + + drawDest[COLOR_OFFSET+BOARD_PIECE_WIDTH-1] = color; + drawDest[COLOR_OFFSET+SCREEN_WIDTH+BOARD_PIECE_WIDTH-1] = color; + drawDest[COLOR_OFFSET+2*SCREEN_WIDTH+BOARD_PIECE_WIDTH-1] = color; + + drawDest[0] = brackets[0]; + drawDest[2*SCREEN_WIDTH] = brackets[1]; + + drawDest[BOARD_PIECE_WIDTH-1] = brackets[2]; + drawDest[2*SCREEN_WIDTH+BOARD_PIECE_WIDTH-1] = brackets[3]; +} + +/*-----------------------------------------------------------------------*/ +void plat_ShowMessage(char *str, char color) +{ + gotoxy(1+(8*BOARD_PIECE_WIDTH),SCREEN_HEIGHT-1); + textcolor(color); + cprintf("%.*s",SCREEN_WIDTH-1-(8*BOARD_PIECE_WIDTH),str); +} + +/*-----------------------------------------------------------------------*/ +void plat_ClearMessage() +{ + memset(SCREEN_RAM+1+(SCREEN_HEIGHT-1)*SCREEN_WIDTH+8*BOARD_PIECE_WIDTH,FONT_SPACE,SCREEN_WIDTH-1-8*BOARD_PIECE_WIDTH); +} + +/*-----------------------------------------------------------------------*/ +void plat_AddToLogWin() +{ + char i; + + // Scroll the log up + for(i=2; i1; --i) + { + memcpy(SCREEN_RAM+1+(i*SCREEN_WIDTH)+(8*BOARD_PIECE_WIDTH), SCREEN_RAM+1+((i-1)*SCREEN_WIDTH)+(8*BOARD_PIECE_WIDTH), SCREEN_WIDTH-1-(8*BOARD_PIECE_WIDTH)); + memcpy(COLOR_OFFSET+SCREEN_RAM+1+(i*SCREEN_WIDTH)+(8*BOARD_PIECE_WIDTH), COLOR_OFFSET+SCREEN_RAM+1+((i-1)*SCREEN_WIDTH)+(8*BOARD_PIECE_WIDTH), SCREEN_WIDTH-1-(8*BOARD_PIECE_WIDTH)); + } + memset(SCREEN_RAM+1+SCREEN_WIDTH+8*BOARD_PIECE_WIDTH,FONT_SPACE,SCREEN_WIDTH-1-8*BOARD_PIECE_WIDTH); + + // If an older entry is there to become visible, add it at the top of the log + if(undo_FindUndoLine(SCREEN_HEIGHT-3)) + { + frontend_FormatLogString(); + gotoxy(2+(8*BOARD_PIECE_WIDTH),1); + textcolor(gColor[0] ? HCOLOR_WHITE : HCOLOR_BLACK); + cprintf("%.*s",SCREEN_WIDTH-2-(8*BOARD_PIECE_WIDTH),gLogStrBuffer); + } +} + +/*-----------------------------------------------------------------------*/ +// Use timer B to time a duration +char plat_TimeExpired(unsigned int aTime, char *timerInit) +{ + if(!*timerInit || (CIA1.tb_lo < aTime)) + { + *timerInit = 1; + + CIA1.crb &= 0xfe; + CIA1.tb_lo = 0xff; + CIA1.tb_hi = 0xff; + CIA1.crb |= 0x41; + + return 1; + } + return 0; +} + +/*-----------------------------------------------------------------------*/ +int plat_ReadKeys(char blocking) +{ + char key = 0; + int keyMask = 0; + + if(blocking || kbhit()) + key = cgetc(); + else + return 0; + + switch(key) + { + case 145: // Up + keyMask |= INPUT_UP; + break; + + case 29: // Right + keyMask |= INPUT_RIGHT; + break; + + case 17: // Down + keyMask |= INPUT_DOWN; + break; + + case 157: // Left + keyMask |= INPUT_LEFT; + break; + + case 3: // Esc + keyMask |= INPUT_BACKUP; + break; + + case 65: // 'a' - Show Attackers + keyMask |= INPUT_TOGGLE_A; + break; + + case 66: // 'b' - Board attacks - Show all attacks + keyMask |= INPUT_TOGGLE_B; + break; + + case 68: // 'd' - Show Defenders + keyMask |= INPUT_TOGGLE_D; + break; + + case 77: // 'm' - Menu + keyMask |= INPUT_MENU; + break; + + case 13: // Enter + keyMask |= INPUT_SELECT; + break; + + case 82: + keyMask |= INPUT_REDO; + break; + + case 85: + keyMask |= INPUT_UNDO; + break; + + // default: // Debug - show key code + // { + // char s[] = "Key:000"; + // s[4] = (key/100)+'0'; + // key -= (s[4] - '0') * 100; + // s[5] = (key/10)+'0'; + // s[6] = (key%10)+'0'; + // plat_ShowMessage(s,COLOR_RED); + // } + break; + } + + return keyMask; +} + +/*-----------------------------------------------------------------------*/ +// Restore the C64 to the state it was in before RUN was typed. Only +// ever gets called if gReturnToOS is true +void plat_Shutdown() +{ + CIA2.ddra = sc_ddra; + CIA2.pra = sc_pra; + VIC.addr = sc_vad; + VIC.ctrl2 = sc_ct2; + VIC.bordercolor = sc_vbc; + VIC.bgcolor0 = sc_vbg0; + VIC.bgcolor1 = sc_vbg1; + VIC.bgcolor2 = sc_vbg2; + *MEM_KRNL_PRNT = sc_mcp; + CIA1.crb &= 0xfe; + CIA1.tb_lo = 0xff; + CIA1.tb_hi = 0xff; + CIA1.crb = 0x8; + + textcolor(sc_txt); + gotoxy(sc_x, sc_y); + + memcpy(COLOUR_RAM, gColor_ram, SCREEN_HEIGHT*SCREEN_WIDTH); + + // didn't want to make a new crt0.s just for this but if the C64 + // was aleady in lowercase before run, then this is wrong ;) + __asm__("lda #142"); + __asm__("jsr $ffd2"); +} diff --git a/src/cpu.c b/src/cpu.c new file mode 100644 index 0000000..c9faf66 --- /dev/null +++ b/src/cpu.c @@ -0,0 +1,713 @@ +/* + * cpu.c + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#include +#include +#include "types.h" +#include "globals.h" +#include "undo.h" +#include "board.h" +#include "cpu.h" +#include "frontend.h" +#include "plat.h" + +/*-----------------------------------------------------------------------*/ +// Used to keep a sorted array of scores with source-destination tiles +typedef struct tagScore +{ + char m_piece; + char m_source; + char m_dest; + int m_score; +} t_score; + +/*-----------------------------------------------------------------------*/ +// Internal function Prototype +void cpu_AddScoreSorted(t_score array[], char width, char source, char dest, int score); +void cpu_InitPieceData(char side); +void cpu_HolisticScore(char side); +int cpu_SourceScore(char position); +int cpu_DestScore(char position, char destination); +void cpu_ScorePieceMoves(); +int cpu_FindBestOpponentMove(char side, char *from, char *to); +int cpu_ScorePieceSubTree(char level, char side, signed char sign, char src, char dst); + +/*-----------------------------------------------------------------------*/ +static const signed char scsc_neighbours[] = {-9,-8,-7,-1,1,7,8,9}; +static t_score sts_pieceScores[NUM_PIECES_SIDE], sts_moveScore[MAX_PIECE_MOVES]; +static char sc_index[64], sc_myMoves[MAX_PIECE_MOVES], sc_numMyMoves; + +/*-----------------------------------------------------------------------*/ +// Insert scores into an array making sure valid destinations end up +// at the head (index[0]), sorted by score +void cpu_AddScoreSorted(t_score array[], char width, char source, char dest, int score) +{ + char i; + + if(NULL_TILE == dest) + return; + + for(i=0; i= array[i].m_score) + { + memmove(&array[i+1], &array[i], ((width-1)-i)*sizeof(t_score)); + array[i].m_source = source; + array[i].m_dest = dest; + array[i].m_score = score; + break; + } + } +} + +/*-----------------------------------------------------------------------*/ +// Init two helper data structures +void cpu_InitPieceData(char side) +{ + char i, piece, iPiece = 0; + + // Init the twho helper arrays. The sc_index is easily removed + // but having it takes less space than the code to do the + // lookups on the C64 so it saves space in RAM1 + memset(sc_index, NULL_TILE, 64); + memset(sts_pieceScores, NULL_TILE, sizeof(sts_pieceScores)); + + for(i=0;i<64;++i) + { + piece = gpChessBoard[i]; + if(NONE != piece) + { + if((piece & PIECE_WHITE) >> 7 == side) + { + // if the piece is on the side the AI is running for, set + // index to point back at the iPiece'th number of the piece (iPiece'th + // being where it was encountered in the scan of the board from tile 0 to 63) + // and init the sts_pieceScores for that iPiece number + sc_index[i] = iPiece; + sts_pieceScores[iPiece].m_piece = piece & PIECE_DATA; + sts_pieceScores[iPiece].m_source = i; + sts_pieceScores[iPiece].m_score = 0; + ++iPiece; + } + } + } +} + +/*-----------------------------------------------------------------------*/ +// This routine is where any code that looks at the global board should +// go. What's here now does only this: a) If the king doesn't have a valid +// move (like the start of the game), encourage any pieces next to the +// king, on his side, to move so the king has an escape; b) encourage +// pawns to keep moving forward, more so the closer they get to +// promotion. +// This routine probably needs a lot more to make the chess any good, +// for example looking at freeing up a pawn stuck behind a pawn of its +// own color, or getting a sense for where the pressure will be and +// putting a defence in place +void cpu_HolisticScore(char side) +{ + int offset; + char i, nTile, nPiece, scratch, src; + + // Get the king + src = gKingData[side]; + board_GeneratePossibleMoves(src, 0); + + // If the king has nowhere to go + if(!gNumMoves) + { + // encourage a neighbour to make space for the king + for(i=0; i<8; ++i) + { + // For each neighbour + nTile = src+scsc_neighbours[i]; + if(nTile<64) + { + // See if it's a piece and if the neighbour piece is on the same side as the king + nPiece = sc_index[nTile]; + if(NULL_TILE != nPiece) + { + // See who's attacking it + offset = giAttackBoardOffset[nTile][1-side]; + scratch = gpAttackBoard[offset]; + // if it has no attackers, encourage it to move + if(!scratch) + sts_pieceScores[nPiece].m_score = 4; + // if the neighbour is a pawn and the pawn has one attacker + else if(1 == scratch && PAWN == sts_pieceScores[nPiece].m_piece) + { + // Get the tile of the attacker + scratch = gpAttackBoard[offset+1]; + // if it is a vertical (head-on) attack, encourage the pawn to step forward + if((nTile & 7) == (scratch & 7)) + sts_pieceScores[nPiece].m_score = 4; + } + } + } + } + } + + // Take a look at the pawns + for(i=0;i> 7; + piece &= PIECE_DATA; + value = gPieceValues[piece]; + other = 1-color; + + // If this piece is under attack increase chance to move, else decrease + if(gpAttackBoard[giAttackBoardOffset[position][other]]) + score += value; + else + score -= 1; + + // If this piece is being defended, decrease chance to move, else increase + if(gpAttackBoard[giAttackBoardOffset[position][color]]) + score -= 1; + else + score += 1; + + // Generate moves, including defend/attack moves + board_GeneratePossibleMoves(position, 1); + + found = 0; + for(i=0;i> 7 == color) + { + found = 1; + // discourage move + score -= 1; + + // Is the piece being supported under attack + if(gpAttackBoard[giAttackBoardOffset[tile][other]]) + { + // Is position the only defender of the piece under attack, then discourace moving + if(1 == gpAttackBoard[giAttackBoardOffset[tile][color]]) + score -= 2; + else + score += 1; + + // If the supported piece is more valuable than the position piece then discourage move + if(gPieceValues[targetPiece & PIECE_DATA] > value) + score -= 2; + else + score += 1; + } + } + } + } + // If not supporting a piece, encourage a move + if(!found) + score += 1; + + return score; +} + +/*-----------------------------------------------------------------------*/ +// This routine scores a piece should it move from position to destination +int cpu_DestScore(char position, char destination) +{ + char i, color, piece, value, tile, targetPiece, other; + int score = 0; + + // extract the piece at position + piece = gpChessBoard[position]; + color = (piece & PIECE_WHITE) >> 7; + piece &= PIECE_DATA; + value = gPieceValues[piece]; + other = 1-color; + + // If this piece will be under attack decrease chance to move + if(gpAttackBoard[giAttackBoardOffset[destination][other]]) + score -= value; + + // Will be defended + if(gpAttackBoard[giAttackBoardOffset[destination][color]]) + score += 1; + else + score -= 1; + + // If a piece is taken at destination, the score is the value of the piece + targetPiece = gpChessBoard[destination]; + if(NONE != targetPiece && color != ((targetPiece & PIECE_WHITE) >> 7)) + score += gPieceValues[targetPiece & PIECE_DATA]; + + // "Move" the piece to destination + gpChessBoard[destination] = gpChessBoard[position]; + // Generate moves, including defend/attack moves + board_GeneratePossibleMoves(destination, 1); + // Restore the destination piece + gpChessBoard[destination] = targetPiece; + + for(i=0;i> 7 == color) + { + // encourage move + score += 1; + + // Is the piece being supported under attack + if(gpAttackBoard[giAttackBoardOffset[tile][other]]) + { + // Encourage move + score += 1; + + // Well dest be the only defender of the piece under attack, then encourage moving + if(1 == gpAttackBoard[giAttackBoardOffset[tile][color]]) + score += 2; + + // If the supported piece is more valuable than the position piece then encourage move + if(gPieceValues[targetPiece & PIECE_DATA] > value) + score += 1; + } + } + else + { + // Attacking a piece on the other team from dest is encouraged + score += 1; + + // If the piece attacked is of greater value than self + targetPiece = gPieceValues[targetPiece & PIECE_DATA]; + if(targetPiece > value) + score += targetPiece - value; + + // The piece that will be attacked has no support + if(!gpAttackBoard[giAttackBoardOffset[tile][other]]) + score += 2; + } + } + } + + return score; +} + +/*-----------------------------------------------------------------------*/ +void cpu_ScorePieceMoves() +{ + char i, j, src, dest; + int sourceScore; + + // Look at all the pieces on this side, using the support data structure + for(i=0;i> 7; + gColor[1] = (gPiece[1] & PIECE_WHITE) >> 7; + gMove[0] = gPiece[0] & PIECE_MOVED; + gMove[1] = gPiece[1] & PIECE_MOVED; + gPiece[0] &= PIECE_DATA; + gPiece[1] &= PIECE_DATA; + + // See if the move is valid + if(OUTCOME_INVALID != (gOutcome = board_ProcessAction())) + { + // if it was a valid move, add it to undo, and immediately undo the move + undo_AddMove(); + undo_Undo(); + } + else + ++j; + } + + // If a valid move was found, add it as the move for this piece + if(OUTCOME_INVALID != gOutcome) + { + // Add the dest and score as the best (highest scoring) + // move for this piece on this side. + sts_pieceScores[i].m_dest = sts_moveScore[j].m_dest; + sts_pieceScores[i].m_score += sts_moveScore[j].m_score; + } + } +} + +/*-----------------------------------------------------------------------*/ +// This function looks at all the pieces on a side and finds the top +// scoring move, tracking the top MAX_PIECE_MOVES moves, one of which +// is hopefully actually valid +int cpu_FindBestOpponentMove(char side, char *from, char *to) +{ + char i, j, piece, color, move; + int score; + + memset(sts_moveScore, NULL_TILE, sizeof(sts_moveScore)); + + for(i=0;i<64;++i) + { + piece = gpChessBoard[i]; + if(NONE != piece) + { + color = (piece & PIECE_WHITE) >> 7; + if(side == color) + { + // For every piece on "side" generate moves + board_GeneratePossibleMoves(i, 0); + + if(!gNumMoves) + continue; + + memcpy(sc_myMoves, gPossibleMoves, gNumMoves); + sc_numMyMoves = gNumMoves; + + // Score the source + score = cpu_SourceScore(i); + for(j = 0; j> 7; + gColor[1] = (gPiece[1] & PIECE_WHITE) >> 7; + gMove[0] = gPiece[0] & PIECE_MOVED; + gMove[1] = gPiece[1] & PIECE_MOVED; + gPiece[0] &= PIECE_DATA; + gPiece[1] &= PIECE_DATA; + + // Do the move + if(OUTCOME_INVALID != (gOutcome = board_ProcessAction())) + { + // if it was a valid move, add it to undo, and immediately undo the move + undo_AddMove(); + undo_Undo(); + } + else + ++j; + } + // If no valid moves are found, return a score of INT_MIN/2 + if(MAX_PIECE_MOVES == j) + return INT_MIN/2; + } + *from = sts_moveScore[j].m_source; + *to = sts_moveScore[j].m_dest; + return sts_moveScore[j].m_score; +} + +/*-----------------------------------------------------------------------*/ +// This function effects a move and then looks for the best move on the +// opposing team. It will recursively call itself with the outcome +// up to gMaxLevel's. The sign flips between 1 and -1. The score is +// multiplied by the sign so you (1), opponent (-1), you (1), etc. will +// calc a score for your team subtracting victories for the opponent +// in the deeper levels but adding in your own victories in those deeper +// levels, generating a single comprhensive score for the impact of this move +int cpu_ScorePieceSubTree(char level, char side, signed char sign, char src, char dst) +{ + int score; + char pieces[2], from, to, col; + + // This move is known (maybe only assumed -based on deepThinking) + // to be valid. Back up the pieces and make the move + pieces[0] = gpChessBoard[src]; + pieces[1] = gpChessBoard[dst]; + // If the king is taken, stop looking + if(KING == (pieces[1] & PIECE_DATA)) + return gPieceValues[KING]; + + // Make the move, set the move bit and update the king tracker if needed + gpChessBoard[src] = NONE; + gpChessBoard[dst] = pieces[0] | PIECE_MOVED; + if(KING == (pieces[0] & PIECE_DATA)) + gKingData[(col = (pieces[0] & PIECE_WHITE)>>7)] = dst; + + // See if this move creates an en passant opportunity for the opposing team + gEPPawn = NULL_TILE; + if(!(pieces[0] & PIECE_MOVED) && PAWN == (pieces[0] & PIECE_DATA)) + { + gTile[0] = src; + gTile[1] = dst; + board_ProcessEnPassant(ENPASSANT_MAYBE); + } + + // If deep thinking, update the attack db to be accurate + if(gDeepThoughts) + board_PlacePieceAttacks(); + + // Find what the opponent will likely do + score = cpu_FindBestOpponentMove(1-side, &from, &to); + if(score > INT_MIN/2) + { + // Invert for opposing side + score = (score * sign); + // Then follow that move for gMaxLevel's deep + if(level < gMaxLevel) + score += cpu_ScorePieceSubTree(level+1, 1-side, sign ^ 0xfe, from, to); + } + else + { + // INT_MIN/2 is artificial for a dead-end sub-tree. + // I haven't thought this through + score = (INT_MIN/2) * sign; + } + + // restore the backed up pieces, including possibly the king-tracker + gpChessBoard[src] = pieces[0]; + gpChessBoard[dst] = pieces[1]; + if(KING == (pieces[0] & PIECE_DATA)) + gKingData[col] = src; + + return score; +} + +/*-----------------------------------------------------------------------*/ +// main entry point for the AI play +char cpu_Play(char side) +{ + t_score ts_pieceScore[NUM_PIECES_SIDE]; + char i, best; + int iBest = INT_MIN; + + // This is realy just to avoid the pop-up for pawn promotion + gAI = 1; + + // Set up some helper structures + cpu_InitPieceData(side); + + // Take a holistic view of the board + cpu_HolisticScore(side); + + // Get a score for all pieces on the AI side + cpu_ScorePieceMoves(); + + memset(ts_pieceScore, NULL_TILE, sizeof(ts_pieceScore)); + + // Sort the AI piece's scores by score and validity + for(i=0;i= iBest) + { + best = i; + iBest = ts_pieceScore[i].m_score; + } + } + + // If deep thinking, update the attack db to be accurate + if(gDeepThoughts) + board_PlacePieceAttacks(); + + // If the destination move is not valid, there's no valid move so stalemate + if(NULL_TILE == ts_pieceScore[best].m_dest) + { + char tile[2], piece[2], color[2], move[2], outcome; + + // See what the last move was + undo_FindUndoLine(0); + + // if the outcome was already something other than OK (like check-mate) + // then nothing more needs happen here + if(OUTCOME_OK == gOutcome) + { + // a hack of sorts to make the outcome in the undo-stack be + // stalemate so the correct outcome is displayed along the + // moved that caused it, which was actually the last move + // the opponent made, but it has only been detected now. + + // save the undo state of the last move + tile[0] = gTile[0]; + tile[1] = gTile[1]; + piece[0] = gPiece[0]; + piece[1] = gPiece[1]; + color[0] = gColor[0]; + color[1] = gColor[1]; + move[0] = gMove[0]; + move[1] = gMove[1]; + outcome = gOutcome; + + // undo the move that caused stalemate + undo_Undo(); + frontend_LogMove(1); + + // restore that state, but set the outcome to + // OUTCOME_STALEMATE + gTile[0] = tile[0]; + gTile[1] = tile[1]; + gPiece[0] = piece[0]; + gPiece[1] = piece[1]; + gColor[0] = color[0]; + gColor[1] = color[1]; + gMove[0] = move[0]; + gMove[1] = move[1]; + board_ProcessAction(); + gOutcome = OUTCOME_STALEMATE; + + // Add this as the move + undo_AddMove(); + + // Log the move to update the display + frontend_LogMove(0); + } + + // will exit so turn off the flag + gAI = 0; + + // returning stalemate, even if it's already check-mate is + // fine as it only means the side-to-go status isn't changed + return OUTCOME_STALEMATE; + } + + // Set up to do board_ProcessAction + gTile[0] = ts_pieceScore[best].m_source; + gTile[1] = ts_pieceScore[best].m_dest; + gPiece[0] = gpChessBoard[gTile[0]]; + gPiece[1] = gpChessBoard[gTile[1]]; + gColor[0] = (gPiece[0] & PIECE_WHITE) >> 7; + gColor[1] = (gPiece[1] & PIECE_WHITE) >> 7; + gMove[0] = (gPiece[0] & PIECE_MOVED); + gMove[1] = (gPiece[1] & PIECE_MOVED); + gPiece[0] &= PIECE_DATA; + gPiece[1] &= PIECE_DATA; + + // The move will have been previously tested so will be valid + if(OUTCOME_INVALID != (gOutcome = board_ProcessAction())) + { + // If no piece is taken, up the counter or reset it to zero + if(NONE != (gPiece[1] & PIECE_DATA)) + gMoveCounter = 0; + else if(NUM_MOVES_TO_DRAW == ++gMoveCounter) + gOutcome = OUTCOME_DRAW; + + // Add the AI move to the undo stack + undo_AddMove(); + + // Update the display + plat_DrawSquare(gTile[0]); + plat_DrawSquare(gTile[1]); + if(NULL_TILE != gTile[2]) + { + plat_DrawSquare(gTile[2]); + gTile[2] = NULL_TILE; + } + if(NULL_TILE != gTile[3]) + { + plat_DrawSquare(gTile[3]); + gTile[3] = NULL_TILE; + } + + // Log the move + frontend_LogMove(0); + } + + gAI = 0; + return gOutcome; +} diff --git a/src/cpu.h b/src/cpu.h new file mode 100644 index 0000000..7c809af --- /dev/null +++ b/src/cpu.h @@ -0,0 +1,14 @@ +/* + * cpu.h + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#ifndef _CPU_H_ +#define _CPU_H_ + +char cpu_Play(char side); + +#endif //_CPU_H_ \ No newline at end of file diff --git a/src/frontend.c b/src/frontend.c new file mode 100644 index 0000000..aec95c8 --- /dev/null +++ b/src/frontend.c @@ -0,0 +1,174 @@ +/* + * frontend.c + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#include "types.h" +#include "globals.h" +#include "plat.h" +#include "frontend.h" + +/*-----------------------------------------------------------------------*/ +// All menu's in the games are the same height +#define MENU_HEIGHT 6 + +/*-----------------------------------------------------------------------*/ +// Show the main menu and deal with the selection. If a confirmation or +// side-selection is needed, show that and deal with that oucome as well. +char frontend_Menu(char activeGame) +{ + char outcome = OUTCOME_MENU; + + // If there's a game in progress, show the "Resume" option + if(activeGame) + { + gMainMenu[4] = gszQuit; + gMainMenu[5] = gszResume; + } + else + { + // if no game in progress, only show the quit (to dos) option + // if okay to do so + gMainMenu[4+gReturnToOS] = 0; + } + + while(OUTCOME_MENU == outcome) + { + // Show the main menu + switch(plat_Menu(gMainMenu, MENU_HEIGHT, gszAbout)) + { + // back-up + case 0: + if(activeGame) + outcome = OUTCOME_OK; + else + outcome = OUTCOME_QUIT; + break; + + // 1 player + case 1: + // Show the menu allowing the user to choose a side + switch(plat_Menu(gColorMenu, MENU_HEIGHT, gszAbout)) + { + case 1: + gUserMode = USER_WHITE; + outcome = OUTCOME_OK; + break; + + case 2: + gUserMode = USER_BLACK; + outcome = OUTCOME_OK; + break; + } + break; + + // 2 human players + case 2: + gUserMode = USER_BLACK | USER_WHITE; + outcome = OUTCOME_OK; + break; + + // Both players AI + case 3: + gUserMode = 0; + outcome = OUTCOME_OK; + break; + + // Quit + case 4: + outcome = OUTCOME_QUIT; + break; + + // Resume + case 5: + outcome = OUTCOME_OK; + break; + } + if(OUTCOME_QUIT == outcome) + { + // Get confirmation of quit + if(1 != plat_Menu(gAreYouSureMenu, MENU_HEIGHT, gszAbout)) + outcome = OUTCOME_MENU; + else if(activeGame) + outcome = OUTCOME_ABANDON; + } + else + { + if(gUserMode != 3) + { + char skill = plat_Menu(gSkillMenu, MENU_HEIGHT, gszAbout); + if(skill) + { + skill -= 1; + skill *= 3; + gWidth = gSkill[skill]; + gMaxLevel = gSkill[skill+1]; + gDeepThoughts = gSkill[skill+2]; + } + else + outcome = OUTCOME_MENU; + } + } + } + // Redraw the whole board, erasing the menu + plat_DrawBoard(0); + return outcome; +} + +/*-----------------------------------------------------------------------*/ +// Show a menu seeing what the user wants to do with the pawn that reached +// the other side +char frontend_GetPromotion() +{ + char promotion; + + switch(plat_Menu(gPromoteMenu, MENU_HEIGHT, gszpromote)) + { + case 2: + promotion = ROOK; + break; + + case 3: + promotion = BISHOP; + break; + + case 4: + promotion = KNIGHT; + break; + + default: + promotion = QUEEN; + break; + } + plat_DrawBoard(0); + return promotion; +} + +/*-----------------------------------------------------------------------*/ +// This formats the log entry. It's here because it's display related +void frontend_FormatLogString() +{ + gLogStrBuffer[0] = 'A' + (gTile[0] & 7); + gLogStrBuffer[1] = '8' - (gTile[0] / 8); + gLogStrBuffer[2] = (gPiece[1] & PIECE_DATA) != NONE ? 'x' : '-'; + gLogStrBuffer[3] = 'A' + (gTile[1] & 7); + gLogStrBuffer[4] = '8' - (gTile[1] / 8); + gLogStrBuffer[5] = gMoveSymbol[gOutcome-1]; +} + +/*-----------------------------------------------------------------------*/ +// Call the platform functions to draw the log update - at the bottom +// or top depending on undo (top) or new/redo (bottom) +// The plat_* functions probably call undo_FindUndoLine which changes +// the global gTile, etc. variables +void frontend_LogMove(char atTop) +{ + if(atTop) + plat_AddToLogWinTop(); + else + plat_AddToLogWin(); +} + diff --git a/src/frontend.h b/src/frontend.h new file mode 100644 index 0000000..0952130 --- /dev/null +++ b/src/frontend.h @@ -0,0 +1,17 @@ +/* + * frontend.h + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#ifndef _FRONTEND_H_ +#define _FRONTEND_H_ + +char frontend_Menu(char activeGame); +char frontend_GetPromotion(); +void frontend_FormatLogString(); +void frontend_LogMove(char atTop); + +#endif //_FRONTEND_H_ \ No newline at end of file diff --git a/src/globals.c b/src/globals.c new file mode 100644 index 0000000..eceaefd --- /dev/null +++ b/src/globals.c @@ -0,0 +1,76 @@ +/* + * globals.c + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#include "types.h" +#include "globals.h" + +/*-----------------------------------------------------------------------*/ +char gChessBoard[8][8]; // The chess board itself +char* gpChessBoard = &gChessBoard[0][0]; // With a linear address alias + +// This is an 8x8 grid with a 2-part array for each cell. At [8][8][0] is the +// number of entries to follow between [y][x]][0] and [y][x][17]. At [y][x][17] +// is the number of entries between [y][x][17] and [y][x+1][0]. These between +// entries are tiles of black [0] or white [17] that can land on this tile to +// take (or defend) whatever piece is on the tile. I refer to this as the +// Attack DB on the code +char gAttackBoard[8][8][ATTACK_WIDTH]; +char* gpAttackBoard = &gAttackBoard[0][0][0]; // Linear version +int giAttackBoardOffset[64][2]; // Offsets to the [0]'s in the AttackDB +char gUserMode; // =0, all AI; =1, Black is user; =2, White; =3, both +char gAI; // Prevents AI code from asking for pawn promotion +char gMoveCounter; // Moves without a piece taken for 50 move rule +char gTile[4]; // [0] = src tile, 1 = to dst +char gPiece[2]; // [0] = piece at src, [1] = piece at dst +char gMove[2]; // [0] = piece0 moved or no, ditto [1] on piece1 +char gColor[2]; // [0] =1 white piece0, =0 black; [1] ditto on piece1 +char gOutcome; // Result of board_ProcessAction for log. Bad design +char gEPPawn; // Tile where a pawn can be taken en passant or NULL_TILE +char gMaxLevel; // How many levels down the AI thinks ahead. 0 = self and opponent +char gWidth; // How many of the top ranked moves for self tried on think ahead +char gDeepThoughts; // =1 - moves are check and AttackDB updated; =0 - inaccurate but faster +char gReturnToOS; // =1 can quit game; =0 cannot quit game +char gPossibleMoves[MAX_PIECE_MOVES]; // All the tiles a piece can get to from current pos +char gNumMoves; // Entries in gPossibleMoves +char gCursorPos[2][2]; // Remember last cursor pos for human players +char gKingData[2]; // Tracks the king's tile on the board +char gShowAttackBoard; // Visibility toggle +char gShowAttacks[2]; // Visibility toggle per side +char gLogStrBuffer[7]; // String placeholder for the movve log +char gKingMovingTo[2][2] = {{2,6},{58,62}}; // Tiles where a king lands when casteling +char gSkill[4*3] = {1,0,0,16,1,0,16,2,1,16,3,1}; // Values for gWidth, gMaxLevel & gDeepThoughts over 4 skills + +// Values AI sees when looking at pieces; +2 compensates for other move hints and 3 * makes the +// piece value much more meaningful +char gPieceValues[PAWN+1] = { + (0), // NONE + 2+(3*5), // ROOK, + 2+(3*3), // KNIGHT, + 2+(3*3), // BISHOP, + 2+(3*10), // KING, + 2+(3*9), // QUEEN, + 2+(3*1), // PAWN, +}; + +/*-----------------------------------------------------------------------*/ +// All Display Strings +char gMoveSymbol[OUTCOME_STALEMATE] = {'\0', '+', '#', '/', '!'}; +char gszNoUndo[] = "No Undo"; +char gszNoRedo[] = "No Redo"; +char gszInvalid[] = "Invalid"; +char gszAbout[] = "cc65 Chess V1.0 by S. Wessels, 2014. "; +char gszResume[] = "Resume Game "; +char gszQuit[] = "Quit Game "; +char gszSelect[] = " Select "; +char gszpromote[] = "Select a rank to promote the pawn to. "; +char* gMainMenu[] = {gszSelect, "1 Human player ","2 Human players","Both players AI",gszQuit, 0, 0}; +char* gSkillMenu[] = {gszSelect, " Very Easy "," Easy "," Harder "," Very Hard ", 0}; +char* gColorMenu[] = {gszSelect," Play White "," Play Black ", 0}; +char* gAreYouSureMenu[] = {" Are you sure? "," Absolutely! "," Not so much ",0}; +char* gPromoteMenu[] = {"Promotion", " Queen ", " Rook ", " Bishop ", " Knight ", 0}; +char* gszSideLabel[2] = {"Black", "White"}; diff --git a/src/globals.h b/src/globals.h new file mode 100644 index 0000000..c60dc10 --- /dev/null +++ b/src/globals.h @@ -0,0 +1,58 @@ +/* + * globals.h + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#ifndef _GLOBALS_H_ +#define _GLOBALS_H_ + +extern char gChessBoard[8][8]; +extern char* gpChessBoard; +extern char gAttackBoard[8][8][ATTACK_WIDTH]; +extern char* gpAttackBoard; +extern int giAttackBoardOffset[64][2]; +extern char gUserMode; +extern char gAI; +extern char gMoveCounter; +extern char gTile[4]; +extern char gPiece[2]; +extern char gMove[2]; +extern char gColor[2]; +extern char gOutcome; +extern char gEPPawn; +extern char gMaxLevel; +extern char gWidth; +extern char gDeepThoughts; +extern char gReturnToOS; +extern char gPossibleMoves[MAX_PIECE_MOVES]; +extern char gNumMoves; +extern char gCursorPos[2][2]; +extern char gKingData[2]; +extern char gShowAttackBoard; +extern char gShowAttacks[2]; +extern char gLogStrBuffer[7]; +extern char gKingMovingTo[2][2]; +extern char gSkill[4*3]; +extern char gPieceValues[PAWN+1]; + +/*-----------------------------------------------------------------------*/ +// Display labels +extern char gMoveSymbol[OUTCOME_STALEMATE]; +extern char gszNoUndo[]; +extern char gszNoRedo[]; +extern char gszInvalid[]; +extern char gszAbout[]; +extern char gszResume[]; +extern char gszQuit[]; +extern char gszpromote[]; +extern char* gMainMenu[]; +extern char* gSkillMenu[]; +extern char* gColorMenu[]; +extern char* gAreYouSureMenu[]; +extern char* gPromoteMenu[]; +extern char* gszSideLabel[2]; + +#endif //_GLOBALS_H_ \ No newline at end of file diff --git a/src/human.c b/src/human.c new file mode 100644 index 0000000..fcb19ac --- /dev/null +++ b/src/human.c @@ -0,0 +1,343 @@ +/* + * human.c + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#include "types.h" +#include "globals.h" +#include "undo.h" +#include "board.h" +#include "human.h" +#include "frontend.h" +#include "plat.h" + +/*-----------------------------------------------------------------------*/ +// Internal function Prototype +void human_ProcessInput(int keyMask); +void human_ProcessToggle(int keyMask, char side, char tile); + +/*-----------------------------------------------------------------------*/ +// Track the user controlled curson on the board +static char sc_cursorX, sc_cursorY; + +/*-----------------------------------------------------------------------*/ +// Handle the cursor movement +void human_ProcessInput(int keyMask) +{ + switch(keyMask) + { + case INPUT_UP: + if(!sc_cursorY) + sc_cursorY = 7; + else + --sc_cursorY; + break; + + case INPUT_RIGHT: + if(7==sc_cursorX) + sc_cursorX = 0; + else + ++sc_cursorX; + break; + + case INPUT_DOWN: + if(7==sc_cursorY) + sc_cursorY = 0; + else + ++sc_cursorY; + break; + + case INPUT_LEFT: + if(!sc_cursorX) + sc_cursorX = 7; + else + --sc_cursorX; + break; + } +} + +/*-----------------------------------------------------------------------*/ +// Deal with the toggling on/off of the attack and defend states +// as well as the 'b'oard state which shows all attcks/defences +void human_ProcessToggle(int keyMask, char side, char tile) +{ + char attack = 0; + + switch(keyMask) + { + case INPUT_TOGGLE_B: + gShowAttackBoard = 1 - gShowAttackBoard; + plat_DrawBoard(0); + break; + + case INPUT_TOGGLE_A: + attack = 1; + // Intentional fall-through since the code + // below is the same for showing attack or defence + + case INPUT_TOGGLE_D: + { + int offset; + char attackers, attacker, i; + + gShowAttacks[side] ^= SET_BIT(attack); + + offset = giAttackBoardOffset[tile][0]; + if((side & !attack) || (!side && attack)) + offset += ATTACK_WHITE_OFFSET; + attackers = gpAttackBoard[offset]; + ++offset; + for(i=0;i> 7; + gMove[selector] = gPiece[selector] & PIECE_MOVED; + gPiece[selector] &= PIECE_DATA; + + if(keyMask & INPUT_MOTION) + { + // If no piece selected and the cursor moved, update the possible moves for this tile + if(!selector) + board_GeneratePossibleMoves(gTile[0], 0); + else + { + // If a piece is selected, see if the second tile, under the cursor, is + // a valid move-to tile + validMove = board_findInList(gPossibleMoves, gNumMoves, gTile[1]); + plat_Highlight(gTile[1], validMove ? HCOLOR_ATTACK : HCOLOR_INVALID); + } + + // Show the cursor + plat_Highlight(gTile[0], selector ? HCOLOR_SELECTED : NONE == gPiece[0] || gColor[0] != side ? HCOLOR_EMPTY : gNumMoves ? HCOLOR_VALID : HCOLOR_INVALID); + } + + // If the cursor moved and the toggle-show-attackers/defenders states were on for this side, + // Handle showing them for the now selected tile. Bit 2/3 says it was on, so toggle it on. + // 2/3 is set whem the selection changes, lower in this same routine + if(gShowAttacks[side] & SET_BIT(2)) + { + gShowAttacks[side] &= ~SET_BIT(2); + human_ProcessToggle(INPUT_TOGGLE_D, side, gTile[selector]); + } + + if(gShowAttacks[side] & SET_BIT(3)) + { + gShowAttacks[side] &= ~SET_BIT(3); + human_ProcessToggle(INPUT_TOGGLE_A, side, gTile[selector]); + } + + // Get input + keyMask = plat_ReadKeys(1); + + // Always clear the message area once a key is pressed + plat_ClearMessage(); + + // If the selected tile changes, make sure the toggle-show updates will happen (Set 2/3 bit) + if(keyMask & (INPUT_MOTION | INPUT_UNDOREDO) || ((keyMask & INPUT_SELECT) && selector && validMove)) + { + if(gShowAttacks[side] & SET_BIT(0)) + { + gShowAttacks[side] |= SET_BIT(2); + human_ProcessToggle(INPUT_TOGGLE_D, side, gTile[selector]); + } + + if(gShowAttacks[side] & SET_BIT(1)) + { + gShowAttacks[side] |= SET_BIT(3); + human_ProcessToggle(INPUT_TOGGLE_A, side, gTile[selector]); + } + } + + if(keyMask & INPUT_MOTION) + { + // Erase the cursor and move it + plat_DrawSquare(gTile[selector]); + human_ProcessInput(keyMask & INPUT_MOTION); + } + else if(keyMask & INPUT_MENU) + { + // Drop out so the menu can be displayed + return OUTCOME_MENU; + } + else if(keyMask & INPUT_TOGGLE) + { + // Handle the toggle-show-attackers-defenders-board state chages + human_ProcessToggle(keyMask & INPUT_TOGGLE, side, gTile[selector]); + } + else if(keyMask & INPUT_BACKUP) + { + // If a piece was selected, deselct the piece + if(selector) + { + plat_DrawSquare(gTile[selector]); + selector = 0; + sc_cursorY = gTile[0] / 8; + sc_cursorX = gTile[0] & 7; + keyMask = INPUT_MOTION; + } + // Otherwise bring up the menu + else + return OUTCOME_MENU; + } + else if(keyMask & INPUT_UNDOREDO) + { + // if there's data in the undo/redo buffers to undo or redo, then do the undo/redo + if(((keyMask & INPUT_UNDO) && undo_CanUndo()) || ((keyMask & INPUT_REDO) && undo_CanRedo())) + { + char numUndo = 1; + + // If there's AI, undo 2 moves, to get back to the humans' previous move + if(gUserMode != (USER_BLACK | USER_WHITE)) + numUndo = 2; + + do + { + plat_DrawSquare(gTile[0]); + if(selector) + plat_DrawSquare(gTile[1]); + + if(keyMask & INPUT_UNDO) + undo_Undo(); + else + undo_Redo(); + + plat_DrawSquare(gTile[0]); + plat_DrawSquare(gTile[1]); + if(NULL_TILE != gTile[2]) + { + plat_DrawSquare(gTile[2]); + gTile[2] = NULL_TILE; + } + if(NULL_TILE != gTile[3]) + { + plat_DrawSquare(gTile[3]); + gTile[3] = NULL_TILE; + } + + gCursorPos[1-side][0] = gTile[0] / 8; + gCursorPos[1-side][1] = gTile[0] & 7; + + // Undo scrolls down so updates at the top of the log, redo scrolls up + if(keyMask & INPUT_UNDO) + frontend_LogMove(1); + else + frontend_LogMove(0); + } while(--numUndo); + + // Fix the Attack DB + board_PlacePieceAttacks(); + + // If 2 humans are playing, return so sides can switch + if(gUserMode == (USER_BLACK | USER_WHITE)) + return OUTCOME_OK; + + keyMask = INPUT_MOTION; + } + else + { + // if there's nothing in the undo/redo buffer, show a message to say so + // This could be if all moves have been undone or redone also + plat_ShowMessage((keyMask & INPUT_UNDO) ? gszNoUndo : gszNoRedo, HCOLOR_INVALID); + } + } + else if(keyMask & INPUT_SELECT) + { + + if(!selector && gColor[0] == side && gNumMoves) + { + // If the cursor is on a piece of this turn, that has moved and no other piece + // has yet been selected, then select this piece + ++selector; + keyMask = INPUT_MOTION; + } + else if(gTile[0] == gTile[1] && gColor[0] == side) + { + // If the selected tile is re-selected, deselct it + --selector; + keyMask = INPUT_MOTION; + } + else if(selector && validMove) + { + // If a dest is selected for the selected tile, try to do the move + // gOutcome of 0 is OUTCOME_INVALID, example moving into check + if((gOutcome = board_ProcessAction())) + { + // If no piece was taken, update the move without taking counter + // else reset it to zero + if(NONE != (gPiece[1] & PIECE_DATA)) + gMoveCounter = 0; + else if(NUM_MOVES_TO_DRAW == ++gMoveCounter) + gOutcome = OUTCOME_DRAW; + + // Add the move to the undo stack and update the display + undo_AddMove(); + plat_DrawSquare(gTile[0]); + plat_DrawSquare(gTile[1]); + if(NULL_TILE != gTile[2]) + { + plat_DrawSquare(gTile[2]); + gTile[2] = NULL_TILE; + } + if(NULL_TILE != gTile[3]) + { + plat_DrawSquare(gTile[3]); + gTile[3] = NULL_TILE; + } + + // Log the move + frontend_LogMove(0); + + // This will exit the while loop + ++selector; + } + else + { + // Show a message to indicate that the move was invalid + plat_ShowMessage(gszInvalid,HCOLOR_INVALID); + keyMask = INPUT_MOTION; + } + } + } + // This does nothing on the C64 but some other platforms may need a + // screen refresh - since this function doesn't always fall back to main + plat_UpdateScreen(); + } while(selector < 2); + + // Save the cursor positions for next time + gCursorPos[side][0] = sc_cursorY; + gCursorPos[side][1] = sc_cursorX; + + return OUTCOME_OK; +} diff --git a/src/human.h b/src/human.h new file mode 100644 index 0000000..42f93b9 --- /dev/null +++ b/src/human.h @@ -0,0 +1,14 @@ +/* + * human.h + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#ifndef _HUMAN_H_ +#define _HUMAN_H_ + +char human_Play(char side); + +#endif //_HUMAN_H_ \ No newline at end of file diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..e10cf7f --- /dev/null +++ b/src/main.c @@ -0,0 +1,115 @@ +/* + * main.c + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#include "types.h" +#include "globals.h" +#include "undo.h" +#include "board.h" +#include "cpu.h" +#include "human.h" +#include "frontend.h" +#include "plat.h" + +/*-----------------------------------------------------------------------*/ +// Internal function Prototype +void mainLoop(); +void init(); + +/*-----------------------------------------------------------------------*/ +int main() +{ + init(); + mainLoop(); + plat_Shutdown(); + + return 0; +} + +/*-----------------------------------------------------------------------*/ +void init() +{ + char i; + int offset; + + // Init the global variables that aren't initialized anywhere else + // (mostly other *_Init() functions, or in mainLoop) + gEPPawn = gTile[0] = gTile[1] = gTile[2] = gTile[3] = NULL_TILE; + gLogStrBuffer[6] = gShowAttacks[0] = gShowAttacks[1] = gShowAttackBoard = gNumMoves = gPiece[0] = gPiece[1] = gOutcome = gMove[0] = gMove[1] = gColor[0] = gColor[1] = gAI = 0; + + // Init the lookup table for ecery piece on the board to + // look up directly in gPAttackBoard how many attackers/defender + // there are for the tile, and what tiles they may be + offset = 0; + for(i=0; i<64; ++i) + { + giAttackBoardOffset[i][0] = offset; + giAttackBoardOffset[i][1] = offset+ATTACK_WHITE_OFFSET; + offset += ATTACK_WIDTH; + } + + plat_Init(); +} + +/*-----------------------------------------------------------------------*/ +void mainLoop() +{ + char activeGame, sideToGo, outcome; + + do + { + // Execute once for every game + board_Init(); + undo_Init(); + plat_DrawBoard(1); + + gMoveCounter = 0; + gUserMode = 0; + activeGame = 0; + sideToGo = SIDE_WHITE; + outcome = OUTCOME_MENU; + + while(outcome <= OUTCOME_MENU) + { + // Allows interruption of AI vs AI + if(INPUT_MENU & plat_ReadKeys(0)) + outcome = OUTCOME_MENU; + + if(OUTCOME_MENU == outcome) + { + outcome = frontend_Menu(activeGame); + if(outcome < OUTCOME_ABANDON) + activeGame = 1; + } + + if(outcome <= OUTCOME_STALEMATE) + { + plat_ShowSideToGoLabel(sideToGo); + + if((sideToGo+1) & gUserMode) + outcome = human_Play(sideToGo); + else + outcome = cpu_Play(sideToGo); + + if(gShowAttackBoard) + plat_DrawBoard(0); + + // Only switch sides if not coming from a menu and it's not STALEMATE + if(outcome != OUTCOME_MENU && outcome != OUTCOME_STALEMATE) + sideToGo = 1 - sideToGo; + + // if it's game-over then make it a USER vs USER state so control + // returns no matter which side should have gone next + if(outcome >= OUTCOME_CHECKMATE) + gUserMode = USER_BLACK | USER_WHITE; + + // Any platforms that need to redraw should do so now + plat_UpdateScreen(); + } + } + } while(OUTCOME_QUIT != outcome); +} diff --git a/src/plat.h b/src/plat.h new file mode 100644 index 0000000..c2b0ef6 --- /dev/null +++ b/src/plat.h @@ -0,0 +1,26 @@ +/* + * plat.h + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#ifndef _PLAT_H_ +#define _PLAT_H_ + +void plat_Init(); +void plat_UpdateScreen(); +char plat_Menu(char **menuItems, char height, char *scroller); +void plat_DrawBoard(char clearLog); +void plat_DrawSquare(char position); +void plat_ShowSideToGoLabel(char side); +void plat_Highlight(char position, char color); +void plat_ShowMessage(char *str, char color); +void plat_ClearMessage(); +void plat_AddToLogWin(); +void plat_AddToLogWinTop(); +int plat_ReadKeys(char blocking); +void plat_Shutdown(); + +#endif //_PLAT_H_ \ No newline at end of file diff --git a/src/term/platTerm.c b/src/term/platTerm.c new file mode 100644 index 0000000..659b8cc --- /dev/null +++ b/src/term/platTerm.c @@ -0,0 +1,401 @@ +/* + * platTerm.c + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#include +#include +#include +#include "types.h" +#include "globals.h" +#include "undo.h" +#include "frontend.h" +#include "plat.h" + +/*-----------------------------------------------------------------------*/ +// Internal function Prototype +char plat_TimeExpired(unsigned int aTime); + +/*-----------------------------------------------------------------------*/ +// These are read from the curses library but I assume at least 24 rows +// and 55 cols +int SCREEN_HEIGHT, SCREEN_WIDTH; +int LOG_WINDOW_HEIGHT; + +/*-----------------------------------------------------------------------*/ +#define SCROLL_SPEED 150000 +#define BOARD_PIECE_WIDTH 6 +#define BOARD_PIECE_HEIGHT 3 + +/*-----------------------------------------------------------------------*/ +// Names of the pieces NONE, Rook, knight, Bishop, Queen, King, pawn +static const char sc_pieces[] = {'\0','R','k','B','Q','K','p'}; + +/*-----------------------------------------------------------------------*/ +void plat_Init() +{ + // Curses init + initscr(); + intrflush(stdscr, FALSE); + keypad(stdscr, TRUE); + nonl(); + cbreak(); + noecho(); + timeout(0); + curs_set(0); + getmaxyx(stdscr, SCREEN_HEIGHT, SCREEN_WIDTH) ; + LOG_WINDOW_HEIGHT = SCREEN_HEIGHT - 2; + + if(has_colors() != FALSE) + { + start_color(); + assume_default_colors(COLOR_WHITE, COLOR_BLACK); + init_pair(1, COLOR_BLACK, COLOR_YELLOW); + init_pair(2, COLOR_WHITE, COLOR_BLACK); + init_pair(3, COLOR_GREEN, COLOR_BLACK); + init_pair(4, COLOR_RED, COLOR_BLACK); + init_pair(5, COLOR_CYAN, COLOR_BLACK); + init_pair(6, COLOR_BLUE, COLOR_BLACK); + init_pair(7, COLOR_WHITE, COLOR_WHITE); + init_pair(8, COLOR_BLACK, COLOR_BLACK); + } + + // Setting this to 0 will not show the "Quit" option in the main menu + gReturnToOS = 1; +} + +/*-----------------------------------------------------------------------*/ +void plat_UpdateScreen() +{ + refresh(); +} + +/*-----------------------------------------------------------------------*/ +// Very simple menu with a heading and a scrolling banner as a footer +char plat_Menu(char **menuItems, char height, char *scroller) +{ + static char *prevScroller, *pScroller; + char *pEnd; + int keyMask; + char i, j, sx, sy, numMenuItems, maxLen = 0; + + if(prevScroller != scroller) + { + prevScroller = scroller; + pScroller = scroller; + } + pEnd = scroller + strlen(scroller); + + for(numMenuItems=0; menuItems[numMenuItems]; ++numMenuItems) + { + char len = strlen(menuItems[numMenuItems]); + if(len > maxLen) + maxLen = len; + } + sy = MAX_SIZE(0, ((8*BOARD_PIECE_HEIGHT) / 2) - (height / 2) - 1); + sx = MAX_SIZE(0, ((8*BOARD_PIECE_WIDTH) / 2) - (maxLen / 2) - 1); + maxLen = MIN_SIZE((8*BOARD_PIECE_WIDTH)-2, maxLen); + + color_set(3,0); + move(sy,sx); + printw(" %.*s ",maxLen, menuItems[0]); + color_set(1,0); + move(++sy, sx); + for(j=0; j%.*s<",maxLen, menuItems[i]); + attroff(WA_REVERSE); + color_set(1,0); + keyMask = plat_ReadKeys(0); + if(keyMask & INPUT_MOTION) + { + move(sy+i,sx); + printw(" %.*s ",maxLen, menuItems[i]); + switch(keyMask & INPUT_MOTION) + { + case INPUT_UP: + if(!--i) + i = numMenuItems-1; + break; + + case INPUT_DOWN: + if(numMenuItems == ++i) + i = 1; + break; + } + } + keyMask &= (INPUT_SELECT | INPUT_BACKUP); + + move(sy+height,sx); + color_set(5,0); + printw(" %.*s ",maxLen, pScroller); + if((pEnd - pScroller) < maxLen-1) + { + move(sy+height,sx+(pEnd-pScroller)+1); + printw(" %.*s ",maxLen-(pEnd - pScroller)-1, scroller); + } + + if(plat_TimeExpired(SCROLL_SPEED)) + { + ++pScroller; + if(!*pScroller) + pScroller = scroller; + } + } while(keyMask != INPUT_SELECT && keyMask != INPUT_BACKUP); + + if(keyMask & INPUT_BACKUP) + return 0; + + return i; +} + +/*-----------------------------------------------------------------------*/ +void plat_DrawBoard(char clearLog) +{ + char i; + + if(clearLog) + erase(); + + for(i=0; i<64; ++i) + plat_DrawSquare(i); +} + +/*-----------------------------------------------------------------------*/ +void plat_DrawSquare(char position) +{ + char piece, color, dx, dy; + char y = position / 8, x = position & 7; + char blackWhite = !((x & 1) ^ (y & 1)); + + if(blackWhite) + color = 7; + else + color = 8; + + for(dy=0; dy>7); + } + + piece = gChessBoard[y][x]; + color = piece & PIECE_WHITE; + piece &= PIECE_DATA; + + if(piece) + { + move(y*BOARD_PIECE_HEIGHT+(BOARD_PIECE_HEIGHT/2),x*BOARD_PIECE_WIDTH+(BOARD_PIECE_WIDTH/2)); + color_set(color?2:1,0); + printw("%c",sc_pieces[piece]); + } +} + +/*-----------------------------------------------------------------------*/ +void plat_ShowSideToGoLabel(char side) +{ + move(0, 2+8*BOARD_PIECE_WIDTH); + color_set(side?2:1, 0); + printw("%s",gszSideLabel[side]); +} + +/*-----------------------------------------------------------------------*/ +void plat_Highlight(char position, char color) +{ + char y = (BOARD_PIECE_HEIGHT/2)+1+BOARD_PIECE_HEIGHT*((position / 8)), x = (BOARD_PIECE_WIDTH/2)+BOARD_PIECE_WIDTH*((position & 7)); + move(y,x); + color_set(color,0); + printw("*"); +} + +/*-----------------------------------------------------------------------*/ +void plat_ShowMessage(char *str, char color) +{ + move((8*BOARD_PIECE_HEIGHT)-1, 1+(8*BOARD_PIECE_WIDTH)); + color_set(color,0); + printw("%.*s",SCREEN_WIDTH-1-(8*BOARD_PIECE_WIDTH),str); + clrtoeol(); +} + +/*-----------------------------------------------------------------------*/ +void plat_ClearMessage() +{ + move((8*BOARD_PIECE_HEIGHT)-1, 1+(8*BOARD_PIECE_WIDTH)); + clrtoeol(); +} + +/*-----------------------------------------------------------------------*/ +// This function can/will gange the gTile and related global variables so +// caution is needed +void plat_AddToLogWin() +{ + char bot = (8*BOARD_PIECE_HEIGHT)-2, y = 1, x = 1+(8*BOARD_PIECE_WIDTH); + + for(; y<=bot; ++y) + { + move(y, x); + if(undo_FindUndoLine(bot-y)) + { + frontend_FormatLogString(); + color_set(gColor[0]+1,0); + printw("%.*s",SCREEN_WIDTH-1-x,gLogStrBuffer); + } + clrtoeol(); + } +} + +/*-----------------------------------------------------------------------*/ +void plat_AddToLogWinTop() +{ + plat_AddToLogWin(); +} + +/*-----------------------------------------------------------------------*/ +char plat_TimeExpired(unsigned int aTime) +{ + static struct timeval sst_store; + static int si_init = 0; + struct timeval now; + gettimeofday(&now, NULL); + if(!si_init || (MAX_SIZE(now.tv_usec,sst_store.tv_usec) - MIN_SIZE(now.tv_usec,sst_store.tv_usec) > SCROLL_SPEED)) + { + si_init = 1; + sst_store = now; + return 1; + } + return 0; +} + +/*-----------------------------------------------------------------------*/ +int plat_ReadKeys(char blocking) +{ + char key = 0; + int keyMask = 0; + + if(blocking) + { + timeout(-1) ; + key = getch(); + timeout(0); + } + else + { + key = getch(); + } + + switch(key) + { + case 3: // Up + keyMask |= INPUT_UP; + break; + + case 5: // Right + keyMask |= INPUT_RIGHT; + break; + + case 2: // Down + keyMask |= INPUT_DOWN; + break; + + case 4: // Left + keyMask |= INPUT_LEFT; + break; + + case 27: // Esc + keyMask |= INPUT_BACKUP; + break; + + case 'a': // 'a' - Show Attackers + keyMask |= INPUT_TOGGLE_A; + break; + + case 'b': // 'b' - Board attacks - Show all attacks + keyMask |= INPUT_TOGGLE_B; + break; + + case 'd': // 'd' - Show Defenders + keyMask |= INPUT_TOGGLE_D; + break; + + case 'm': // 'm' - Menu + keyMask |= INPUT_MENU; + break; + + case 13: // Enter + keyMask |= INPUT_SELECT; + break; + + case 'r': + keyMask |= INPUT_REDO; + break; + + case 'u': + keyMask |= INPUT_UNDO; + break; + + // default: // Debug - show key code + // { + // char s[] = "Key:000"; + // if(key != 255) + // { + // s[4] = (key/100)+'0'; + // key -= (s[4] - '0') * 100; + // s[5] = (key/10)+'0'; + // s[6] = (key%10)+'0'; + // plat_ShowMessage(s,COLOR_RED); + // } + // refresh(); + // } + // break; + } + + return keyMask; +} + +/*-----------------------------------------------------------------------*/ +void plat_Shutdown() +{ + endwin(); +} diff --git a/src/types.h b/src/types.h new file mode 100644 index 0000000..da6b04a --- /dev/null +++ b/src/types.h @@ -0,0 +1,91 @@ +/* + * types.h + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#ifndef _TYPES_H_ +#define _TYPES_H_ + +#define SET_BIT(x) (1<<(x)) +#define MAX_SIZE(x,y) ((x)>=(y) ? (x) : (y)) +#define MIN_SIZE(x,y) ((x)<=(y) ? (x) : (y)) +#define MK_POS(y,x) ((y)*8+x) + +#define SIDE_BLACK 0 +#define SIDE_WHITE 1 +#define USER_BLACK 1 +#define USER_WHITE 2 +#define NUM_PIECES_SIDE 16 +#define MAX_PIECE_MOVES 28 +#define NULL_TILE 128 +#define NUM_MOVES_TO_DRAW 50 + +#define HCOLOR_WHITE 1 +#define HCOLOR_BLACK 2 +#define HCOLOR_EMPTY 4 +#define HCOLOR_VALID 5 +#define HCOLOR_INVALID 2 +#define HCOLOR_SELECTED 6 +#define HCOLOR_ATTACK 3 + +#define ATTACK_WIDTH (2+2*NUM_PIECES_SIDE) +#define ATTACK_WHITE_OFFSET 17 + +#define PIECE_WHITE SET_BIT(7) +#define PIECE_MOVED SET_BIT(6) +#define PIECE_EXTRA_DATA (PIECE_WHITE | PIECE_MOVED) +#define PIECE_DATA (SET_BIT(0) | SET_BIT(1) | SET_BIT(2)) + +#define INPUT_UP SET_BIT(0) +#define INPUT_RIGHT SET_BIT(1) +#define INPUT_DOWN SET_BIT(2) +#define INPUT_LEFT SET_BIT(3) +#define INPUT_BACKUP SET_BIT(4) +#define INPUT_TOGGLE_A SET_BIT(5) +#define INPUT_TOGGLE_B SET_BIT(6) +#define INPUT_TOGGLE_D SET_BIT(7) +#define INPUT_SELECT SET_BIT(8) +#define INPUT_MENU SET_BIT(9) +#define INPUT_UNDO SET_BIT(10) +#define INPUT_REDO SET_BIT(11) +#define INPUT_UNDOREDO (INPUT_UNDO | INPUT_REDO) +#define INPUT_MOTION (INPUT_UP | INPUT_RIGHT | INPUT_DOWN | INPUT_LEFT) +#define INPUT_TOGGLE (INPUT_TOGGLE_A | INPUT_TOGGLE_B | INPUT_TOGGLE_D) + +#define PAWN_PROMOTE SET_BIT(1) +#define PAWN_ENPASSANT SET_BIT(4) + +#define ENPASSANT_TAKE 0 +#define ENPASSANT_UNTAKE 1 +#define ENPASSANT_MAYBE 2 + +#define OUTCOME_MASK 0x07 + +enum +{ + NONE, + ROOK, + KNIGHT, + BISHOP, + QUEEN, + KING, + PAWN, +}; + +enum +{ + OUTCOME_INVALID, + OUTCOME_OK, + OUTCOME_CHECK, + OUTCOME_CHECKMATE, + OUTCOME_DRAW, + OUTCOME_STALEMATE, + OUTCOME_MENU, + OUTCOME_ABANDON, + OUTCOME_QUIT, +}; + +#endif //_TYPES_H_ \ No newline at end of file diff --git a/src/undo.c b/src/undo.c new file mode 100644 index 0000000..d861e11 --- /dev/null +++ b/src/undo.c @@ -0,0 +1,222 @@ +/* + * undo.c + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#include "types.h" +#include "globals.h" +#include "undo.h" +#include "board.h" +#include "frontend.h" +#include "plat.h" + +/*-----------------------------------------------------------------------*/ +// The size of the undo buffer. This buffer is a circular buffer and +// access is available to UNDO_STACK_SIZE-1 moves in the past +// This implementation does math with char's so 255 is the max +#define UNDO_STACK_SIZE 255 + +/*-----------------------------------------------------------------------*/ +// m_piece1 is in 2 parts. Bits 0-3 is the piece that was at gPiece[1] +// whereas bits 4-7 is the piece that's there now - which isn't gPiece[0] if +// there was a pawn promotion. +// m_code is a bit structure: Bits are: +// 0 - gColor[0], 1 - gColor[1], 2 - gMove[0], 3 - gMove[1], +// 4 - en passant, 5-7 - gOutcome +typedef struct tag_UndoEntry +{ + char m_tile0; + char m_tile1; + char m_piece0; + char m_piece1; + char m_code; +} t_UndoEntry; + +/*-----------------------------------------------------------------------*/ +// the undo stack and 3 pointers to use it +t_UndoEntry stu_undoStack[UNDO_STACK_SIZE]; +static char sc_undoTop, sc_undoBottom, sc_undoPtr; + +/*-----------------------------------------------------------------------*/ +void undo_Init() +{ + sc_undoTop = sc_undoBottom = sc_undoPtr = 0; +} + +/*-----------------------------------------------------------------------*/ +// Store the globals for tile, piece, color, move and outcome on the undo +// stack +void undo_AddMove() +{ + stu_undoStack[sc_undoPtr].m_tile0 = gTile[0]; + stu_undoStack[sc_undoPtr].m_tile1 = gTile[1]; + stu_undoStack[sc_undoPtr].m_piece0 = gPiece[0]; + stu_undoStack[sc_undoPtr].m_piece1 = gPiece[1]; + // Back up the piece found on the board at position gTile1. If a pawn was promoted + // it's not the same piece that left gTile[0] + stu_undoStack[sc_undoPtr].m_piece1 |= (gpChessBoard[gTile[1]] & PIECE_DATA) << 4; + // Pack the status items into a single char + stu_undoStack[sc_undoPtr].m_code = gColor[0] | (gColor[1] << 1) | (gMove[0] >> 4) | (gMove[1] >> 3) | (gOutcome << 5); + + // if there was an en passant capture, record it + if(gTile[2] != NULL_TILE && gTile[3] == NULL_TILE) + stu_undoStack[sc_undoPtr].m_code |= PAWN_ENPASSANT; + + if(++sc_undoPtr == UNDO_STACK_SIZE) + sc_undoPtr = 0; + + if(sc_undoPtr == sc_undoBottom) + { + if(++sc_undoBottom == UNDO_STACK_SIZE) + sc_undoBottom = 0; + } + + sc_undoTop = sc_undoPtr; +} + +/*-----------------------------------------------------------------------*/ +// This routine doesn't check if an undo is available. canUndo must be +// checked before calling this +void undo_Undo() +{ + if(sc_undoPtr == 0) + sc_undoPtr = UNDO_STACK_SIZE; + + --sc_undoPtr; + + // In the case of an undo the move might have turned off the en passant + // opportunuti created by the previous move, so go look at that move + // to see if it created an en passant opportunity and if it did, reset + // gEPPawn for the opportunuty + gEPPawn = NULL_TILE; + if(undo_FindUndoLine(0)) + { + if(!gMove[0] && PAWN == gPiece[0]) + board_ProcessEnPassant(ENPASSANT_MAYBE); + } + + gTile[0] = stu_undoStack[sc_undoPtr].m_tile0; + gTile[1] = stu_undoStack[sc_undoPtr].m_tile1; + gPiece[0] = stu_undoStack[sc_undoPtr].m_piece0; + // For undo, restore the piece that was at gTile1 before the move + gPiece[1] = stu_undoStack[sc_undoPtr].m_piece1 & PIECE_DATA; + gColor[0] = stu_undoStack[sc_undoPtr].m_code & 1; + gColor[1] = (stu_undoStack[sc_undoPtr].m_code & 2) >> 1; + gMove[0] = (stu_undoStack[sc_undoPtr].m_code & 4) << 4; + gMove[1] = (stu_undoStack[sc_undoPtr].m_code & 8) << 3; + gOutcome = (stu_undoStack[sc_undoPtr].m_code >> 5) & OUTCOME_MASK; + + gpChessBoard[gTile[0]] = gPiece[0] | (gColor[0] << 7) | gMove[0]; + gpChessBoard[gTile[1]] = gPiece[1] | (gColor[1] << 7) | gMove[1]; + + // if en passant set, also restore the en passant taken pawn + if(stu_undoStack[sc_undoPtr].m_code & PAWN_ENPASSANT) + { + board_ProcessEnPassant(ENPASSANT_UNTAKE); + } + else if(KING == gPiece[0]) + { + gKingData[gColor[0]] = gTile[0]; + board_ProcessCastling(2, 3); + } +} + +/*-----------------------------------------------------------------------*/ +void undo_Redo() +{ + gTile[0] = stu_undoStack[sc_undoPtr].m_tile0; + gTile[1] = stu_undoStack[sc_undoPtr].m_tile1; + // Put the piece on gTile1 that was there after the move + gPiece[0] = (stu_undoStack[sc_undoPtr].m_piece1 >> 4) & PIECE_DATA; + gPiece[1] = stu_undoStack[sc_undoPtr].m_piece1 & 0x0f; + gColor[0] = stu_undoStack[sc_undoPtr].m_code & 1; + gColor[1] = (stu_undoStack[sc_undoPtr].m_code & 2) >> 1; + gMove[0] = (stu_undoStack[sc_undoPtr].m_code & 4) << 4; + gMove[1] = (stu_undoStack[sc_undoPtr].m_code & 8) << 3; + gOutcome = (stu_undoStack[sc_undoPtr].m_code >> 5) & OUTCOME_MASK; + + if(stu_undoStack[sc_undoPtr].m_code & PAWN_ENPASSANT) + { + board_ProcessEnPassant(ENPASSANT_TAKE); + } + else if(KING == gPiece[0]) + { + gKingData[gColor[0]] = gTile[1]; + board_ProcessCastling(3, 2); + } + else if(PAWN == gPiece[0]) + { + board_ProcessEnPassant(ENPASSANT_MAYBE); + } + + gpChessBoard[gTile[0]] = NONE; + + // Flag the piece now on gTile1 as having moved, not with the + // move status it had before the move or the status the piece + // on gTile1 had previously + gpChessBoard[gTile[1]] = gPiece[0] | (gColor[0] << 7) | PIECE_MOVED; + + if(++sc_undoPtr == UNDO_STACK_SIZE) + sc_undoPtr = 0; +} + +/*-----------------------------------------------------------------------*/ +// Look backwards in the undo stack to see what the move was, linesBack +// ago. Beware: This function also changes the global gTile, etc. +// variables even though it's just looking back. +char undo_FindUndoLine(char linesBack) +{ + char line; + + // Since the undoPtr is always pointing 1 ahead of the last move, + // asking for linesBack 0 is asing for 1 before the current undoPtr + // Can't ask further back than the # of entries being kept + if(++linesBack >= UNDO_STACK_SIZE) + return 0; + + // check for a wrap in the circular buffer + if(sc_undoPtr < linesBack) + { + // Can't give back entries further back than have been added + if(sc_undoPtr >= sc_undoBottom) + return 0; + + line = UNDO_STACK_SIZE - (linesBack - sc_undoPtr); + } + else + { + line = sc_undoPtr - linesBack; + // Catches the case where the bottom > 0 due to wrap + // and the undoPtr is > bottom due to previous undo's + // and linesBack is durther back than what's available + if(sc_undoBottom <= sc_undoPtr && line < sc_undoBottom) + return 0; + } + + gTile[0] = stu_undoStack[line].m_tile0; + gTile[1] = stu_undoStack[line].m_tile1; + gPiece[0] = stu_undoStack[line].m_piece0; + gPiece[1] = stu_undoStack[line].m_piece1 & 0x0f; + gColor[0] = stu_undoStack[line].m_code & 1; + gColor[1] = (stu_undoStack[line].m_code & 2) >> 1; + gMove[0] = (stu_undoStack[line].m_code & 4) << 4; + gMove[1] = (stu_undoStack[line].m_code & 8) << 3; + gOutcome = (stu_undoStack[line].m_code >> 5) & OUTCOME_MASK; + + return 1; +} + +/*-----------------------------------------------------------------------*/ +char undo_CanUndo() +{ + return sc_undoPtr != sc_undoBottom; +} + +/*-----------------------------------------------------------------------*/ +char undo_CanRedo() +{ + return sc_undoPtr != sc_undoTop; +} diff --git a/src/undo.h b/src/undo.h new file mode 100644 index 0000000..97e8b20 --- /dev/null +++ b/src/undo.h @@ -0,0 +1,20 @@ +/* + * undo.h + * cc65 Chess + * + * Created by Stefan Wessels, February 2014. + * + */ + +#ifndef _UNDO_H_ +#define _UNDO_H_ + +void undo_Init(); +void undo_AddMove(); +void undo_Undo(); +void undo_Redo(); +char undo_FindUndoLine(char linesBack); +char undo_CanUndo(); +char undo_CanRedo(); + +#endif //_UNDO_H_ \ No newline at end of file