Steven Pemberton, CWI Amsterdam
Version: 2019-04-01.
As the name suggests, XForms was originally designed for dealing with forms. However, thanks to its generalised design, since version 1.1, it has been suitable for much more.
In this example, we will show how to write a Minesweeper game.
We're going to program for a 10Γ10 board of cells.
We could just write the board out wholesale:
<instance> <game xmlns=""> <row><c/><c/><c/><c/><c/><c/><c/><c/><c/><c/></row> <row><c/><c/><c/><c/><c/><c/><c/><c/><c/><c/></row> <row><c/><c/><c/><c/><c/><c/><c/><c/><c/><c/></row> <row><c/><c/><c/><c/><c/><c/><c/><c/><c/><c/></row> <row><c/><c/><c/><c/><c/><c/><c/><c/><c/><c/></row> <row><c/><c/><c/><c/><c/><c/><c/><c/><c/><c/></row> <row><c/><c/><c/><c/><c/><c/><c/><c/><c/><c/></row> <row><c/><c/><c/><c/><c/><c/><c/><c/><c/><c/></row> <row><c/><c/><c/><c/><c/><c/><c/><c/><c/><c/></row> <row><c/><c/><c/><c/><c/><c/><c/><c/><c/><c/></row> </game> </instance>
but this has three disadvantages:
Instead, we start off with a board of one row of one cell:
<instance> <game xmlns=""> <row><c/></row> </game> </instance>
and then grow it to the size we want. To do this we use an
action
that responds to the xforms-ready
event, that
gets dispatched when the XForms processor starts:
<action ev:event="xforms-ready"> ... </action>
The simplest form of the <insert/>
action just duplicates
the last element of a (non-empty) list. So starting with
<row><c/></row>
the action
<insert ref="row/c"/>
will give you
<row><c/><c/></row>
So we just do that until we have ten cells in the row:
<insert ref="row/c" while="count(//c) < 10"/>
Then we do the same for the rows:
<insert ref="row" while="count(//row) < 10"/>
giving us a 10Γ10 board.
To put a mine at position 3, 5 you write:
<setvalue ref="row[3]/c[5]">π£</setvalue>
The function random()
returns a value between zero and one. So
random()*9
returns a number between zero and 9, and
round(random()*9)+1
returns an integer between one and ten. So to
place a mine at a random row and column, you write:
<setvalue ref="row[round(random()*9)+1]/c[round(random()*9)+1]">π£</setvalue>
And to distribute ten mines at random locations, you write:
<setvalue ref="row[round(random()*9)+1]/c[round(random()*9)+1]" while="count(//c[.='π£']) < 10">π£</setvalue>
Note that this ensures that there are ten mines on the board, even if one or
more of the setvalue
s happens to place a mine where there already
is one.
So putting this all together so far, let us also add data to specify the size of the board and the number of mines:
<instance> <game size="10" mines="10" xmlns=""> <row><c/></row> </game> </instance> <action ev:event="xforms-ready"> <insert ref="row/c" while="count(//c) < /game/@size"/> <insert ref="row" while="count(//row) < /game/@size"/> <setvalue ref="row[round(random()*9)+1]/c[round(random()*9)+1]" while="count(//c[.='π£']) < /game/@mines">π£</setvalue> </action>
We can display the board like so (with a suitable bit of CSS to format the cells, setting height and width and adding a border, not shown here):
<repeat ref="row"> <repeat ref="c"> <output value="."/> </repeat> </repeat>
which would look like this:
To play the game, all cells are initially blank, and the player clicks on one. If that cell contains a mine, the game is over; otherwise, a number is displayed that tells you how many of the eight neighbouring cells contain a mine.
So we fill each non-mine cell with the count of its mine-filled neighbours:
<bind ref="row/c[.!='π£']" calculate="count(...something here...)"/>
The neighbours are the eight cells that surround a cell. Those are the preceding and following cell in the same row, and then, in the preceding and following row, the three cells around the same position as in the context row.
The preceding and following cells in the same row are easy:
preceding-sibling::c[1]
and
following-sibling::c[1]
The preceding row is
../preceding-sibling::row[1]
and we want to find the cell at the same position as the context cell. The position of the context cell is the count of the preceding cells in the row, plus one:
1+count(context()/preceding-sibling::c)
so the same cell in the preceding row is:
../preceding-sibling::row[1]/c[1+count(context()/preceding-sibling::c)]
and the preceding and following cells to that are:
../preceding-sibling::row[1]/c[ count(context()/preceding-sibling::c)]
and
../preceding-sibling::row[1]/c[2+count(context()/preceding-sibling::c)]
The same argument goes for the following row, but using
following-sibling
instead.
So we now have eight neighbours; what we want to do is select those that contain mines, and then count them.
You join the eight together with the "|" operator. You select the mines
using [.='π£']
, and you count them using the count
function:
count( (../preceding-sibling::row[1]/c[2+count(context()/preceding-sibling::c)] | ../preceding-sibling::row[1]/c[1+count(context()/preceding-sibling::c)] | ../preceding-sibling::row[1]/c[ count(context()/preceding-sibling::c)] | preceding-sibling::c[1] | following-sibling::c[1] | ../following-sibling::row[1]/c[2+count(context()/preceding-sibling::c)] | ../following-sibling::row[1]/c[1+count(context()/preceding-sibling::c)] | ../following-sibling::row[1]/c[ count(context()/preceding-sibling::c)]) [.='π£'] )
(Note that this is XPath 2; to write "(a|b|c)[x]
" in XPath 1,
you have to write "(a[x]|b[x]|c[x])
". )
So putting this together,
<bind ref="row/c[.!='π£']" calculate="...the expression above..."/>
Displaying this gives something like:
So, as we said, to play the game, all cells are initially blank, and the player clicks on one which either reveals that the cell is a mine, or otherwise the number of mine neighbours. So we have to do two things:
We wrap the output of the number with a trigger
. This is the
equivalent of a button
in HTML, except that XForms tries to be
representation-neutral, and so avoids using naming that suggests a particular
representation:
<repeat ref="row"> <repeat ref="cell" > <trigger appearance="minimal"> <label><output value="."/></label> </trigger> </repeat> </repeat>
The appearance="minimal"
is an indication that you don't want
it formatted as a button, just as regular text, but still acting as a
button.
However, this trigger doesn't do anything yet. To achieve this we add an action within the trigger:
<trigger appearance="minimal"> <label><output value="."/></label> <setvalue ev:event="DOMActivate" ref="@clicked">yes</setvalue> </trigger>
This responds to the DOMActivate
event on the
trigger
, which is the event that occurs when a trigger is clicked
on. Its effect is to set the clicked
attribute on the cell. So we
have to add that attribute to the instance:
<instance> <game size="10" mines="10" xmlns=""> <row><c clicked=""/></row> </game> </instance>
And now to change the output:
<trigger appearance="minimal"> <label><output value="if(@clicked='', 'β‘', .)"/></label> <setvalue ev:event="DOMActivate" ref="@clicked">yes</setvalue> </trigger>
This would be sufficient, except in traditional Minesweeper games, if a cell has no mine neighbours (so the count is zero), it is displayed as a blank square, rather than a zero:
<trigger appearance="minimal"> <label><output value="if(@clicked='', 'β‘', if(.=0, ' ', .))"/></label> <setvalue ev:event="DOMActivate" ref="@clicked">yes</setvalue> </trigger>
(The character  
is a non-breaking space).
Which gives us the following game. You can try it out:
One other thing: if you click on a cell with a count of zero, that means that none of the eight neighbours are mines, and so they can be clicked on with impunity. Traditional Minesweeper games do this work for you, so that if you click on a cell with a count of zero, all neighbouring squares are automatically revealed.
To do this, we need to record not only if a cell has been clicked on, but
also if it has been revealed. So we add a revealed
attribute to
the cells:
<instance> <game size="10" mines="10" xmlns=""> <row><c clicked="" revealed=""/></row> </game> </instance>
change the output to use that instead:
<trigger appearance="minimal"> <label><output value="if(@revealed='', 'β‘', if(.=0, ' ', .))"/></label> <setvalue ev:event="DOMActivate" ref="@clicked">yes</setvalue> </trigger>
and then add a bind that tells us whether the cell should be revealed or not.
A cell should be revealed if it has been clicked on or if one of its neighbours has been revealed and has a count of zero. Fairly straightforward:
<bind ref="row/c/@revealed" type="boolean" context=".." calculate="@clicked or count(neighbours[@revealed and .=0])!=0)"/>
(where neighboursΒ is the expression we had above). Actually, when used in a boolean expression like above, you don't have to count the neighbours: if there are any matching neighbours the expression is true, and otherwise false:
<bind ref="row/c/@revealed" type="boolean" context=".." calculate="@clicked or (neighbours[@revealed and .=0])"/>
This gives us the following game:
We can keep track of the state of the game (whether it is won or lost) by
adding a state
attribute to the game element. You have lost if
there is a cell with a mine that has been revealed; you have won if the number
of cells not yet revealed is the same as the number of mines. This is
expressible with the following bind:
<bind ref="@state" calculate="if(//c[.='π£' and @revealed], 'lose', if(count(//c[not(@revealed)])=count(//c[.='π£']), 'win', '')))"/>
We can help the player by displaying the count of cells that still have to be revealed:
<bind ref="@count" calculate="count(//c[not(@revealed)])-count(//c[.='π£'])"/>
Giving us our final game:
<model> <instance> <game size="10" mines="10" state="" count="" xmlns=""> <row><c clicked="" revealed=""/></row> </game> </instance> <bind ref="row/c" calculate="if(.='π£', ., count( (../preceding-sibling::row[1]/c[2+count(context()/preceding-sibling::c)] | ../preceding-sibling::row[1]/c[1+count(context()/preceding-sibling::c)] | ../preceding-sibling::row[1]/c[ count(context()/preceding-sibling::c)] | preceding-sibling::c[1] | following-sibling::c[1] | ../following-sibling::row[1]/c[2+count(context()/preceding-sibling::c)] | ../following-sibling::row[1]/c[1+count(context()/preceding-sibling::c)] | ../following-sibling::row[1]/c[ count(context()/preceding-sibling::c)]) [.='π£'] )"/> <bind ref="row/c/@revealed" type="boolean" context=".." calculate="@clicked!='' or ( (../preceding-sibling::row[1]/c[2+count(context()/preceding-sibling::c)] | ../preceding-sibling::row[1]/c[1+count(context()/preceding-sibling::c)] | ../preceding-sibling::row[1]/c[ count(context()/preceding-sibling::c)] | preceding-sibling::c[1] | following-sibling::c[1] | ../following-sibling::row[1]/c[2+count(context()/preceding-sibling::c)] | ../following-sibling::row[1]/c[1+count(context()/preceding-sibling::c)] | ../following-sibling::row[1]/c[ count(context()/preceding-sibling::c)])[@revealed and .=0])"/> <bind ref="@state" calculate="if(//c[.='π£' and @revealed], 'lose', if(count(//c[not(@revealed)]=@mines, 'win', ''))" relevant=".!=''"/> <bind ref="@count" calculate="count(//c[not(@revealed)])-count(//c[.='π£'])" relevant=". > 0"/> <action ev:event="xforms-ready"> <insert ref="row/c" while="count(//c) < /game/@size"/> <insert ref="row" while="count(//row) < /game/@size"/> <setvalue ref="row[round(random()*9)+1]/c[round(random()*9)+1]" while="count(//c[.='π£']) < /game/@mines">π£</setvalue> </action> </model> <output ref="@state"/> <output ref="@count" label="Left: "/> <repeat ref="row"> <repeat ref="c"> <trigger appearance="minimal"> <label><output value="if(not(@revealed), 'β‘', if(.=0, ' ', .))"/></label> <setvalue ev:event="DOMActivate" ref="@clicked">yes</setvalue> </trigger> </repeat> </repeat>
Which looks like this: