These tasks are broken down for you below.
There is a very simple front-end which should work in all Haskell
implementations in Main.hs. There is
also a fancy front-end using "brick", "stack", and "cabal"
developed by Ben MacAdam which is available here
(superseded version here)
-- only if you are very keen/know what you are doing should you
consider download this (it needs a unix platform). Niran Pon
has cloned the repository here:
he has removed the dependency on the Haskell version (this
involves changing "
base >=4.13 && < 4.14"
to "
base" everywhere in the cabal
files) and has some work arounds for windows users (
for the Windows users, checkers-brick
depends on VTE a Unix library, thus this dependency must be
removed).
Once you have a frontend (and survived these operating system
level dependencies if you go for the fancy frontend), you need the
datatype for a game state in Types.hs:
data GameState =
GameState { blackPieces :: [Coord]
, redPieces :: [Coord]
, blackKings :: [Coord]
, redKings :: [Coord]
, status :: Status
, message :: String
, history :: Moves }
The "status" field tells you whether it is red or black to play,
while the "message" field allows you to display information to the
user(s) -- the "message" field is very useful while
debugging! The pieces (or pawns) and the kings positions are
indicated by two dimensional coordinates starting at zero: the
first coordinate counts across the board from left to right, the
second coordinate counts down the board. The red player
starts at the bottom of the board (moving up) while the black
player starts at the top of the board (moving down).
The initial game state has all the pieces in their home position
and red gets first move. There are four ways provided to
play the game:
==================================================================
Finding legal moves
Your first job is to, given a state of the game, find all the
legal moves which can be made from that state of the
game. To test this on gradescope you will need to
package this into a module called "Checkers.Moves" (which will
import "Checkers.Types") in the file "Moves.hs" which you can
submit to gradescope. This module should export a function:
moves::GameState -> (SMorJM [Move])
Here is the template for "Moves.hs".
This function breaks down the moves into the simple (non-jump)
moves and the jump moves. A move is specified by a list of
coordinates, [PorK Coord], which indicate the sequence of places
where the piece lands in its move and whether the piece is a Pawn
or a King (this information is useful in checking repeated moves).
To program this one breaks the moves into two parts the
"jump_moves" and the "simple_moves". Recall that
if a jump move is available then the player -- whose turn it is --
must make a jump. Only if there are no jumps
can he make a simple move. A simple move for a piece (pawn)
is a diagonal one square move forward (to an empty position) while
a king can move one square backward or forward diagonally.
Thus a simple move is specified by two coordinates,
[start,end]. A jump move is specified by a list of
coodinates [start,landing1,landing2,...,end] where each sequential
pair is a two step diagonal move which "captures" or "jumps over"
a position occupied by an opponent piece which is removed from the
board. The basic jump captures only one piece: more
interesting are the mutiple jump moves. For a king these can
literally go in circles as a king can jump diagonally forward or
backward. A piece (pawn), however, must always jump
diagonally forward (although the direction can
change). A complication (Calgary rules) is that half
way through a jump sequence a piece (or pawn) can become a king by
reaching the opposite end so the jumps can then continue in any
direction!
You should develop two functions:
simple_moves::GameState -> [Move]
and jump_moves::GameState -> [Move]
which combine intomoves::GameState -> (SMorJM [Move])
moves st | jumpmoves /= [] = JM (jump_moves st)
| simplemoves /= [] = SM (simple_moves st)
| otherwise = EndM
Your next task is to program the required function:
apply_move:: Move -> GameState -> GameState
I suggest you put this in a module "Checkers.ApplyMove" (in the
file "ApplyMove.hs") you should import "Checkers.Moves" and
"Checkers.Types" one can then see whether the move is in the set
of all possible legal moves you have calculated. If it is
you can go ahead and make the move: be careful to turn pieces
(pawns) into kings when they reach the far end. If the
move is not legal, you can modify the "message" field of the state
to indicate that an illegal move has been made. Don't
forget you can use this field for debugging as well ...
Once you have an apply_move function you can try out the program
as a human-human interaction. It is important that you
thoroughly test this stage of the development as if you get the
moves wrong everything will be wonky after that!!!
You can check this on gradescope: to do this you have to
upload both "Moves.hs" and "ApplyMove.hs"
(here is a template for ApplyMove.hs).
This stage should involve about 200 lines of Haskell code.
This stage requires some careful planning: you must be clear on
how you are going to break down the various tasks before you
launch into programming them!!! It is worth discussing this
in the labs ...
Programming the AI
The two basic techniques are to use a "heuristic" function and
"min-max search". Let us start with a really simple
heuristic function (see more below on this). You can package
this into a module "Heuristic" if you wish. You will need
two functions:
The first gives a heuristic evaluation of the game state from the perspective of the the black player. For example one may take the difference in the number of pieces:black_heuristic:: GameState -> Int red_heuristic::GameState -> Int
Similarly by swapping black and red one may obtain a red heuristic. Using this you must program a min-max search. For the tournament it will be useful to have this in a module: you could initially call "ABsearch". This involves "looking ahead" a number of moves by searching the (implicit) game tree formed by considering your moves, the opponent responses, your responses, etc to some specified depth (called a "ply") at which you use the heuristic function to determine how good the state reached is for the player you are representing. One then evaluates the tree with your player trying to maximize the value of his move while the opponent tries to minimize the value of the move. This is why the game tree is sometimes called a "min-max tree"!#blackPieces-#redPieces+2*(#blackKings-#redKings).
It is useful to make the number of "ply" look-ahead a parameter
of your program so that you can experiment with how effective and
how long your ai takes given the depth of look-ahead! A
basic program should be able to look-ahead about 6 moves and take
less than 10 seconds. A technique you may want to add
-- when you have a basic ai going -- is called "waiting for
quiescence": the idea is that before using the heuristic one
should wait for the "jump" moves to be over as these greatly
effect the value of a position being evaluated.
A basic min-max search with \alpha-\beta pruning should only take
only about 40 lines of code ... but the code is quite
sophisticated so you must understand what it is doing!
Again test your code carefully. Once you have a basic system
you can start adding all sorts of improvements!!
Heuristics
The last step in developing an ai is to consider more
sophisticated heuristics. Something to consider is that what
is important is actually different at the different stages
of the game!
Thus it is possible to have quite sophisticated heuristics ...
!GOOD LUCK!
We have published some test game states to help you to determine whether you are finding all the possible moves (see below). You should also try playing some (human/human) checkers games with these functions.moves::GameState -> SMorJM [Move]
apply_move::Move -> GameState -> GameState
Stage III: heuristicsred_ai:: GameState -> Move
black_ai::GameState -> Move
You will need to import this module into main. If you get to this stage you will have a tournament ready program!!module Checkers.RobinCockett (moves, apply_move, red_ai, black_ai)
These are fairly detailed implementation notes for stage
I:
Ultimately you need to write the program in main.hs
in the src folder ... but in order to test it you may want to have
a light weight version which imports the datatypes from Types.hs.
What I suggest is that you pick up the file Types.hs which
contains all the datatypes needed. You can place this in
your working directory (perhaps the src directory):
=============================
<Moves.hs>
module Checkers.Moves where
import Checkers.Types
-------------ADD YOUR PROGRAM FOR MOVES-------------
--------------------BELOW HERE----------------------
==============================
If you arrange things like this we can then pick up your moves
file and easily test it by importing your moves module into our
tests on gradescope
Moves:
As mentioned above your first aim is to get the
simple moves sorted out various possibilities must be covered
depending on the status of the game:
simple_moves:: GameState -> [Move]
simple_moves st
| _status
st == Red
= (simpleKing (redKings st))++(simplePiece (redPieces st))
| _status st == Black
= (simpleKing (blackKings st))++ (simplePiece (blackPieces st))
| otherwise = []
where ...
One then has to program the simple moves in the
context of the "where". For example to get the simple moves
for the pieces one might use a list comprehension roughly like
(note PorK data is missing!):
simplePiece xs =
[ [(x,y),(x',y')]
| (x,y) <- xs
, (x',y') <- let y'=y+dir in [(x+1,y'),(x-1,y')]
, notoccupied (x',y') st && onboard (x',y')]
where "dir" is the direction of the player (either
Red or Black determined by (status st) ) involved.
The form of the moves function involves testing
whether there are jump moves: recall if there are jump moves they
must be taken: one only does a simple move when there are
no jumps. This suggests a "moves" function something like:
moves st = (simplemoves st,jumpmoves st)
The first challenge of this first stage is to get
the jump moves programmed. Below is an incomplete version
for jumping kings (there is no PorK data). It is very
similar for pieces/pawns (recall that they can turn into kings).
jumpKing xs = [(x,y):ys | (x,y)
<- xs, ys <- jumpKing' (x,y) [] (x,y)]
jumpKing' start rem (x,y) =
[ (x'',y''):ys
| ((x',y'),(x'',y'')) <-
[((x+1,y+1),(x+2,y+2)),((x-1,y+1),(x-2,y+2)),((x+1,y-1),(x+2,y-2)),((x-1,y-1),(x-2,y-2))]
, not (member (x',y') rem) && opponent_occupied (x',y') st
&& (start==(x'',y'') & notoccupied (x'',y'') st
&& onboard (x'',y'')
, ys <- jump_over (jumpKing' start ((x',y'):rem) (x'',y'')) ]
jump_over [] = [[]]
jumpover z = z
The complication is that as one jumps pieces they
get removed from the board and one must avoid jumping them again
(otherwise you can go round in circles!). Also your starting
position is no longer occupied so it is possible to jump through
that. For this reason in defining the jumps recursively one
must pass in the positions (in "rem") which have already been
jumped over and the start position ("start"). This
allows the appropriate checks for validity of the jump.
There is one additional subtelty: when the jumps are over one must
return not just an empty list of jumps (signifying there are none)
but rather a list containing the empty list in order to smoothly
terminate the recursive calling of the jumps. This is the
purpose of "jump_over" function.
This should get you through generating the possible
moves from a state!!!!!
Repeated states:
The wrinkle I have not addressed (this is the
challenging part of the whole assignment and should get you head
scratching!!!) is using the history to tell whether one has
repeated a state. As pawn moves and jumps are irreversible
the only moves one has to worry about are the simple king
moves. In this case you are looking for a repetition of the
current state: this means looking back in the moves to see whether
there is an earlier stage at which each piece was in exactly the
same spot or, more challengingly, the black and red kings are in
some permutation of their original position. So there must
have been a sequence of history moves in which, for each king,
there is a series of simple king moves which take the colored
kings back to the same place or (more generally) a permutation of
those places.
To determine this is one uses the history to trace
where each piece came from. To program this keep two lists
-- black moves and red moves -- of "movement pairs" (a start
position and a current position): these lists will initially be
empty. For each move in the history update the appropriate lists
by altering the start position of the piece being moved back to
its earlier start position. When the appropriate list is
empty -- or the piece has not been moved before -- this will
amount to adding the move itself to that appropriate list.
Note that all you need to know is the positions of the pieces as
each piece (red or black) occupies a unique position and a move
gives a before and after position. Furthermore, you need
not keep track of pieces which have not moved. This means
one can eliminate movement pairs that record no movement.
Furthermore you can eliminate any cycles which occur in either of
these lists. If at some stage in the history (of simple king
moves) you discover each piece came from where it is currently --
which means the movement list is empty again -- you have repeated
a state. Similarly, if you reach a state in which
every move (in each list) is in a cycle so can be removed then you
have a repeated state.
In programming this it may be worth first not
worrying initially about removing permutations from the list so
that you check for non-repeating positions which allow similarly
colored pieces to be permuted. After mastering this
add removing cycles ...
Apply_moves:
Your next task is to apply the moves to a
state! Again for debugging you may like to start by
developing this in your "Moves" module in your working
directory. Eventually however you will want to import
"Moves" into the "ApplyMove" module.
In order to use the front end in "Main" you must
import "Moves" and "ApplyMove" and call:
main = frontend $ GameConfig { engine =
apply_move
, blackMove = Human
, redMove = Human
, state = initialGameState }
This will allow you to interactively play checkers
using the arrow keys, space bar (to enter a position) and return
(to return a move).
Of course, the apply move function not only does the
move but checks that it is valid. This can be done by just
checking whether it is in the list of legal moves:
apply_move mv st | inmoves mv (moves st) = ....
| otherwise = st{message = "Illegal move!!"}
The last is record syntax for modifying a field in a
record and it produces a GameState which is the same as "st"
except the message has been changed.
When the code is legal one must separate out the
cases: is it a simple move or a jump move? What are you
moving?
Here a code sample for making a simple red king
move:
make_simple_move [start,end] st
| _status st == Red
&& member start (redKings st)
= st{redKings = replace start end (redKings st)
, status = change_player st
, message = ""}
| _status st == Black
&& member start (blackKings st)
= ...
For jump moves the code is recursive but not very
different. Here is a code snippet for a red king jump
make_jump_move (start:(next:rest)) st
| _status st == Red
&& member start (redKings st)
= make_jump_move (next:rest)
(st{blackKings = remove (jumped start next) (blackKings st)
, blackPieces = remove (jumped start next) (blackPieces st)
, redKings = next:(remove start (redKings st))
, message = ""})
| _status st == Black
&& member start (blackKings st)
= ...
This should allow one to get through to having an
interactive board in which human can play human.
You next task is to get the "ai" to play one of the
players ...
Currently you don't need
stack .... so ignore this section!!
This system does work on unix based personal computers:
(1) For the Mac system you need Xcode tools: in the command line "xcode-select --install". To then install stack go to "haskellstack.org" and follow the unix operating system instructions. Then go to the git repository "github.com/benjamin-macadam/Haskell-Checkers-Frontend" and download the repository.
(2) If you have a Linux/Fedora/.. system goto "haskellstack.org" and follow the unix operating system instructions.
N.B. You may need to install the "libtinfo" (terminal info) library. On ubuntu you say "sudo apt install libtinfo-dev".
Then go to the git repository "github.com/benjamin-macadam/Haskell-Checkers-Frontend" and download the repository.
For Windows systems:
(a) Lower than Windows
10: goto https://www.virtualbox.org/ then
install VirtualBox (it is free). Then install ubuntu under
the VirtualBox environment (you are guided through the
installation by VirtualBox). Open a terminal inside ubuntu
... then proceed as for the unix system. Make sure you have
allocated enough disc space ...
(b) For Windows 10 or more you need install a unix emulator. Here is our recommendation: pick up ubuntu (for wsl2 -- Windows Subsystem for Linux) from the window app store (it is free): here is a link to how to install "https://docs.microsoft.com/en-us/windows/wsl/install-win10".
Once you have installed ubuntu, click the windows key and type "ubuntu": this should bring up a ubuntu shell. You can now follow the instructions for unix described above, although everything now has to be done in the ubuntu shell. Don't forget that you need the "libtinfo" library ...
Your "normal" windows files are in /mnt/c. You need to download the repository into /home/username ...
There is a nice editor for working with wsl called "Vscode" ....
Please note: The downloads are rather slow so this can take some time!!!