The two games use the same underlying combinators for implementing the board. The first is a button that can display changing graphics:
whereboardButtonF :: (ColorGen bgcolor, Graphic gfx) => bgcolor -> Size -> F gfx Click boardButtonF bg size = buttonF'' (setBgColor bg) (g (blankD size)) >=^< Left . setLabel . g
buttonF''
is the dynamically customisable (see
Section 30.3) version of buttonF
and
setLabel
is a customiser that changes the button
label.
The second combinator is boardF
,
which, given the size of the board and a function from the coordinates of a square to a fudget implementing that square, creates a parallel composition of square fudgets with the appropriate layout. The square fudgets are addressed with their coordinates.type Coord = (Int,Int) boardF :: Coord -> (Coord -> F a b) -> F (Coord,a) (Coord,b) boardF (w,h) squareF = placerF (matrixP w) $ listF [((x,y),sqF (x,y)) | y<-[0..h-1],x<-[0..w-1]]
Figure 98. The Explode game.
Figure 99. The Othello game.
In the Explode game, two players take turns placing stones, or atoms, in the squares of a board. A player can not place atoms in a square that already contains atoms from the opponent. When a square is full, that is, contains as many atoms as it has neighbours, it explodes, sending one atom to each neighbour. All atoms of the invaded square change color to the invading atom's color. Invaded squares may become full and explode in turn. When the board has settled, a new move can be entered. When the board starts to get full of atoms, placing a new atom may cause an infinite chain reaction. When this happens, the game is over and the player who caused it is the winner.
main = fudlogue (shellF "Explode" explodeBoardF) explodeBoardF = loopF (absF routeSP >==< boardF boardSize boardSize atomsSquareF) where routeSP = concatMapAccumlSP route White route side (src,msg) = case msg of ClickedWhileEmpty -> (otherSide side, [(src,side)]) ClickedWithColor side' -> if side'==side then (otherSide side, [(src,side)]) else (side, []) -- illegal move Explode dsts side' -> (side, [(dst,side')|dst<-dsts]) atomsSquareF :: Coord -> F AtomColor (SquareEvent Coord) atomsSquareF (x,y) = loopThroughRightF (absF ctrlSP) atomsButtonF where ctrlSP = concatMapAccumlSP ctrl (0,Nothing) ctrl s@(oldcnt,oldside) msg = case msg of Left Click -> (s,[Right (case oldside of Just side -> ClickedWithColor side Nothing -> ClickedWhileEmpty)]) Right side -> let cnt=oldcnt+1 in if cnt>=size then become (cnt-size) side (explodemsgs side) else become cnt side [] become cnt side msgs = ((cnt,optside),Left (cnt,side):msgs) where optside = if cnt==0 then Nothing else Just side size = length neighbours explodemsgs = (:[]) . Right . Explode neighbours neighbours = filter inside2d [(x-1,y),(x+1,y),(x,y-1),(x,y+1)] inside2d (x,y) = inside x && inside y inside x = 0<=x && x<boardSize atomsButtonF :: F (NumberOfAtoms,AtomColor) Click atomsButtonF = boardButtonF bgColor sqsize >=^< drawAtoms drawAtoms (count,color) = ... -- 20 linesFigure 100. Fudgets implementation of the Explode game.
Comments:
routeSP
allow
all square fudgets to communicate with each other. However, each
square knows its coordinates and send messages only to its
neighbours. The actual communication structure is thus not
directly reflected in the program structure.routeSP
also acts as a referee. It keeps track of whose
turn it is, to be able to discard illegal moves.
This is also where you would put a test for
explosions involving all squares (which should end the
game). Otherwise, all the work is done by the squares
themselves.SquareEvent
:
where the messages meandata SquareEvent dest = ClickedWhileEmpty | ClickedWithColor AtomColor | Explode [dest] AtomColor
ClickedWhileEmpty
"I was clicked and I was
empty". routeSP
then replies with an atom of the appropriate
color (depending on whose turn it is).ClickedWithColor color
: "I was clicked and my color
was color". If the color matches with color of the current
player, routeSP
replies with an atom of that color, otherwise
the message is ignored (some indication of an illegal move
could be produced).Explode square color
: "I explode and
invade square with color". routeSP
forwards the message to the square at square. 2-4 messages of
this kind are sent when a square explodes.atomsButtonF
.drawAtoms
in atomsButtonF
produces the appropriate graphical image for a square, using
the types FlexibleDrawing
and Drawing
described in
Chapter 27.In the Fudgets solution, the squares are in effect connected to all other squares, whereas in the Gadgets solution, each square process is connected through wires only to its neighbours. As noted in [NR95], combining fudgets to achieve exactly this connectivity would be difficult, and it would probably also be difficult to add new processes to such a solution.
As described above, in the current Fudgets solution, each square fudget knows its coordinates and computes the coordinates of its neighbours. It would of course be possible to parameterise the square fudgets by an abstract representation of the neighbours instead,
and letatomsSquareF :: [address] -> F AtomColor (SquareEvent address)
routeSP
compute the concrete addresses of the
neighbours. This perhaps makes the communication pattern more
visible in the program text and prevents errors in the
implementation of atomsSquareF
from breaking the pattern.
Since the Gadgets system uses indeterministic process scheduling, it is necessary to explicitly keep track of when an explosion is in progress and when the board is stable, since moves are allowed to be entered only when the board is stable. The implementation of Fudgets is deterministic and internal communication has priority over external communication, so user input is automatically queued until an explosion has subsided.
In the implementation of the Explode game, we used a distributed
solution: the work of computing the effects of the users' moves is
handled almost entirely by the square fudgets. In the
implementation of Othello, we have taken the opposite approach:
the board fudget only displays the board and receives user
input. The checking of the validity of a move and computation of
its effect is handled by a stream processor attached in the
typical way with the loopThroughRightF
combinator (see
Section 18.2). The structure of the program is:
The functionmain = fudlogue (shellF "Othello" ottoF) ottoF = displayF >==< loopThroughRightF (absF newGameSP) ottoBoardF >==< buttonF "New Game" ottoBoardF :: F BoardReq Coord ottoBoardF = ... newGameSP = playSP (reptree moves startpos) playSP :: GameTree -> SP (Either Coord Click) (Either BoardReq String) playSP current_position = ...
reptree
(lazily) computes a game tree which has
startpos
as the root node and where the children of a node
are obtained by applying the function moves
to it.
The stream processor playSP
checks the current position for
whose turn it is to play, and then either waits for the user to
enter a move or computes a good move for the computer using
standard alfa-beta game-tree search.