# # checkers.icn # # # An abstract class for turn-based board games that implements # a minimax algorithm. # class GameState(player, # list of players turn) # whose turn it is # # Child classes have to provide these capabilities. # abstract method evaluate() abstract method finished() abstract method generate_moves() abstract method copy() abstract method draw_board() # # This minimax algorithm assumes that evaluate() returns an # assessment of the board state from the current player's point of # view. This is so that it will be more extensible to n-person games. # method minimax(depth, player) local alpha /player := turn if (depth = 0) | finished() then { ev := evaluate() if ev === &null then { write("-------------------------------------------") draw_board() write("-------------------------------------------") stop("evaluate() returned null for preceding board") } return ev } else if player == turn then { every childmove := generate_moves(node) do { child := copy() child.apply_move(childmove) kidval := child.minimax(depth-1, player) if not (/alpha := kidval) then alpha <:= kidval } #write("max'ing ", player, " depth ", depth, " alpha ", alpha) } else { # minimizing player every childmove := generate_moves(node) do { child := copy() child.apply_move(childmove) kidval := -child.minimax(depth-1,player) if not (/alpha := kidval) then alpha >:= kidval } #write("min'ing ", player, " depth ", depth, " alpha ", alpha) } return \alpha | 0 end method advance_turn() if turn == player[i := 1 to *player] then { i +:= 1 if i > *player then i := 1 } else stop("no player named ", image(turn)) turn := player[i] end end # # A checkers gamestate is a GameState with a representation of a game board # (a list of lists of strings, with names like "red queen" or "white"). # class CheckersGameState : GameState(square, mydepth, human) # # the representation of a move is a list of row1,col1,row2,col2... # that moves a piece from row1,col1 to rowN,colN through a series of # 0 or more intermediate locations. # method apply_move(L) every i := 3 to *L by 2 do { # if target game.square is not empty, re-do if not (square[L[i], L[i+1]] == " ") then fail write("piece being moved is ", square[L[1],L[2]]) if find("queen", square[L[1],L[2]]) then { # queen rules if abs(L[1]-L[3]) = abs(L[4]-L[2]) = 1 then { square[L[1],L[2]] :=: square[L[3],L[4]] return } else if abs(L[i]-L[i-2]) = abs(L[i+1]-L[i-1]) = 2 then { square[L[i],L[i+1]] :=: square[L[i-2],L[i-1]] square[(L[i]+L[i-2])/2, (L[i+1]+L[i-1])/2] := " " } else fail } else { # regular piece dir := (if turn=="red" then -1 else 1) write("regular piece move, abs = ", abs(L[i]-L[i-2])) write("L[i+1]-L[i-1] = ", L[i+1]-L[i-1], " dir*2=", dir*2) if abs(L[i-1]-L[i+1]) = 1 & ((L[i]-L[i-2]) = dir) then { square[L[i-2],L[i-1]] :=: square[L[i],L[i+1]] if L[i] = (1 | 8) then square[L[i],L[i+1]] ||:= " queen" return } else if abs(L[i-1]-L[i+1]) = 2 & (L[i]-L[i-2]) = dir*2 then { square[L[i],L[i+1]] :=: square[L[i-2],L[i-1]] square[(L[i]+L[i-2])/2, (L[i+1]+L[i-1])/2] := " " if L[i] = (1 | 8) then square[L[i],L[i+1]] ||:= " queen" } else { write("Can't perform requested move.") fail } } } return # COMPLETE previous text of apply_move() here: # i := 0 # while i+4 <= *L do { # srcrow := L[i+1]; srccol := L[i+2]; destrow := L[i+3]; destcol:=L[i+4] # square[srcrow,srccol] :=: square[destrow, destcol] # if abs(srcrow-destrow)=2 then # square[(srcrow+destrow)/2,(srccol+destcol)/2] := " " # i +:= 2 # } end # # major heuristic: how to evaluate a board position? # value of friendly pieces - value of enemy pieces # nearness of friendly promotion - nearness of enemy promotion # Probably not good yet. Maybe should simplify. # method evaluate() local dir # direction this player is moving if turn=="white" then dir := 1 else dir := -1 points := 0 every row := 1 to 8 do every col := 1 to 8 do { if square[row,col] == " " then next else if find(turn,square[row,col]) then sgn := 1 else sgn := -1 points +:= 1000*sgn if find("queen", square[row,col]) then { points +:= 10000*sgn write(if sgn=1 then "I'd get a queen" else "They'd have a queen") } else points -:= sgn * 2 ^ (if dir=-1 then row else (8-row)) } return points end # # This function generates 2nd and subsequent jumps possible # from a given position after having done a first jump. # method generate_jumps(L) local dir # direction this player is moving if turn=="white" then dir := 1 else dir := -1 row := L[-2] col := L[-1] every (rsgn := (dir|(if find("queen",square[row,col]) then -dir))) & (csgn := (1|-1)) & (square[row+rsgn*2, col+2*csgn]==" ") & enemy_in(row+rsgn, col+csgn) do { L2 := L ||| [row+rsgn*2, col+2*csgn] suspend L2 suspend generate_jumps(L2) } end # # Generate all the moves possible from a given board configuration. # method generate_moves() # for every current-player's piece on the current board... every row := 1 to 8 & col := 1 to 8 & match(turn,square[row,col]) do { # write("consider the piece at row ", row, ", col ", col) #, " within ", image(self), ": ", image(self.square)) # for every move that piece could make... if find("queen", square[row,col]) then { # queen moves every (dir := (1|-1)) & (csgn := (1|-1)) do { if square[row+dir,col+csgn] == " " then suspend [row,col,row+dir,col+csgn] if square[row+dir*2,col+csgn*2] == " " & enemy_in(row+dir, col+csgn) then { L := [row,col,row+dir,col+csgn] suspend L # multijumps logic here suspend generate_jumps(L) } } } else { # normal moves # trivial moves if turn=="white" then dir := 1 else dir := -1 every square[row+dir,(c := ((col-1)|col+1))]== " " do { suspend [row,col,row+dir,c] } # jumps every (csgn := (1|-1)) & (square[row+dir*2, col+csgn*2]== " ") & enemy_in(row+dir,col+csgn) do { L:= [row,col,row+dir*2, col+2*csgn] suspend L # multijumps logic here suspend generate_jumps(L) } } } end # # Boolean: is there an enemy at row,col? # method enemy_in(row,column) if (row|column)=0 then fail s := square[row,column] return not (s == (" " | turn)) end # # Copy the current board configuration. Overriding the name of a # built-in function is generally a bad idea. This works, but maybe # it would be better to call the method "cp". Note the trickiness # of copying an object: if you are not careful, the new object still # points at the old one via its __s field. # method copy() cp := proc("copy",0) x := cp(self) x.__s := x x.square := cp(square) every i := 1 to *square do { x.square[i] := cp(x.square[i]) every j := 1 to *x.square[i] do { x.square[i,j] := cp(x.square[i,j]) } } return x end # # Return whether the game is over, and if so, who won. # method finished() # probably need to add conditions of not being able to move. if not find(player[1], !!square) then return player[2] if not find(player[2], !!square) then return player[1] end # # Draw a (ASCII art) game board. Should upgrade to graphic version. # method draw_board() local i, j write("(draw board ",image(square),")") write(" \\ 1 2 3 4 5 6 7 8 column") write("row -----------------") every i := 1 to 8 do { writes(" ",i," ") every j := 1 to 8 do { if find("queen", square[i,j]) then writes("|",map(square[i,j,1],&lcase,&ucase)) else writes("|",square[i,j,1]) } write("|\n -----------------") } end # obtain the next move, from either a human or computer player method get_move() if human == turn then { repeat { if not (L := humanmove()) then fail # ensure that starting square held one of our pieces if find(turn, square[L[1],L[2]]) then return L write("Did not find a piece to move, try again.") } } else { write("computer move") L := computermove() every write("\t", !L) write("-------------") } return L end method humanmove() local L, input write("human move") write("It is ", turn, "'s turn, move in row,col,...row,col format:") if not (input := read()) then fail L := [ ] input ? while put(L, integer(tab(many(&digits)))) do ="," return L end # # The actual AI. Rate each possible move. Select the best one. # The game tree nodes are constructed by copy+modify of the current # game state. The moves are flat lists of alternating row,col # coordinates; returning the computer move like this, instead of the # winning "game state" allows the computer player to look just like # a human player who input their move as a list of row,col coordinates. # method computermove() list_of_possible := [] write("it is ", turn, "'s turn (computer play)") t1 := &time every possible := generate_moves() do { write("considering: ", image(possible)) put(list_of_possible, possible) mv := copy() mv.apply_move(possible) thepoints := mv.minimax(mydepth) mv.draw_board() write("...evaluates to ", thepoints) if /bestpoints | thepoints>bestpoints then { bestpoints := thepoints bestmove := possible list_of_possible := [possible] } else if thepoints=bestpoints then { put(list_of_possible, possible) } } write("so that's like ", *list_of_possible, " possibilities") if *list_of_possible = 0 then stop("no possible moves") if *list_of_possible > 1 then { write("multiple (",*list_of_possible,") equal moves") } possible := ?list_of_possible t2 := &time write("depth ", mydepth, " calculated in ", t2-t1, "ms") if t2-t1 < 1000 then { delay(1000-(t2-t1)) mydepth +:= 1 } else { mydepth -:= 1 } return possible end initially(p) mydepth := 1 player := ["red", "white"] human := p turn := "red" square := list(8) every !square := list(8, " ") every row := 1 to 3 do every col := 1 + (row % 2) to 8 by 2 do square[row,col] := "white" every row := 6 to 8 do every col := 1 + (row % 2) to 8 by 2 do square[row,col] := "red" end procedure main(argv) game := CheckersGameState((argv[1] == ("red"|"white")) | "red") while not game.finished() do { game.draw_board() if not (L := game.get_move()) then stop("goodbye") if game.apply_move(L) then game.advance_turn() } write((game.finished() || " wins") | "game over, result a draw?") end