Minesweeper in XForms

Steven Pemberton, CWI Amsterdam

Version: 2019-04-01.

Introduction

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.

The Board

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:

  1. it is a lot of repetitive text,
  2. it makes it hard to change if we want to add attributes to the cells, and
  3. it makes it hard to change to other sizes.

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) &lt; 10"/>

Then we do the same for the rows:

<insert ref="row" while="count(//row) &lt; 10"/>

giving us a 10Γ—10 board.

Filling the board with mines

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[.='πŸ’£']) &lt; 10">πŸ’£</setvalue>

Note that this ensures that there are ten mines on the board, even if one or more of the setvalues 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) &lt; /game/@size"/>
  <insert ref="row" while="count(//row) &lt; /game/@size"/>
  <setvalue ref="row[round(random()*9)+1]/c[round(random()*9)+1]"
          while="count(//c[.='πŸ’£']) &lt; /game/@mines">πŸ’£</setvalue>
</action>

Display it

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:

Source

Playing

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:

Source

Adding interaction

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:

  1. record if a cell has been clicked on, and
  2. adapt how the cell is displayed.

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, '&#x00A0;', .))"/></label>
    <setvalue ev:event="DOMActivate" ref="@clicked">yes</setvalue>
</trigger>

(The character &#x00A0; is a non-breaking space).

Which gives us the following game. You can try it out:

Source

Change the output

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, '&#x00A0;', .))"/></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:

Source

Bells, whistles

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=". &gt; 0"/>
    <action ev:event="xforms-ready">
        <insert ref="row/c" while="count(//c) &lt; /game/@size"/>
        <insert ref="row" while="count(//row) &lt; /game/@size"/>
        <setvalue ref="row[round(random()*9)+1]/c[round(random()*9)+1]" 
                  while="count(//c[.='πŸ’£']) &lt; /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, '&#x00A0;', .))"/></label>
            <setvalue ev:event="DOMActivate" ref="@clicked">yes</setvalue>
        </trigger>
    </repeat>
</repeat>

Which looks like this:

Source