! Z-Chess: two-player chess for the Z-machine ! Copyright (C) 2002, 2003, 2004 Eric Schmidt ! This program is free software; you can redistribute it and/or modify ! it under the terms of the GNU General Public License as published by ! the Free Software Foundation; either version 2 of the License, or ! (at your option) any later version. ! This program is distributed in the hope that it will be useful, ! but WITHOUT ANY WARRANTY; without even the implied warranty of ! MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ! GNU General Public License for more details. ! You should have received a copy of the GNU General Public License ! along with this program; if not, write to the Free Software ! Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ! The author may be contacted at . Release 4; Serial "040124"; ! --------------------------------------- ! Constants, Arrays, and Global Variables ! --------------------------------------- Constant NULL = -1; ! The screen size Global width; Global height; ! The board position Global bleft; Global btop; ! The board colors Global colorflag; Constant normal = 1; Constant black = 2; Constant red = 3; Constant cyan = 8; Constant white = 9; ! Unicode Global unicode_support = false; ! Code points for chess symbols Constant uni_wKing = $2654; Constant uni_wQueen = $2655; Constant uni_wRook = $2656; Constant uni_wBishop = $2657; Constant uni_wKnight = $2658; Constant uni_wPawn = $2659; Constant uni_bKing = $265a; Constant uni_bQueen = $265b; Constant uni_bRook = $265c; Constant uni_bBishop = $265d; Constant uni_bKnight = $265e; Constant uni_bPawn = $265f; ! Reading from the keyboard Constant uKey = $81; Constant dKey = $82; Constant lKey = $83; Constant rKey = $84; Constant EnterKey = $0d; ! The board caption system Global mrow; Constant whitemove = 0; Constant blackmove = 1; Constant promote = 2; Constant illegal = 3; Constant cillegal = 4; Constant check = 5; Constant checkmate = 6; Constant stalemate = 7; Constant lowpieces = 8; Constant saveyes = 9; Constant loadyes = 10; Constant saveno = 11; Constant loadno = 12; ! Notice that all the messages the game displays are of even length. ! This is deliberate. The window is likely to be of even width ! and so only even-lengthed messages can be perfectly centered. Array mArray --> ! message length "White's turn to move" 20 "Black's turn to move" 20 "Type letter of piece" 20 "Illegal move" 12 "Illegal move - check" 20 "Check!" 6 "Checkmate!" 10 "Stalemate!" 10 "Draw by too few pieces" 22 "Save succeeded" 14 "Load succeeded" 14 "Unable to save" 14 "Unable to load" 14; ! Cursor location Global rank = 6; Global file = 0; ! This defaults to WQRP ! Currently selected piece; Global mover = NULL; ! The position Constant wPawn = 1; Constant wKnight = 2; Constant wBishop = 3; Constant wRook = 4; Constant wQueen = 5; Constant wKing = 6; Constant threshold = 7; ! If (a_piece < threshold), it is white. Constant bPawn = 7; Constant bKnight = 8; Constant bBishop = 9; Constant bRook = 10; Constant bQueen = 11; Constant bKing = 12; Array position -> bRook bKnight bBishop bQueen bKing bBishop bKnight bRook bPawn bPawn bPawn bPawn bPawn bPawn bPawn bPawn nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing nothing wPawn wPawn wPawn wPawn wPawn wPawn wPawn wPawn wRook wKnight wBishop wQueen wKing wBishop wKnight wRook; Array working_position -> 64; Global WhiteToMove = true; Constant e1 = 60; Constant e8 = 4; ! For calculating check ! We define "old" variables for backups Global wKingPos = e1; Global owKingPos; Global bKingPos = e8; Global obKingPos; ! Castling Global just_castled; Global wking_moved; Global owking_moved; Global bking_moved; Global obking_moved; Global a1rook_moved; Global oa1rook_moved; Global h1rook_moved; Global oh1rook_moved; Global a8rook_moved; Global oa8rook_moved; Global h8rook_moved; Global oh8rook_moved; ! The numbers of squares to do with castling ! (It would be nice if Inform had octal numbers.) Constant a8 = 0; Constant a1 = 56; Constant h1 = 63; Constant h8 = 7; Constant f1 = 61; Constant g1 = 62; Constant d1 = 59; Constant c1 = 58; Constant f8 = 5; Constant g8 = 6; Constant d8 = 3; Constant c8 = 2; ! En passant Global epPawn = NULL; Global oepPawn = NULL; ! The current game status Global GameOver; ! ------------ ! The Routines ! ------------ ! The main routine: set game up and receive input [ Main input square piece i j; ! Check for large enough screen height = 0->$20; width = 0->$21; if (width < 22) "Regretably, this interpreter has not provided a wide enough window for this program."; if (height < 10) "Regretably, this interpreter has not provided a tall enough window for this program."; ! Check for color. If the fixed-pitch font bit is set in the header, we got ! here from a restart and shouldn't warn about the color. if (0->1 & 1) colorflag = true; else if (0-->$8 & $$10) font on; ! Turn bit back off else { print "WARNING: This interpreter has not provided color. The game may not perform optimally.^"; @read_char 1 -> input; } ! Locate board bleft = (width - 16) / 2 + 1; btop = (height - 10) / 2 + 2; if (height % 2) btop++; ! Favor lower row mrow = btop + 8; CheckUnicode(); CompleteRedraw(); ! Main gain loop while (1) { ! Set cursor position i = btop + rank; j = file * 2 + bleft; @set_cursor i j; ! Receive input @read_char 1 -> input; switch (input) { 'c', 'C': @erase_window -1; print "^^Release 1 - Initial release^^ Release 2 ^ * Added color support ^ * Fixed two castling bugs^^ Release 3 ^ * Unicode support ^ * Changed license to GPL^^ Release 4 ^ * Added ability to save and restore game"; @read_char 1 -> input; CompleteRedraw(); 'd', 'D': @erase_window -1; print "Use the arrow keys to move the cursor around. Press space bar (or enter) to select a piece. Move the cursor to the square you want to move the piece, and press space bar (or enter) again to move. To castle, move the king to its destination, and the rook will automatically move to its. To deselect the piece you've selected, press space bar when the cursor is on the piece.^^ White pieces are indicated by the letter W and black pieces by B. If supported by your system, the piece type will be shown as a chess figurine. If not, a letter is used. This is the first letter of the piece's name, except that a knight is represented by N.^^ When a pawn promotion occurs, you must specify which type of piece to promote it to. Do this by typing the letter of the piece you want to promote it to.^^ At the main display, you can type D to view these directions, I to view legal information, C to view major changes between versions, Q to quit, N to start a new game, S to save a game, or L to load a game from disk.^^ The author may be contacted at ."; @read_char 1 -> input; CompleteRedraw(); 'i', 'I': @erase_window -1; print "Z-Chess: Chess for the Z-Machine^ Copyright (C) 2002, 2003, 2004 Eric Schmidt^^ This program is free software. It may be distributed under the terms of the GNU General Public License version 2, or (at your option) any later version.^^ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA"; @read_char 1 -> input; CompleteRedraw(); 'l', 'L': @erase_window -1; @restore -> i; CompleteRedraw(); Message(loadno); ! Load failed if we get here 'n', 'N': font off; ! Hack: We use the fixed-pitch font bit to @restart; ! signal that a restart is taking place 'q', 'Q': @set_cursor height 1; @erase_line 1; quit; 'r', 'R': @erase_window -1; @restore -> i; CompleteRedraw(); Message(loadno); ! Cannot reach here if succeeded 's', 'S': @erase_window -1; @save -> i; CompleteRedraw(); switch (i) { 0: Message(saveno); 1: Message(saveyes); 2: Message(loadyes); } uKey: if (rank > 0) { rank--; PrintCaption(); } dKey: if (rank < 7) { rank++; PrintCaption(); } lKey: if (file > 0) { file--; PrintCaption(); } rKey: if (file < 7) { file++; PrintCaption(); } EnterKey, ' ': if (GameOver) break; square = rank * 8 + file; piece = position->square; if (mover == NULL && piece && WhiteToMove == (piece < threshold)) mover = square; else if (mover == square) mover = NULL; else if (mover ~= NULL) { if (~~AttemptToMove(mover,square)) PrintCaption(); mover = NULL; } PrintPos(); } } ]; [ CheckUnicode i; ! Pre 1.0 terps won't allow @check_unicode if (~~(0->$32)) return; @split_window height; @set_window 1; ! Would there be support for one chess glyph but not the ! others? Well, we might as well be on the safe side. @"EXT:12S" uni_wKing -> i; if (~~(i & 1)) return; @"EXT:12S" uni_wQueen -> i; if (~~(i & 1)) return; @"EXT:12S" uni_wRook -> i; if (~~(i & 1)) return; @"EXT:12S" uni_wBishop -> i; if (~~(i & 1)) return; @"EXT:12S" uni_wKnight -> i; if (~~(i & 1)) return; @"EXT:12S" uni_wPawn -> i; if (~~(i & 1)) return; @"EXT:12S" uni_bKing -> i; if (~~(i & 1)) return; @"EXT:12S" uni_bQueen -> i; if (~~(i & 1)) return; @"EXT:12S" uni_bRook -> i; if (~~(i & 1)) return; @"EXT:12S" uni_bBishop -> i; if (~~(i & 1)) return; @"EXT:12S" uni_bKnight -> i; if (~~(i & 1)) return; @"EXT:12S" uni_bPawn -> i; if (~~(i & 1)) return; ! The interpreter has passed the sieve. unicode_support = true; ]; [ CompleteRedraw; @split_window height; @set_window 1; @erase_window -2; style reverse; print "(D)irections"; spaces width - 18; print "(I)nfo"; style roman; PrintPos(); PrintCaption(); ]; ! Routine to print the position [ PrintPos i; @set_cursor btop bleft; for (: i < 64: i++) { ! Set printing colors, considering color support if (colorflag) { if (i / 8 % 2 == i % 2) @push cyan; else @push red; if (position->i < threshold) @push white; else @push black; @set_colour sp sp; } else if (i / 8 % 2 == i % 2) style roman; else style reverse; ! Print the piece symbol ! Could look weird if system does not support color or both reverse and ! bold type if (i == mover) style bold; if (~~position->i) print (char) ' '; else if (position->i < threshold) print (char) 'W'; else print (char) 'B'; if (unicode_support) { switch (position->i) { nothing: print (char) ' '; wPawn: @"EXT:11" uni_wPawn; wKnight: @"EXT:11" uni_wKnight; wBishop: @"EXT:11" uni_wBishop; wRook: @"EXT:11" uni_wRook; wQueen: @"EXT:11" uni_wQueen; wKing: @"EXT:11" uni_wKing; bPawn: @"EXT:11" uni_bPawn; bKnight: @"EXT:11" uni_bKnight; bBishop: @"EXT:11" uni_bBishop; bRook: @"EXT:11" uni_bRook; bQueen: @"EXT:11" uni_bQueen; bKing: @"EXT:11" uni_bKing; } } else { switch (position->i) { nothing: print (char) ' '; wPawn, bPawn: print (char) 'P'; wKnight, bKnight: print (char) 'N'; wBishop, bBishop: print (char) 'B'; wRook, bRook: print (char) 'R'; wQueen, bQueen: print (char) 'Q'; wKing, bKing: print (char) 'K'; } if (i == mover) style roman; } ! If at the end of a row, move to the next line if (i % 8 == 7) { style roman; @set_colour normal normal; if (i < 63) { new_line; spaces bleft - 1; } } } ]; ! The board caption [ PrintCaption; if (GameOver) Message(GameOver); else { if (WhiteToMove) Message(whitemove); else Message(blackmove); } ]; ! AttemptToMove evaluates the move and makes it if legal ! It returns true if it prints a message, false if not [ AttemptToMove start end; ! First, the basic test if (~~MovePrimitive(start, end, position)) { Message(illegal); return; } ! Make the move TransferPos(position, working_position); DoMove(start, end, working_position); ! If the move is illegal due to check, restore the old game status if (InCheck(working_position)) { TransferVarsToNew(); Message(cillegal); return; } ! Do promotion if needed if ((end <= h8 || end >= a1) && working_position->end == wPawn or bPawn) Promotion(end); ! Update the board TransferPos(working_position, position); TransferVarsToOld(); WhiteToMove = ~~WhiteToMove; ! Finally, evaulate for a draw, win, or check if (HasntLegal()) rfalse; if (InCheck(position)) return Message(check); rfalse; ]; ! Moving data around [ TransferPos p_array1 p_array2 i; for (: i < 64: i++) p_array2->i = p_array1->i; ]; [ TransferVarsToNew; wKingPos = owKingPos; bKingPos = obKingPos; wking_moved = owking_moved; bking_moved = obking_moved; a1rook_moved = oa1rook_moved; h1rook_moved = oh1rook_moved; a8rook_moved = oa8rook_moved; h8rook_moved = oh8rook_moved; epPawn = oepPawn; ]; [ TransferVarsToOld; owKingPos = wKingPos; obKingPos = bKingPos; owking_moved = wking_moved; obking_moved = bking_moved; oa1rook_moved = a1rook_moved; oh1rook_moved = h1rook_moved; oa8rook_moved = a8rook_moved; oh8rook_moved = h8rook_moved; oepPawn = epPawn; ]; ! MovePrimitive tests if a move is legal, but doesn't ! consider check. Returns true if the move seems legal, ! false if it seems illegal. [ MovePrimitive start end p_array side otherside srank sfile erank efile a b c d; ! Find the sides of the pieces on the starting ! and ending squares side = p_array->start < threshold; if (p_array->end == nothing) otherside = NULL; else otherside = (p_array->end < threshold); ! If they are the same, stop now if (side == otherside) rfalse; ! Locate the pieces on the board srank = start / 8; sfile = start % 8; erank = end / 8; efile = end % 8; ! Now, the actual evaluation switch (p_array->start) { wPawn, bPawn: ! For pawns, the rules are different for each side ! so test and set key numbers appropriately. if (side) { a = 1; b = 6; c = 4; d = 8; } else { a = -1; b = 1; c = 3; d = -8; } if (srank - erank == a) { ! Standard move? if (sfile == efile && otherside == NULL) rtrue; ! Capture? if (sfile - efile == -1 or 1 && (otherside ~= NULL || eppawn == end+d)) rtrue; } ! Two square move? if (srank == b && erank == c && sfile == efile && (~~p_array->(start-d)) && otherside == NULL) rtrue; ! The knights, bishops, rooks, queens, and kings wKnight, bKnight: if ((srank - erank == 1 or -1 && sfile - efile == 2 or -2) || (srank - erank == 2 or -2 && sfile - efile == 1 or -1)) rtrue; wBishop, bBishop: return DiagonalMove(srank, sfile, erank, efile, p_array); wRook, bRook: return StraightMove(srank, sfile, erank, efile, p_array); wQueen, bQueen: return DiagonalMove(srank, sfile, erank, efile, p_array) || StraightMove(srank, sfile, erank, efile, p_array); wKing, bKing: if (srank - erank < 2 && erank - srank < 2 && sfile - efile < 2 && efile - sfile < 2) rtrue; } ! Castling? if (p_array->start == wKing && ~~wking_moved) { if (end == g1 && ~~h1rook_moved) return StraightMove(7, 4, 7, 7, p_array); if (end == c1 && ~~a1rook_moved) return StraightMove(7, 4, 7, 0, p_array); } if (p_array->start == bKing && ~~bking_moved) { if (end == g8 && ~~h8rook_moved) return StraightMove(0, 4, 0, 7, p_array); if (end == c8 && ~~a8rook_moved) return StraightMove(0, 4, 0, 0, p_array); } rfalse; ]; ! DiagonalMove tests if a diagonal move is valid. ! That is, that the squares are diagonal from each other ! and nothing is in the way. Returns true if valid, false if not. [ DiagonalMove srank sfile erank efile p_array i j; ! Make sure squares are diagonal from each other if (srank - erank ~= sfile - efile or efile - sfile) rfalse; ! Before testing for obstacles, find the direction of the move if (srank < erank) i = 1; else i = -1; if (sfile < efile) j = 1; else j = -1; ! Now test for obstacles for (::) { srank = srank + i; sfile = sfile + j; if (srank == erank) break; if (p_array->(srank * 8 + sfile)) rfalse; } ]; ! StraightMove is similar to DiagonalMove [ StraightMove srank sfile erank efile p_array i; if (srank ~= erank && sfile ~= efile) rfalse; if (srank == erank) { if (sfile < efile) i = 1; else i = -1; for (::) { sfile = sfile + i; if (sfile == efile) break; if (p_array->(srank * 8 + sfile)) rfalse; } } else { if (srank < erank) i = 1; else i = -1; for (::) { srank = srank + i; if (srank == erank) break; if (p_array->(srank * 8 + sfile)) rfalse; } } ]; ! DoMove actually performs the move, taking special note of ! castling and en passant. [ DoMove start end p_array; ! If en passant, remove the pawn being taken if (p_array->end == nothing && (start - end) % 8 && p_array->start == wPawn or bPawn) p_array->(epPawn) = nothing; ! If a pawn is being moved 2 squares, store it in epPawn ! If not, we clear it epPawn = NULL; if (p_array->start == wPawn or bPawn && start - end == 16 or -16) epPawn = end; ! Set movement variables if necessary if (start == a1) a1rook_moved = true; else if (start == h1) h1rook_moved = true; else if (start == a8) a8rook_moved = true; else if (start == h8) h8rook_moved = true; else if (start == e1) wKing_moved = true; else if (start == e8) bKing_moved = true; ! The move p_array->end = p_array->start; p_array->start = nothing; ! Set variable if a king moved if (p_array->end == wKing) wKingPos = end; else if (p_array->end == bKing) bKingPos = end; ! If castling, move the rooks also, and set just_castled just_castled = false; if (p_array->end == wKing && start == e1) { if (end == g1) {DoMove(h1, f1, p_array); just_castled = true;} else if (end == c1) {DoMove(a1, d1, p_array); just_castled = true;} } else if (p_array->end == bKing && start == e8) { if (end == g8) {DoMove(h8, f8, p_array); just_castled = true;} else if (end == c8) {DoMove(a8, d8, p_array); just_castled = true;} } ]; ! Promotion finds from the player the piece to promote to and executes ! the promotion. [ Promotion pawn input a i; if (working_position->pawn == bPawn) a = 6; message(promote); i = (width - 20) / 2 + 21; @set_cursor mrow i; while (1) { @read_char 1 -> input; switch (input) { 'q', 'Q': working_position->pawn = wQueen + a; return; 'n', 'N': working_position->pawn = wKnight + a; return; 'b', 'B': working_position->pawn = wBishop + a; return; 'r', 'R': working_position->pawn = wRook + a; return; } } ]; ! InCheck tests if the current side to move is in check. ! Returns true if in check, false if not. [ InCheck p_array king kstart kmiddle i s_ep; ! First, find the king if (WhiteToMove) king = wKingPos; else king = bKingPos; ! If castling, check the starting and middle squares too. if (just_castled) { if (king == g1) { kstart = e1; kmiddle = f1; } else if (king == c1) { kstart = e1; kmiddle = d1; } else if (king == g8) { kstart = e8; kmiddle = f8; } else if (king == c8) { kstart = e8; kmiddle = d8; } } if (kstart) { s_ep = epPawn; ! DoMove wrecks epPawn. Save a backup. DoMove(king, kstart, p_array); if (InCheck(p_array)) i = true; DoMove(kstart, kmiddle, p_array); if (InCheck(p_array)) i = true; DoMove(kmiddle, king, p_array); if (WhiteToMove) p_array->kmiddle = wRook; else p_array->kmiddle = bRook; epPawn = s_ep; if (i) rtrue; } ! Now, find all enemy pieces, and use MovePrimitive ! to see if they can take the king if (WhiteToMove) for (: i < 64: i++) { if (p_array->i < threshold) continue; if (MovePrimitive(i, king, p_array)) rtrue; } else for (: i < 64: i++) { if (p_array->i == nothing) continue; if (p_array->i < threshold && MovePrimitive(i, king, p_array)) rtrue; } rfalse; ]; ! HasntLegal tests if the game is over due to checkmate, stalemate, ! or insufficiant material to mate, and sets gameover accordingly. [ HasntLegal i j k knight lbishop dbishop; ! Try to find a legal move for side to move with MovePrimitive, DoMove, and InCheck. if (WhiteToMove) for (: i < 64: i++) { if (position->i == nothing || position->i >= threshold) continue; for (j = 0: j < 64: j++) { if (MovePrimitive(i, j, position) && DoMove(i, j, position) && (~~InCheck(position))) k = true; TransferPos(working_position, position); TransferVarsToNew(); if (k) jump out; } } else for (: i < 64: i++) { if (position->i < threshold) continue; for (j = 0: j < 64: j++) { if (MovePrimitive(i, j, position) && DoMove(i, j, position) && (~~InCheck(position))) k = true; TransferPos(working_position, position); TransferVarsToNew(); if (k) jump out; } } .out; ! If i is 64, the entire loop found no legal move if (i == 64) { if (InCheck(position)) gameover = checkmate; else gameover = stalemate; rtrue; } ! The final task is to check for low pieces. Anything other than ! a minor piece or king means no draw. Two minor pieces ! mean no draw, unless they are two bishops found on the same ! color square. The same is true of any number of bishops ! of the same color square. for (i = 0: i < 64: i++) switch (position->i) { wPawn, bPawn, wRook, bRook, wQueen, bQueen: rfalse; wKnight, bKnight: if (knight || lbishop || dbishop) rfalse; knight = true; wBishop, bBishop: if (i / 8 % 2 == i % 2) lbishop = true; else dbishop = true; if (knight || (lbishop && dbishop)) rfalse; } gameover = lowpieces; ]; ! A routine to center all the messages neatly under the board [ Message num start; num = num * 2; start = (width - mArray-->(num+1)) / 2; @set_cursor mrow 1; spaces start; print (string) mArray-->num; spaces start; ];