# A Maple program to play the game of Noughts and Crosses # using a Maplet interface if available. For Maple 6 and later. # # Copyright © 2003 by Francis J. Wright. # School of Mathematical Sciences, Queen Mary, University of London, UK. # http://centaur.maths.qmul.ac.uk/ # Time-stamp: <01 May 2003> # # This file may be opened directly by Windows with Maple 8 or later # installed and should automatically start the Maplet Viewer. On all # platforms, it may be read into either a Maple worksheet or # command-line Maple by executing # # > read "OandX.maplet"; # including a filepath if necessary # # This file defines a package module called OandX that exports the # following three procedures. OandX:-PlayC runs a conventional # sequential interactive I/O loop interface. If Maplet support is # available then OandX:-PlayM runs a much better event-driven Maplet # interface. OandX:-Play is identical to PlayM if it is defined or to # PlayC otherwise. It is run automatically unless the variable # OandX_no_autoplay is assigned the value true before this file is # read. # OandX_QUIET := interface('quiet, quiet=true'): # Reset at EOF # This is just for tidier execution in command-line Maple. OandX := module() export PlayC, PlayM, Play; local # Procedures: n2ij, ij2n, Initialize, ShowGrid, TryMove, TryNeighbour, Wins, I_Win, Check_Status, Move, tty, response, EnableSelect, EnablePlay, FlashWin, UpdateMove, Options, Update, Restart, # Variables: Grid, moves, allowed, message, WinLine, WinSets, ComputerWinSets, UserWinSets, user_starts, user, computer, playlevel; option package; ###################################################################### # Utility, initialization and display procedures ###################################################################### n2ij := proc(n::posint) # Convert square label n to matrix indices (i,j). local i, j; i := iquo(n-1, 3, j); (i+1, j+1) end proc; ij2n := proc(i::posint, j::posint) # Convert matrix indices (i,j) to square label n. # This inline proc MUST be DEFINED before it is used! option inline; 3*(i-1)+j end proc; Initialize := proc() local i, j; Grid := Array([seq([seq(ij2n(i,j), j=1..3)],i=1..3)]); moves := 0; allowed := [$1..9]; message := ""; WinLine := {} end proc; ShowGrid := proc() local i, j; plots[display]({ plot({[[0,1],[3,1]], [[0,2],[3,2]], [[1,0],[1,3]], [[2,0],[2,3]]}, colour = BLACK), seq(seq(`if`(nargs > 0 and member(ij2n(i,j), WinLine), NULL, plots[textplot]([j-0.5, 3.5-i, Grid[i,j]], font=[HELVETICA,`if`(Grid[i,j]::string,48,24)], colour=`if`(member(ij2n(i,j), WinLine), RED, BLACK))), i=1..3), j=1..3)}, scaling = CONSTRAINED, axes = NONE) end proc; ###################################################################### # Move making procedures ###################################################################### WinSets := {{1,2,3},{4,5,6},{7,8,9}, # horizontal {1,4,7},{2,5,8},{3,6,9}, # vertical {1,5,9},{7,5,3}}: # diagonal # The global constant `WinSets' represents the set of all the winning # lines as sets of square labels. The global variables `UserWinSets' # and `ComputerWinSets' are subsets of `WinSets' still accessible to # the user and computer respectively. TryMove := proc(i::posint, j::posint) # Try to move computer to square (i,j) and # return true if possible, false otherwise. local n; if Grid[i,j]::integer then Grid[i,j] := computer; n := ij2n(i,j); UserWinSets := remove(has, UserWinSets, n); allowed := remove(`=`, allowed, n); true else false end if end proc; TryNeighbour := proc(i::posint, j::posint) # Try all four squares neighbouring square (i,j). # Try right, left, down, up in that order. # Return true if successful, false otherwise. (j < 3 and TryMove(i,j+1)) or (j > 1 and TryMove(i,j-1)) or (i < 3 and TryMove(i+1,j)) or (i > 1 and TryMove(i-1,j)) end proc; Wins := proc(XO::string) # "X" or "O" # Return true if X/O has won. local S, w; # Construct the set of XO squares: S := select(n->Grid[n2ij(n)]=XO, {$1..9}); # Check whether it includes a winning set # as a subset: for w in WinSets do if w subset S then WinLine := w; return true end if end do; false end proc; I_Win := proc() message := "Yippee! I win."; true end proc; Check_Status := proc() if Wins(computer) then I_Win() else moves := moves + 1; false end if end proc; Move := proc(n::posint) # Make user's move to square n, then make computer's response. # Return true if the game has been won, false otherwise. local i, j, S, w, r; # Make user's move: i,j := n2ij(n); Grid[i,j] := user; if Wins(user) then message := "Congratulations! You win."; return true end if; allowed := remove(`=`, allowed, n); # Make computer's move: if playlevel > 0 then # Expert mode -- play to win! # Respond to the user's first move: if moves = 0 then UserWinSets := WinSets; ComputerWinSets := remove(has, WinSets, n); if user_starts then # Take the centre if possible, # otherwise take a random corner: if not TryMove(2,2) then r := rand(1..2); TryMove((1,3)[r()],(1,3)[r()]) end if; else # computer started (in centre)... # Respond to user by choosing a neighbouring square: TryNeighbour(i,j) end if; moves := 1; return false else ComputerWinSets := remove(has, ComputerWinSets, n); # First, look for a winning move. # Construct the set of computer's squares: S := select(n->Grid[n2ij(n)]=computer, {$1..9}); # Check whether it includes a winning set: for w in ComputerWinSets do if nops(w intersect S) = 2 and TryMove(n2ij(op(w minus S))) then WinLine := w; return I_Win() end if end do; # Second, try to block user from winning. # Construct the set of user's squares: S := select(n->Grid[n2ij(n)]=user, {$1..9}); # Check whether it includes a winning set: for w in UserWinSets do if nops(w intersect S) = 2 and TryMove(n2ij(op(w minus S))) then return Check_Status() end if end do end if end if; # The fallback is to use VERY NAIVE ALGORITHMS! # If playlevel = 0 then respond to the user by choosing a # neighbouring square if appropriate and possible: if not ( playlevel = 0 and TryNeighbour(i,j) ) then # Otherwise, just move randomly: TryMove(n2ij(allowed[rand(1..nops(allowed))()])) end if; Check_Status() end proc; ###################################################################### # User interface procedures I. This version provides a conventional # loop-driven command-line interface. ###################################################################### tty := evalb(substring(interface(version),1..3) = 'TTY'); response := proc(PROMPT::string) if tty then printf(PROMPT) else interface('prompt' = PROMPT) end if; readline() end proc; PlayC := proc(pl::integer) local PROMPT, n, play_again, won; PROMPT := interface('prompt'); # Now ensure that the prompt is reset regardless of errors: try do print(); if nargs = 0 then print(`Play levels: -1 = random; 0 = naive; +1 = expert`); print(`These values can also be passed as arguments to Play().`); do playlevel := parse(response("Enter play level: n > ")); if playlevel = NULL then return end if; if playlevel::integer then break end if; print(`Please enter a number!`) end do else playlevel := pl end if; Initialize(); user_starts := response("Do you want to start: y(es) or n(o) > "); if user_starts = "" then return end if; user_starts := member(user_starts[1], {"y","Y"}); if user_starts then user, computer := "X", "O" else computer, user := "X", "O"; # Make the computer's first move: if playlevel >= 0 then # expert or naive n := 5 else # random n := rand(1..9)() end if; Grid[n2ij(n)] := "X"; allowed := remove(`=`, allowed, n) end if; won := false; # Maximum of 4 VALID moves possible. while not won and moves < 4 do print(ShowGrid()); print(`To play, enter one of the numbers shown in the grid.`); print(`To win, complete a line before the computer does.`); print(`Null input terminates the game.`); print(cat( `You play as `, user, `; the computer plays as `, computer, `.`)); n := parse(response(cat("Enter your move, one of ", convert(allowed, 'string')[2..-2], ": > "))); if n = NULL then break end if; if not member(n, allowed) then print(`Please enter a free square number!`); next end if; won := Move(n) end do; print(ShowGrid()); if moves = 4 then message := "Stalemate! (No winner)" end if; if message <> "" then print(convert(message, 'symbol')) end if; print(`Game over! To play again later execute Play().`); play_again := response("Do you want to play again now: y(es) or n(o) > "); if play_again = "" or not member(play_again[1], {"y","Y"}) then break end if end do finally interface('prompt' = PROMPT) end try end proc; Play := PlayC; ###################################################################### # User interface procedures II. This version provides an event-driven # graphical user interface via a maplet. ###################################################################### if type(Maplets, `module`) then PlayM := proc(pl::integer) local at, PL; at := "@"; # to hide my email address from webcrawlers # Default play level: if nargs = 0 then PL := 0 else PL := pl end if; Initialize(); # Note that callbacks from the maplet must be defined as # identifiers, rather than strings, so as to pick up the lexical # rather than global scope! use Maplets:-Elements in Maplets[Display]( Maplet( Window( "Play Noughts and Crosses", resizable = false, BoxRow( [Plotter['PL1'](ShowGrid())], BoxColumn( BoxColumn('inset' = 0, 'spacing' = 0, 'border' = true, 'caption' = "Select play options", BoxRow( Label("Play level: "), RadioButton['RB1']("r&andom", 'tooltip' = "Computer plays randomly", 'value'=evalb(PL=-1), 'group'='BG1'), RadioButton['RB2']("na&ive", 'tooltip' = "Computer plays naively", 'value'=evalb(PL=0), 'group'='BG1'), RadioButton['RB3']("&expert", 'tooltip' = "Computer plays expertly", 'value'=evalb(PL=+1), 'group'='BG1')), BoxRow( Label("Who starts: "), RadioButton['RB4']("c&omputer", 'tooltip' = "Computer plays first", 'value'=true, 'group'='BG2'), RadioButton['RB5']("&user", 'tooltip' = "User plays first", 'value'=false, 'group'='BG2')) ), BoxRow( Button['B1']("&Play", 'tooltip' = "Start playing the game", Evaluate('function' = 'Options', Argument('RB1'), Argument('RB2'), Argument('RB3'), Argument('RB5'))), Button("&Restart", Evaluate('function' = 'Restart'), 'tooltip' = "Start a new game"), Button("&Cancel", Shutdown(), 'tooltip' = "Close the Maplet"), Button("A&bout", RunDialog('MD1'), 'tooltip' = "About Noughts and Crosses") ), BoxColumn( 'border' = true, 'caption' = "Play the game", BoxRow('inset' = 0, Label("To play, select a number shown in the grid.")), BoxRow('inset' = 0, Label("To win, complete a line before the computer does.")), BoxRow('inset' = 0, Label['L1']('foreground' = 'blue', " ")), GridLayout('inset' = 0, [seq([seq(Button[BM||k]("&"||k, 'enabled' = false, 'tooltip' = "Play in square "||k, Evaluate('function' = 'Update'(k))), k=i)], i=[1..3,4..6,7..9])]) ), BoxColumn( 'border' = true, 'caption' = "Result", BoxRow('inset' = 0, Label['L2']('foreground' = 'red', " ")) ), BoxRow( Label("To play again later within Maple execute Play().")) ))), ButtonGroup['BG1'](), ButtonGroup['BG2'](), MessageDialog['MD1']( information, title = "About Noughts and Crosses", caption = cat( "Copyright © 2003 by Francis J. Wright.\n", "School of Mathematical Sciences,\n", "Queen Mary, University of London, UK.\n", "F.J.Wright", at, "qmul.ac.uk\n", "http://centaur.maths.qmul.ac.uk/")) )) end use end proc; EnableSelect := proc(tf::boolean) # Set Select dialogue enabled options to tf. local i; Maplets:-Tools:-Set(seq(RB||i('enabled') = tf, i=1..5), 'B1(enabled)' = tf) end proc; EnablePlay := proc(tf::boolean) # Set Play dialogue enabled options to tf. local i; Maplets:-Tools:-Set(seq(BM||i('enabled') = tf, i=1..9)) end proc; FlashWin := () -> plots[display]([ShowGrid(), ShowGrid('hide')], insequence = true); UpdateMove := proc(won::boolean) # Update display of grid and allowed moves. local i; Maplets:-Tools:-Set( `if`(won,op(['PL1(value)' = FlashWin(), 'PL1(play)' = true, 'PL1(continuous)' = true, 'PL1(delay)' = 500]), 'PL1' = ShowGrid()), seq(BM||i('enabled') = member(i, allowed), i=1..9)) end proc; Options := proc() # 4 booleans # Called when "Select" button is pressed. local n; for n to 3 while not args[n] do end do; playlevel := (-1,0,+1)[n]; user_starts := args[4]; if user_starts then user, computer := "X", "O"; EnablePlay(true) else computer, user := "X", "O"; # Make the computer's first move: if playlevel >= 0 then # expert or naive n := 5 else # random n := rand(1..9)() end if; Grid[n2ij(n)] := "X"; allowed := remove(`=`, allowed, n); UpdateMove(false) end if; Maplets:-Tools:-Set('L1(caption)' = cat("You play as ", user, ", the computer plays as ", computer, ".")); EnableSelect(false) end proc; Update := proc(n::posint) # Called when "Play" button is pressed. # Get user's move, then update grid and possible moves. local won; won := Move(n); UpdateMove(won); # Maximum of 4 VALID moves possible. if moves = 4 then message := "Stalemate! (No winner)" end if; if won or moves = 4 then EnablePlay(false); if message <> "" then Maplets:-Tools:-Set('L2(caption)' = message) end if end if end proc; Restart := proc() # Called when "Restart" button is pressed. Initialize(); Maplets:-Tools:-Set('PL1' = ShowGrid(), 'L1(caption)' = " ", 'L2(caption)' = " "); EnablePlay(false); EnableSelect(true) end proc; Play := PlayM; end if; end module: ###################################################################### # For Emacs: # Local Variables: # mode: text # eval: (auto-fill-mode nil) # indent-tabs-mode: nil # End: interface('quiet' = OandX_QUIET); if not OandX_no_autoplay = true then OandX:-Play() end if;