The purpose of this assignment is fourfold:
You will complete problems 1-2 and problems A-C.
Hints:
Put your signature, functor, and argument (in comments) in file [[arbitrary.sml]].
Formalize your abstraction by giving an ML signature [[GRAPH]] describing the abstraction. Be sure to
Put your signature into a file called graph-sig.sml. You will need to compile it with the -toplevel option, e.g.,
mosmlc -toplevel -c graph-sig.smlNotice that for this part of the problem you write no code. All you write is the interface.
mosmlc -toplevel -c toposort.sml
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.
You will use two-player games in the last three parts of this assignment: implement a particular game, exercise the AGS on that game, and implement an AGS of your own.
mosmlc -c -toplevel filename.smlThis puts compiler-interface information into filename.ui and implementation information into filename.uo. Perhaps surprisingly, either a signature or a structure will produce both .ui and .uo files. This behavior is an artifact of the way Moscow ML works; it should not alarm you.
Once you have compiled a bunch of modules, you can make an executable binary using mosmlc. Here is an example of a command line I use on my system to build an interactive game player:
mosmlc -toplevel -o games player-sig.uo player.uo game-sig.uo \ ags-sig.uo play-sig.uo slickttt.uo ttt.uo \ ags.uo aggress.uo nim.uo four.uo peg.uo mrun.uoOrder does matter here; for example, I have to put player.uo after player-sig.uo because the Player structure defined in player.sml uses the PLAYER signature defined in player-sig.sml.
When you are debugging, it is tremendously useful to get these compiled modules into the interactive system. You do this with the Moscow ML load function. I have an example use of load in Part B. Remember that if you recompile, you must exit Moscow ML and start over. Once a module is loaded, loading it again has no effect, even if the code has changed.
When writing [[TTT]], you must define all types and values mentioned in the signature [[GAME]], and all values must have the types specified. You might want to define additional values, which you will be able to use as helper functions. No other code will be able to use them outside---they will not be exported, because we've forced structure [[TTT]] to have signature [[GAME]].
So we can test your code, we insist that you use the following names of squares in [[Move.toString]] and [[Move.fromString]]:
upper left | upper middle | upper right -------------+---------------+--------------- middle left | middle | middle right -------------+---------------+--------------- lower left | lower middle | lower rightYou should always print and recognize these full names. If you wish, you may also recognize the abbreviations ul, um, ur, ml, m, mr, ll, lm, and lr in the function [[Move.fromString]].
Here are some hints about how to get started.
You might be tempted to use mutable data to represent game state. Don't even think of it! The problem is that you can't tell when it is safe to mutate the state, because you never know when the AGS might go back to it. If you think you might want immutable arrays, check out the [[Vector]] structure (see the ML supplement). (You can find out what's in any ML structure by typing, e.g., [[open Vector]] at the interactive prompt, or you can consult the Standard Basis documentation. You can also use Moscow ML's help system, e.g,
- help "Vector";If you get interested in vectors, don't overlook the function [[Vector.tabulate]].)
One more thing. You may be tempted to start out by representing the contents of a square on the board using 0 and 1 or other arbitrary values. If you go this route, why not use [[Player.player option]]? It will make your program more elegant and easier to understand.
One thing that AGS assumes that is not captured formally in the signature is that if the function [[finished]] returns false on a configuration then [[possmoves]] will return a non-empty list on that configuration.
If you want to be clever, you can exploit rotation and reflection symmetries to prune the list returned by [[possmoves]]. You may be surprised how much difference this makes. For extra credit,
fun time f arg = let val start = Timer.startRealTimer() val answer = f arg val endit = Timer.checkRealTimer start in print ("Time is " ^ Time.toString endit ^ "\n"); answer endYou can also try [[startCPUTimer]] and [[checkCPUTimer]], but the answers you get are a bit more complicated.
You should try to write [[Move.toString]] in such a way that [[Move.fromString]] and [[Move.toString]] cannot possibly be inconsistent, even if you make a mistake. (Hint: how should you represent a bidirectional map between our names for locations and your internal representation of locations?)
[[Move.toString]] and [[toString]] are not involved in the correctness of the AGS; they are used by the interactive player to show you what's happening. The better your output, the more fun it will be to play against the AGS. You can see a simple sample by running [[~cs152/bin/ttt]].
You should try to write [[Move.fromString]] in such a way that [[Move.fromString]] and [[Move.toString]] cannot possibly be inconsistent, even if you make a mistake. Be sure to try your functions on simple configurations.
The most common mistake on this problem is to permit players to continue to move even when the game is over.
Bob Harper's code for Tic-Tac-Toe is 146 lines of Standard ML. I have a slicker version at only 87 lines---and it is four times faster. It works by exploiting bit-level parallelism using the [[Word]] structure and by flagrantly disregarding most of the hints given above.
These functions can be slow because the AGS tries all possible combinations of moves. Be patient.
We have also provided you an
interactive player. It uses the AGS so
you must instantiate it to the
Tic-Tac-Toe AGS using the following
command:
<
The function [[play]] expects an input
function (one built by [[getamove]]) and a
starting configuration. This function
then starts an interactive loop printing the
intermediate configurations and prompting
the users for moves (or asking the AGS
where appropriate). One example is :
<
This exercise has the following parts:
We've also implemented a version of ``Connect 4'' that would be better
called ``Connect 3'' (since 4 would be too slow).
It is in [[~cs152/bin/four]].
There are a variety of ways to view
benefits; for example, we could
assign larger benefits to winning quickly, and so on.
For this assignment, however, it will be sufficient to consider three
levels of benefits:
Write an AGS using the following template:
<>=
functor AgsFun (structure Game : GAME) : AGS = struct
structure Game = Game
fun bestresult conf = ...
fun bestmove conf = ...
fun forecast conf = ...
end
@ Note that the [[AgsFun]] definition uses the plain colon ([[:]]),
not the opaque signature match [[:>]].
This means that the identity of the types [[Game.Move.move]] and
[[Game.config]] is allowed to ``leak out.''
An alternative is to write the functor this way:
<
You might be tempted to use a ``relative'' outcome like ``Win, Lose, or Tie.''
This can be made to work, but it is harder to get right, especially in games
where players don't always take turns.
Hints:
To test your AGS, you'll need to replace our [[ags.ui]] and [[ags.uo]]
files with the ones you compile from your source code.
At this point you'll be able to run the same test cases you used
earlier, as well as what's in part B.
My AGS takes 34 lines of Standard ML.
This is an adversary game played by two persons using a 3x3 square board.
The players (traditionally called X and O) take turns in placing X's or O's
in the empty squares on the board (player X places only X's and O only O's).
The board is empty in the initial
configuration.
The first player who managed to obtain a full line, column or diagonal
marked with his name is the winner. The game
can also end in a tie. In the picture below the first configuration is
a win for O, the next two are wins for X and the
last one is a tie.
For this game the first player can
always win no matter what the other does.
If you let the AGS start you have no chance. If you play first
you can beat the AGS, but you have to play well.
Proof. Prove the ``forcing'' property of these simple
games as described above above.
Four. Implement Connect 4.
Game.
Suggest another simple adversary game, and (with the instructor's
approval) implement it.
The game
should be small with a small number of possible moves; otherwise the
exhaustive search is infeasible.
Aggression.
With the simple benefits outlined above, the AGS will ``give up'' if
it can't beat a perfect player---all moves are equally bad, and it
apparently moves at random.
What this scheme doesn't account for is that the other player might
not be perfect, so there is a reason to prefer the most distant loss.
In the dual situation, when the AGS knows it can win no matter
what, it will pick a winning move at random instead of winning as
quickly as possible.
This behavior may lead you to suspect bugs in your AGS.
Don't be fooled.
Change your benefits so that the AGS prefers the closest win and the
most distant loss. (This means you can only prune the search if you
find a win in one move.) If you are clever,
you can encode all this information in one value of type [[real]].
Learning.
We can re-use the [[GAME]] signature for more than one purpose.
Implement a ``matchbox'' learning engine in the style explained by
Martin Gardner's article
on the reading list (also on reserve in McKay).
You can use the SML/NJ library to store state with each configuration,
using the following signautre:
-------------
| | X | |
-------------
| | O | |
-------------
| | | |
-------------
Playing Other Games
The code we supply includes a description of the game ``Nim''.
The structure that implements ``Nim''
is called [[structure Nim]]. After
you create an AGS solver and an interactive
player for ``Nim'' you can play Nim
with the AGS. The commands to instantiate AGS
to ``Nim'' are:
<
Building an AGS
C. [30 points] Implement an Abstract Game Solver.
We can summarize the operation of the AGS by saying that in a
particular configuration it evaluates the benefits of all
possible moves and picks the best one.
We could phrase this more precisely as follows:
given a configuration and a player, assign a benefit to that player of
that configuration.
A final configuration in which X has won should have maximum benefit
to X and minimum benefit to O, and vice versa.
Ties should have intermediate and equal benefit to both players.
We compute the benefit of an intermediate configuration by looking at
all possible moves and the benefits of the resulting configurations.
For extra credit you can prove that one of these three
situations must hold in any game described by the [[GAME]] signature,
provided that the game is deterministic and is guaranteed to
terminate after finitely many moves.
In order to make [[bestresult]] work, you'll need some recursive calls.
You'll also want a helper function that lets you compare the benefits
of different outcomes, so [[bestresult]] can choose the most
desirable outcome for the current player.
Descriptions of the games
Here are descriptions of 3 games:
``Tic-Tac-Toe'', ``Nim'' ``and ``Connect 4''.
Do not worry if you
haven't seen the game before---you can learn by playing against a
quasi-perfect player (it would have been perfect if it
were faster). For the purpose of this
assignment you do not have to know
any tricks of the games but only to
understand their rules.
Tic Tac Toe
------------- ------------- ------------- -------------
| X | | X | | | | X | | X | O | | | O | O | X |
------------- ------------- ------------- -------------
| | X | | | O | X | O | | X | O | | | X | X | O |
------------- ------------- ------------- -------------
| O | O | O | | X | | O | | X | | O | | O | X | O |
------------- ------------- ------------- -------------
In this game a player
who plays perfectly cannot lose. I
doubt one can beat the AGS.
Nim
This is an adversary game
played by two persons. The
game is played with number of sticks
arranged in 3 rows. In the initial
state the rows usually contain 3, 5 and 7
sticks respectively. The players take turns in removing sticks:
each player can remove 1, 2 or 3
adjacent sticks from one row. The
one that removes the last stick is the
loser. Or, stated differently the first
player who has no sticks to remove is the
winner. Below were presented two
configurations. The first one is the initial
configuration (for the 3, 5 and 7)
case and the other one is the configuration
obtained after a few moves. A
possible sequence of moves that
might lead to this configuration is:
Row 0: | | | | _ |
Row 1: | | | | | _ _ _ | |
Row 2: | | | | | | | | | | _ _ | _
We have represented a stick using a ``|''and a missing stick using a
``_''. It might be wise to
play with a smaller configuration (2, 3 and 4 for example)
because otherwise the AGS will take too long to produce its answers.
Connect 4
This is an
adversary game played by two persons
using 6 rods and 36 balls. Imagine
the rods standing vertically, and each ball has a hole in it, so you
can drop a ball onto a rod.
The balls are divided in two equal groups
marked X and O. The players take turns in
making moves. A move for a player consists in sliding one
of its own balls down a rod which is not full (the capacity
of a rod is 6). The purpose is to
obtain 4 balls of the same type adjacent on
a horizontal, vertical or diagonal
line. The game ends in a tie when all the
rods are full and no player has won.
We represent below the initial
configuration of the game and a final state where
X has won.
| | | | | | | | | | | |
| | | | | | | | | | | |
| | | | | | | | | | | |
| | | | | | O | | | | |
| | | | | | O | O | | |
| | | | | | O X X X X |
----------- -----------
Our version uses 5 rods and connects 3, because otherwise the AGS
takes too long.
@
Extra Credit
Symmetry. Speed up Tic-Tac-Toe by exploiting symmetry as
described above.
signature ORDERED_GAME = sig
include ORD_KEY
include GAME
sharing type conf = ord_key
end;
You may have to modify the AGS to notify each player of the
outcome of the game. See me for more help with details.
Cross-reference
ML Identifiers
Code chunks
What code we give you and how to compile
In directory [[~cs152/homework/sml]], you'll find sources
for most of the signatures,
structures, and functors in this assignment.
You'll also find an AGS in binary form only.
You'll compile your code using the Moscow ML compiler, [[mosmlc]].
<
You can do two things with the [[.uo]] files:
: nr@labrador 2856 ; mosml
Moscow ML version 2.00 (June 2000)
Enter `quit();' to quit.
- load "ttt";
> val it = () : unit
- open TTT;
> type config = ...
type move = ...
exn Move = Move : exn
val finished = fn : config -> bool
val getmove = fn : player -> string * (string -> move option)
...
Note that once you load a module, you cannot recompile it and
reload it later.
You have to start Moscow ML over again, or perhaps you can recompile
from within the interactive session (I'm not sure about this one).
What to submit
For this assignment you should use the script [[submit-sml]] to submit
The ML files should contain all structure and
function definitions that you write for this assignment
(including any helper functions that may be
necessary), in the order they should be
compiled.
The files you submit must compile with Moscow ML;
files for parts A-B must compile using the Makefile
we give you.
We
will reject files with syntax
or type errors.
Your files should compile without warning messages; we will
deduct points for compiler warnings.
Your [[exercise.sml]] file should be usable with [[use "exercise.sml";]].
If you must, you can include multiple structures in your files, but
please don't make copies of the structures and signatures
above; we already have them.
Acknowledgments
This assignment is derived from one graciously provided by
Bob Harper.
George Necula,
who was his TF at the time (and is now a professor at Berkeley and is world famous as
the inventor of proof-carrying code), did the bulk of the work.
Compiling Standard ML using MLton
If you find that your games are running too slow, you may want to try
compiling them with MLton.
MLton is a whole-program compiler; all your modules must be in a
single file. For example:
cat player-sig.sml player.sml game-sig.sml ags-sig.sml play-sig.sml \
play.sml ttt.sml ags.sml mrun.sml > playttt.sml
mlton-compile playttt.sml
Because MLton requires source code, you will be able to use it only
once you have your own AGS.
More information about MLton is available on the man page and at www.mlton.org.