(* Copyright (C) 2010 Free Software Foundation, Inc. *) (* This file is part of GNU Modula-2. GNU Modula-2 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, or (at your option) any later version. GNU Modula-2 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 gm2; see the file COPYING. If not, write to the Free Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. *) MODULE halma ; FROM STextIO IMPORT WriteString, WriteLn, WriteChar, ReadToken, SkipLine ; FROM SWholeIO IMPORT WriteCard, WriteInt ; FROM Strings IMPORT Length ; FROM Selective IMPORT Timeval, GetTimeOfDay, GetTime, InitTime, KillTime ; FROM WholeStr IMPORT StrToCard, ConvResults ; FROM SYSTEM IMPORT CARDINAL8 ; CONST TwoPlayer = TRUE ; FourPlayer = FALSE ; BoardX = 16 ; BoardY = 16 ; BoardSize = BoardX * BoardY ; Pieces = 19 ; (* total pieces per player on the board *) PieceHeap = 4000 ; (* maximum moves we will examine per ply *) MaxScore = 100000 ; MinScore = -100000 ; WinScore = MaxScore ; LooseScore = -WinScore ; Debugging = FALSE ; Thinking = 10 ; (* how many seconds can the program think? *) slowEvaluation = FALSE ; HomeWeight = BoardX ; TYPE Squares = [0..BoardSize-1] ; SoS = SET OF Squares ; Colour = (Blue, Red, Green, White) ; Board = RECORD used : SoS ; (* is the square used at all? *) colour: ARRAY [0..1] OF SoS ; (* if so which colour occupies the square? *) pieces: ARRAY [MIN(Colour)..MAX(Colour)] OF ARRAY [1..Pieces] OF CARDINAL8 ; home : ARRAY [MIN(Colour)..MAX(Colour)] OF CARDINAL ; END ; Moves = RECORD pieceHead: ARRAY [0..Pieces] OF CARDINAL ; (* pieceHead[0] is start of peg 1 moves in the heap *) pieceList: ARRAY [0..PieceHeap] OF CARDINAL8 ; (* pieceHead[1] is start of peg 2 moves in the heap *) END ; Reachable = RECORD no : CARDINAL ; prev: CARDINAL ; dist: CARDINAL ; list: ARRAY Squares OF CARDINAL ; END ; Graph = RECORD graph: ARRAY Squares OF Reachable ; END ; VAR count : CARDINAL ; homeBase: ARRAY [MIN(Colour)..MAX(Colour)] OF SoS ; (* +-----------------------------------------------------------------+ | 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 | | | | 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | | | | 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | | | | 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | | | | 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | | | | 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | | | | 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | | | | 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 | | | | 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | | | | 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | | | | 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | | | | 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | |--------- | | 48 49 \50 51 52 53 54 55 56 57 58 59 60 61 62 63 | | \ | | 32 33 34 \35 36 37 38 39 40 41 42 43 44 45 46 47 | | \ | | 16 17 18 19| 20 21 22 23 24 25 26 27 28 29 30 31 | | | | | 0 1 2 3| 4 5 6 7 8 9 10 11 12 13 14 15 | +-----------------------------------------------------------------+ *) (* stop - *) PROCEDURE stop ; BEGIN END stop ; (* Min - *) PROCEDURE Min (a, b: INTEGER) : INTEGER ; BEGIN IF a0, b, t-1, p, c, m) ; (* -1, 0 *) ifFreeAdd(x0, b, t-BoardX, p, c, m) ; (* 0, -1 *) ifFreeAdd(y0) AND (y>0), b, t-(BoardX+1), p, c, m) ; (* -1, -1 *) ifFreeAdd((x0), b, t-(BoardX-1), p, c, m) ; (* 1, -1 *) ifFreeAdd((x>0) AND (y0 THEN e := (BoardY-y) DIV 2 ELSIF j<0 THEN e := y DIV 2 END ELSIF j=0 THEN IF i>0 THEN e := (BoardX-x) DIV 2 ELSIF i<0 THEN e := x DIV 2 END ELSE IF (i=1) AND (j=1) THEN e := Min((BoardX-x) DIV 2, (BoardY-y) DIV 2) ELSIF (i=-1) AND (j=1) THEN e := Min(x DIV 2, (BoardY-y) DIV 2) ELSIF (i=-1) AND (j=-1) THEN e := Min(x DIV 2, y DIV 2) ELSE (* 1, -1 *) e := Min((BoardX-x) DIV 2, y DIV 2) END END ; LOOP IF d>e THEN (* no point searching further as there is no room for the reflective jump *) RETURN END ; x := x + i ; y := y + j ; IF (x<0) OR (y<0) OR (x>=BoardX) OR (y>=BoardY) THEN RETURN END ; t := VAL(CARDINAL, y)*BoardX+VAL(CARDINAL, x) ; IF isUsed(b, t) THEN (* found pivot, keep looking for the destination *) WHILE d>0 DO x := x + i ; y := y + j ; (* IF i>=0 THEN INC(x, i) ELSE DEC(x, -i) END ; IF j>=0 THEN INC(y, j) ELSE DEC(y, -j) END ; *) IF (x<0) OR (y<0) OR (x>=BoardX) OR (y>=BoardY) THEN RETURN END ; t := VAL(CARDINAL, y)*BoardX+VAL(CARDINAL, x) ; IF isUsed(b, t) THEN RETURN END ; DEC(d) END ; IF NOT isRecorded(m, t, p) THEN IF Debugging THEN WriteString('adding move ') ; WriteCard(t, 0) ; WriteLn END ; recordMove(m, t, p) END ; RETURN END ; INC(d) END END addMultipleV ; (* addMultiple - adds moves which involve jumping. Current peg, p, is at at position indicated by, m.pieceList[low]. *) PROCEDURE addMultiple (VAR b: Board; VAR m: Moves; c: Colour; p: CARDINAL; low: CARDINAL) ; VAR t : INTEGER ; x, y: INTEGER ; BEGIN WITH b DO WHILE low0, b, t-1, p, c, m, from, g) ; (* -1, 0 *) ifFreeRecord(x0, b, t-BoardX, p, c, m, from, g) ; (* 0, -1 *) ifFreeRecord(y0) AND (y>0), b, t-(BoardX+1), p, c, m, from, g) ; (* -1, -1 *) ifFreeRecord((x0), b, t-(BoardX-1), p, c, m, from, g) ; (* 1, -1 *) ifFreeRecord((x>0) AND (y0 THEN e := (BoardY-y) DIV 2 ELSIF j<0 THEN e := y DIV 2 END ELSIF j=0 THEN IF i>0 THEN e := (BoardX-x) DIV 2 ELSIF i<0 THEN e := x DIV 2 END ELSE IF (i=1) AND (j=1) THEN e := Min((BoardX-x) DIV 2, (BoardY-y) DIV 2) ELSIF (i=-1) AND (j=1) THEN e := Min(x DIV 2, (BoardY-y) DIV 2) ELSIF (i=-1) AND (j=-1) THEN e := Min(x DIV 2, y DIV 2) ELSE (* 1, -1 *) e := Min((BoardX-x) DIV 2, y DIV 2) END END ; LOOP IF d>e THEN (* no point searching further as there is no room for the reflective jump *) RETURN END ; x := x + i ; y := y + j ; IF (x<0) OR (y<0) OR (x>=BoardX) OR (y>=BoardY) THEN RETURN END ; t := VAL(CARDINAL, y)*BoardX+VAL(CARDINAL, x) ; IF isUsed(b, t) THEN (* found pivot, keep looking for the destination *) WHILE d>0 DO x := x + i ; y := y + j ; IF (x<0) OR (y<0) OR (x>=BoardX) OR (y>=BoardY) THEN RETURN END ; t := VAL(CARDINAL, y)*BoardX+VAL(CARDINAL, x) ; IF isUsed(b, t) THEN RETURN END ; DEC(d) END ; IF NOT isRecorded(m, t, p) THEN IF Debugging THEN WriteString('adding move ') ; WriteCard(t, 0) ; WriteLn END ; recordMove(m, t, p) ; addToGraph(g, from, t) ; addToGraph(g, t, from) END ; RETURN END ; INC(d) END END recordMultipleV ; (* recordMultiple - adds moves which involve jumping. Current peg, p, is at at position indicated by, m.pieceList[low]. *) PROCEDURE recordMultiple (VAR b: Board; VAR m: Moves; c: Colour; p: CARDINAL; low: CARDINAL; VAR g: Graph) ; VAR from: INTEGER ; x, y: INTEGER ; BEGIN WITH b DO WHILE low0 THEN x := CAP(s[0]) ; IF x='?' THEN displayAllMoves(b, c) ELSIF (x>='A') AND (x<='P') THEN from := ORD (x) - ORD ('A') ; s[0] := '0' ; IF Length(s)>0 THEN StrToCard(s, y, res) ; IF (res=strAllRight) AND ((y=0) OR (y>BoardY)) THEN WriteString('Please enter a number between [1-16]') ; WriteLn ELSE from := from+(y-1)*BoardY ; IF isUsed(b, from) AND isColour(b, from, c) THEN RETURN from ELSE WriteString('That position is occupied by your opponent') ; WriteLn END END END ELSE WriteString('please enter a letter [A-P] followed by a number [1-16]') ; WriteLn END END ; RETURN BoardSize END getFirstPos ; (* getSecondPos - *) PROCEDURE getSecondPos (s: ARRAY OF CHAR; VAR b: Board; c: Colour; peg: CARDINAL) : CARDINAL ; VAR from: CARDINAL ; x : CHAR ; y : CARDINAL ; res : ConvResults ; BEGIN IF Length(s)>0 THEN x := CAP(s[0]) ; IF x='?' THEN displayMovesPeg(b, c, peg) ; displayBoardPeg(b, c, peg) ELSIF (x>='A') AND (x<='P') THEN from := ORD (x) - ORD ('A') ; s[0] := '0' ; IF Length(s)>0 THEN StrToCard(s, y, res) ; IF (res=strAllRight) AND ((y=0) OR (y>BoardY)) THEN WriteString('Please enter a number between [1-16]') ; WriteLn ELSE from := from+(y-1)*BoardY ; IF NOT isUsed(b, from) THEN RETURN from ELSIF isColour(b, from, c) THEN WriteString('That position is already occupied by another of your pegs') ; WriteLn ELSE WriteString('That position is occupied by your opponent') ; WriteLn END END END ELSE WriteString('please enter a letter [A-P] followed by a number [1-16]') ; WriteLn END END ; RETURN BoardSize END getSecondPos ; (* getPeg - *) PROCEDURE getPeg (VAR b: Board; c: Colour; from: CARDINAL) : CARDINAL ; VAR p: CARDINAL ; BEGIN FOR p := 1 TO Pieces DO IF b.pieces[c][p] = VAL (CARDINAL8, from) THEN RETURN p END END ; HALT ; RETURN Pieces+1 END getPeg ; (* checkLegal - *) PROCEDURE checkLegal (VAR b: Board; col: Colour; from, to: CARDINAL; peg: CARDINAL) : BOOLEAN ; VAR m : Moves ; i, j: CARDINAL ; BEGIN IF (to=BoardSize) OR (from=BoardSize) THEN RETURN FALSE END ; genMoves(b, m, col) ; IF VAL (CARDINAL8, from) # b.pieces[col][peg] THEN RETURN FALSE END ; i := m.pieceHead[peg-1]+1 ; (* skip the initial move *) j := m.pieceHead[peg] ; WHILE i=MaxScore) END maximumScore ; (* calcScoreForPos - returns the score for Colour, c, pos, on Board, b. *) PROCEDURE calcScoreForPos (VAR b: Board; c: Colour; pos: CARDINAL) : INTEGER ; VAR home, x, y: CARDINAL ; BEGIN IF c=Red THEN pos := (BoardSize-1) - pos ELSIF c=Blue THEN (* nothing to do *) ELSE HALT (* not implemented yet *) END ; IF pos IN homeBase[c] THEN home := HomeWeight ELSE home := 0 END ; (* our score is dependant upon how far this piece is away from the opposite corner *) x := pos MOD BoardX ; y := pos DIV BoardY ; IF x>y THEN (* max squares from 0,0 *) RETURN BoardX-x+home ELSE RETURN BoardY-y+home END END calcScoreForPos ; (* calcScoreFor - returns the score for Colour, c. *) PROCEDURE calcScoreFor (VAR b: Board; c: Colour) : INTEGER ; VAR score: INTEGER ; p : CARDINAL ; BEGIN score := 0 ; FOR p := 1 TO Pieces DO INC(score, calcScoreForPos(b, c, b.pieces[c][p])) END ; RETURN score END calcScoreFor ; (* updateMove - *) PROCEDURE updateMove (VAR b: Board; col: Colour; peg: CARDINAL; topos: CARDINAL) ; VAR frompos: CARDINAL ; BEGIN frompos := b.pieces[col][peg] ; subPiece(b, frompos, col) ; addPiece(b, topos, col, peg) END updateMove ; (* retractMove - *) PROCEDURE retractMove (VAR b: Board; col: Colour; peg: CARDINAL; topos: CARDINAL) ; BEGIN updateMove(b, col, peg, topos) END retractMove ; (* calcScore - make the move and update the score. *) PROCEDURE calcScore (VAR b: Board; score: INTEGER; peg: CARDINAL; topos: CARDINAL; col: Colour) : INTEGER ; VAR i, j, k: INTEGER ; BEGIN IF slowEvaluation THEN (* compute the score by examine each peg in turn *) updateMove(b, col, peg, topos) ; (* check whether one side has won *) IF b.home[Blue]=Pieces THEN RETURN MaxScore ELSIF b.home[Red]=Pieces THEN RETURN MinScore END ; RETURN calcScoreFor(b, Blue) - calcScoreFor(b, Red) ELSE i := calcScoreForPos(b, col, b.pieces[col][peg]) ; updateMove(b, col, peg, topos) ; (* move the peg *) (* check whether one side has won *) IF b.home[Blue]=Pieces THEN RETURN MaxScore ELSIF b.home[Red]=Pieces THEN RETURN MinScore END ; j := calcScoreForPos(b, col, topos) ; IF col=Red THEN score := score + i - j ELSE score := score - i + j END ; IF Debugging THEN k := calcScoreFor(b, Blue) - calcScoreFor(b, Red) ; IF score#k THEN HALT END END ; RETURN score END END calcScore ; (* alphaBeta - returns the score estimated should move, pos, be chosen. The board, b, and score is in the state _before_ move pos is made. *) PROCEDURE alphaBeta (peg: CARDINAL; frompos, topos: CARDINAL; VAR b: Board; col: Colour; depth: CARDINAL; alpha, beta, score: INTEGER) : INTEGER ; VAR try : INTEGER ; i, j, n, p : CARDINAL ; m : Moves ; from, to: CARDINAL ; op : Colour ; BEGIN score := calcScore(b, score, peg, topos, col) ; (* make move and update score *) IF (depth=0) OR maximumScore(score) THEN retractMove(b, col, peg, frompos) ; INC(count) ; IF col=Red THEN RETURN score+VAL(INTEGER, depth) ELSE RETURN score-VAL(INTEGER, depth) END ELSE op := opponent(col) ; genMoves(b, m, op) ; IF op=Blue THEN (* blue to move, move is possible, continue searching *) FOR p := 1 TO Pieces DO from := b.pieces[op][p] ; i := m.pieceHead[p-1]+1 ; (* skip the initial move *) j := m.pieceHead[p] ; WHILE i alpha THEN (* found a better move *) alpha := try END ; IF alpha >= beta THEN retractMove(b, col, peg, frompos) ; RETURN alpha END ; INC(i) END END ; retractMove(b, col, peg, frompos) ; RETURN alpha ELSE (* red to move, move is possible, continue searching *) FOR p := 1 TO Pieces DO from := b.pieces[op][p] ; i := m.pieceHead[p-1]+1 ; (* skip the initial move *) j := m.pieceHead[p] ; WHILE i= beta THEN (* no point searching further as Red would choose a different previous move *) retractMove(b, col, peg, frompos) ; RETURN beta END ; INC(i) END END ; retractMove(b, col, peg, frompos) ; RETURN beta (* the best score for a move Blue has found *) END END END alphaBeta ; (* makeMove - computer makes a move for colour, col. *) PROCEDURE makeMove (VAR b: Board; col: Colour; score: INTEGER; VAR peg: CARDINAL) : INTEGER ; VAR no : CARDINAL ; p, from, frompos, topos, to : CARDINAL ; start, end: Timeval ; try, r, best : INTEGER ; secS, usec, secE, i, j: CARDINAL ; m : Moves ; plies : CARDINAL ; outOfTime : BOOLEAN ; BEGIN start := InitTime(0, 0) ; end := InitTime(0, 0) ; r := GetTimeOfDay(start) ; best := MinScore-1 ; (* worst than minimum score so we will choose a loosing move if forced *) count := 0 ; i := 0 ; genMoves(b, m, col) ; no := noOfMoves(m) ; peg := Pieces+1 ; outOfTime := FALSE ; plies := 0 ; frompos := BoardSize ; topos := BoardSize ; REPEAT WriteString("I'm going to look ") ; WriteCard(plies, 0) ; WriteString(' moves ahead') ; WriteLn ; FOR p := 1 TO Pieces DO from := b.pieces[col][p] ; i := m.pieceHead[p-1]+1 ; (* skip the initial move *) j := m.pieceHead[p] ; IF (no=1) AND (i Thinking) ; IF outOfTime THEN WriteString('out of time...') ; WriteLn ELSE to := m.pieceList[i] ; try := alphaBeta(p, from, to, b, col, plies, MinScore, MaxScore, score) ; IF try>best THEN best := try ; topos := to ; frompos := from ; peg := p END END ; INC(i) END END END ; IF (NOT outOfTime) AND (frompos= WinScore THEN WriteString('I think I can force a win') ; WriteLn END ; IF best <= LooseScore THEN WriteString('You should be able to force a win') ; WriteLn END ; IF no=1 THEN WriteString('I can only play one move, so there is little point wasting time') ; WriteLn ELSIF no=0 THEN WriteString('I cannot move, so there is little point wasting time') ; WriteLn ELSE WriteString('I took ') ; WriteCard(secE-secS, 0) ; WriteString(' seconds and evaluated ') ; WriteCard(count, 0) ; WriteString(' positions,') ; WriteLn ; END ; start := KillTime(start) ; end := KillTime(end) ; RETURN topos END makeMove ; (* test - *) PROCEDURE test ; VAR b : Board ; c : Colour ; s : INTEGER ; peg, to : CARDINAL ; BEGIN initBoard(b) ; c := Red ; s := 0 ; displayBoard(b) ; peg := getPeg(b, c, 4) ; displayBoardPeg(b, c, peg) ; to := 36 ; s := calcScore(b, s, peg, to, c) ; peg := 5 ; c := opponent(c) ; peg := getPeg(b, c, 12*BoardX+15) ; to := 12*BoardX+13 ; s := calcScore(b, s, peg, to, c) ; displayBoardPeg(b, c, peg) ; c := Red ; displayBoard(b) ; peg := getPeg(b, c, 36) ; stop ; displayBoardPeg(b, c, peg) ; to := 4 ; s := calcScore(b, s, peg, to, c) ; displayBoardPeg(b, c, peg) ; END test ; (* displayHow - *) PROCEDURE displayHow (from, to: CARDINAL; VAR rec: ARRAY OF CARDINAL; r: CARDINAL) ; VAR i: CARDINAL ; BEGIN writePosition(from) ; WriteString(' can move to ') ; writePosition(to) ; WriteString(' by: ') ; i := 0 ; WHILE (iPieces THEN WriteString('I cannot move') ; WriteLn ELSE from := b.pieces[c][peg] ; WriteString('I am ') ; showMove(b, c, peg, from, to) ; s := calcScore(b, s, peg, to, c) ; displayBoard(b) ; WriteString('Current score = ') ; WriteInt(s, 0) ; WriteLn ; IF s>=MaxScore THEN WriteString('Good try, but I win') ; WriteLn ; RETURN END END ; c := opponent(c) END END play ; (* writePosition - *) PROCEDURE writePosition (x: CARDINAL) ; BEGIN WriteChar(' ') ; WriteChar(CHR(ORD('a')+x MOD BoardX)) ; WriteCard(x DIV BoardX+1, 0) END writePosition ; (* displayMovesForPeg - *) PROCEDURE displayMovesForPeg (VAR b: Board; m: Moves; c: Colour; peg: CARDINAL) ; VAR p, i, j: CARDINAL ; BEGIN WriteString('peg at') ; writePosition(b.pieces[c][peg]) ; IF m.pieceHead[peg-1]+1