CS152 Homework: Standard ML Modules

Due Friday, December 15 at 12:10AM (aka Thursday night at midnight)

The purpose of this assignment is threefold:

You will implement a system for playing simple adversary games. The program will show game configurations, accept moves from the user and choose the best move. Additionally, we will have a function that will help us find out if the program is unbeatable.

The system is based on an abstract game solver (AGS) which, given a description of the rules of the game, will be able to select the best move in a particular configuration. An AGS is obtained by abstracting (separating) the details of a particular game from the details of the solving procedure. As the procedure is based on exhaustive searching (trying all valid moves and selecting the best), it is general enough that we can abstract it. If the solving procedure were highly dependent on the game, such a separation would not be easy to make.

An important issue in such a modular design is to determine the interface between the AGS and the particular game. Such an interface is specified using the SML signature [[GAME]]. This signature declares all the data and functions that an AGS must know about the game in order to do its job. We must be careful to make the signature general enough to cover a wide variety of games. Even details like ``the players take turns'' are considered to be part of the game and not the AGS and will therefore be hidden in the game abstraction. You could even implement a solitaire as a ``two-player'' game in which the second player never gets a turn!

The idea behind the AGS is that it tries all possible moves. For each move, it takes the resulting configuration and tries all possible moves for the adversary, and so on. It assumes at each level that the player plays perfectly, that is, he plays the move most likely to result in a win.

This method (``exhaustive search'') is suitable only for very small games. Nobody would use it for a game like chess, for example. Nevertheless, variations of this idea are used successfully even for chess; the idea is to stop or ``prune'' the search before it goes too far. You may have seen the ``alpha-beta'' pruning strategy in CS 51. We won't worry about these variations; we will just implement the basic algorithm.

The assignment has three parts. You will implement a particular game, exercise the AGS on that game, then finally implement an AGS of your own.

Players and Winners

We start by describing a few basic functions which are independent of the game but related to the fact that there are two adversaries. The specifications for these functions are given by the signature [[PLAYER]] and their implementation is given to you in a structure [[Player]]. <>= signature PLAYER = sig datatype player = X | O (* 2 players called X and O *) datatype winner = WINS of player | TIE (* Returns the other player *) val otherplayer : player -> player (* Pretty print. Convert a value to a * printable representation *) val pp_player : player -> string val pp_winner : winner -> string end @ %def PLAYER player winner otherplayer pp_player pp_winner Here's the implementation: <>= structure Player : PLAYER = struct datatype player = X | O datatype winner = WINS of player | TIE fun otherplayer X = O | otherplayer O = X (* Returns the name of a player *) fun pp_player X = "X" | pp_player O = "O" (* The name of the winner *) fun pp_winner TIE = "Tie" | pp_winner (WINS p) = pp_player p end @ %def Player Although it might seem overly pedantic, we prefer to isolate details like the player names and how to convert them to a printable representation. Whenever you refer to a player in your code you will have to use one of the following notations: [[Player.otherplayer p]] or [[Player.X]] or [[Player.O]] or [[Player.WINS p]]. Note that the last three expressions can also be used as patterns.

Specification of a Game

Next we describe the specification of a game (the information our AGS needs to know about the game in order to be able to play it ``intelligently''). Such a description must match the signature [[GAME]]. Read the comments for each type and each value in the signature. <>= signature GAME = sig type config (* A representation for a game configuration. It must include a full description of the state of a game at a particular moment, including keeping track of whose turn it is to move *) eqtype move (* A move (probably a set of coordinates) *) exception Move (* Raised by makemove for invalid moves *) val initial : Player.player -> config (* Initial configuration for a game when "player" is the one to start. We need the parameter because the configuration includes the player to move *) val whoseturn : config -> Player.player (* Extracts the player whose turn is to move from a configuration. We need this because occasionally the solver needs to know whose turn is and it cannot extract this information from the configuration by itself because it doesn't know the layout of a configuration *) val makemove: config -> move -> config (* Changes the configuration by making a move. The player making the move is encoded in the configuration. Be sure that the new configuration knows who is to move. *) val whowon : config -> Player.winner (* Returns the winner. If the configuration is not a final one then it is considered a tie *) val finished : config -> bool (* true if the configuration is final. This might be because everybody is stuck (Tie) or because one has won *) val poss_moves : config -> move list (* A list of possible moves in a given configuration. ONLY final configurations might return nil. This means that a configuration which is not final MUST have some possible moves *) val showmove : Player.player * move -> string (* Returns a short message describing a move. Example: "Player X moves to ..." *) val showconf : config -> string (* Returns an ASCII representation of the configuration. It must show whose turn it is *) val getmove : Player.player -> string * (string -> move option) (* Given a player, return (prompt, f), where prompt is a prompt to request a move for that player f is a function that converts the response to a move If the string response given to f does not correspond to a valid move, f returns NONE. The AGS will interpret such a response as the user wanting to stop the game *) end @ %def GAME initial whoseturn makemove whowon finished poss_moves showmove @ %def showconf getmove

Implement Tic-Tac-Toe

A. [50 points] Implement the description for ``Tic-Tac-Toe.'' For a verbal description of the game refer to the end of this assignment. Call your description structure [[TTT]], put it in the file [[ttt.sml]], and use the following pattern : <