Exploring the game

In this notebook we’ll explore the game in more depth.

First, we’ll load in the work we did on symmetries in the previous notebook.

)copy notebook2
done
./notebook2.dws saved Wed Dec 30 11:40:36 2020
)fns
decode encode list m r show

)vars is another useful system command. It shows us the names of the variables that )copy brought in for us.

)vars
board1 boards encoded hereDir i mi mr180 mr270 mr90 r180 r270 r90 start symmetries

As we saw earlier, ⎕io is not copied, so we’ll need to set it to zero.

⎕io  0

Using symmetries

Let’s start by looking at how we can use our work on symmetries to reduce the number of board positions we need to consider when playing.

For any given board position there are up to seven other positions that are equivalent to it.

Why did I say up to?

Some board configurations are symmetric, so some transformations will leave them unchanged.

Here’s an extreme example. Imagine that x has played in the centre of the board. The board is the same whether you rotate it, reflect it, combine rotations and reflections or leave it unchanged.

  symmetric  3 3'....×....'
... .×. ...

To examine the symmetries using APL we need to convert the board to its vector representation.

Let’s create a function to do that.

see{{,'.×○'}2}

It’s a little more complicated than you might expect because it can handle arrays of boards as well as individual boards.

Let’s try it out.

 sym  see symmetric
0 0 0 0 1 0 0 0 0

You can think of see as an inverse to show.

Let’s check the symmetric transformations on the board.

sym[symmetries]
0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0

They are all identical (as they should be).

How can we get rid of the redundant copies?

APL has a primitive function Unique represented as which does just that.

sym[symmetries] ⍝ the unique version
0 0 0 0 1 0 0 0 0

Let’s look at a position with less symmetry.

 asymmetric  3 3'.×.×○×...'
.×. ×○× ...
+less  see asymmetric
0 1 0 1 2 1 0 0 0
less[symmetries]
0 1 0 1 2 1 0 0 0 0 1 0 0 2 1 0 1 0 0 0 0 1 2 1 0 1 0 0 1 0 1 2 0 0 1 0 0 1 0 1 2 1 0 0 0 0 1 0 1 2 0 0 1 0 0 0 0 1 2 1 0 1 0 0 1 0 0 2 1 0 1 0
less[symmetries]
0 1 0 1 2 1 0 0 0 0 1 0 0 2 1 0 1 0 0 0 0 1 2 1 0 1 0 0 1 0 1 2 0 0 1 0

Now there are four distinct transformations.

Let’s see what they are.

2 show less[symmetries]
┌───┬───┬───┬───┐ │.×.│.×.│...│.×.│ │×○×│.○×│×○×│×○.│ │...│.×.│.×.│.×.│ └───┴───┴───┴───┘
show board1
××. .○○ .×.
2 show board1[symmetries]
┌───┬───┬───┬───┬───┬───┬───┬───┐ │××.│..×│.×.│.○.│.××│×..│.×.│.○.│ │.○○│×○×│○○.│×○×│○○.│×○×│.○○│×○×│ │.×.│.○.│.××│×..│.×.│.○.│××.│..×│ └───┴───┴───┴───┴───┴───┴───┴───┘

board1 is asymmetric and so it has eight distinct symmetries.

Canonical representations

In order to apply Michie’s first simplification we need a way of converting every board position to just one of its symmetries.

We’ll use the idea of a canonical representation. I’ve borrowed that term from Mathematics, but the mathematical details don’t matter to us. You’ll see how we create and use canonical representations in below.

Defining a position’s canonical representation

As we’ve seen, every board position is a member of a set of one to eight boards which can be regarded as the same as far as the game is concerned.

The functions encode and decode allow us to switch between the vector representation of the position and a number in the range zero to 19682.

The symmetries of a given position form a set of up to eight vectors, and each vector can be converted to its unique number using decode.

We’ll take the smallest of those numbers and use that as the canonical representation of the position.

Let’s do that for the boards we just considered: sym, less and board1. We are going to perform the same operation on each, so we can save time by defining a function to do it.

canonical  {  decode [symmetries]}
show encode canonical sym
... .×. ...
show encode canonical less
... ×○× .×.
show encode canonical board1
..× ×○× .○.

Can we find out the canonical representations of all positions?

Let’s try

all  canonical encode 3*9
RANK ERROR
canonical[0] canonical←{⌊⌿decode ⍵[symmetries]}
                                  ∧

What’s gone wrong?

We’ve used a simple version of indexing in the definition of canonical. The [] form of indexing corresponds to the notation that is used in a lot of programming languages, but the format is slightly different for vectors and matrices. The syntax above works for vectors, but we’re using it with a matrix.

Luckily APL has another form of indexing based on the Index function represented as . Index can solve the problem we hit above.

You can see how Index works in the example below.

(0 3 2)7 8 9 10 20 ⍝ the same as 7 8 9 10 20[0 3 2]
7 10 9

Below you’ll see a function called ild, which is short for index last dimension.

Here’s the definition for ild, along with an updated definition of canonical.

ild  {()1} ⍝ index last dimension
canonical  {decode symmetries ild }
 canonical encode 3*9
19683

How many of these are distinct? We’d expect quite a few repetitions.

⍴∪canonical encode 3*9
2862

That’s a big simplification, but we can further reduce the number of board positions by eliminating some of the positions that can’t crop up in play.

One example: in any valid board position, there will be one fewer ’s than ×’s after the first player has moved, and the number of × and characters will be the same after the second player has moved.

If we count the number of ones and twos in the vector representation, for any valid board position the number of ones will be the same or one greater than the number of twos.

There’s a neat way to check that in APL. Below you’ll see the approach working on a small set of position vectors.

 samples  encode 0 1 9 10 11 18 20 19682 
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 1 0 2 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0 2 0 2 2 2 2 2 2 2 2 2 2

Of these, let’s decide which are valid by counting the 1’s and 2’s by eye:

0 0 0 0 0 0 0 0 0 is valid

0 0 0 0 0 0 0 0 1 is valid

0 0 0 0 0 0 1 0 0 is valid

0 0 0 0 0 0 1 0 1 is invalid because × has played twice but has not played at all

0 0 0 0 0 0 1 0 2 is valid

0 0 0 0 0 0 2 0 0 is invalid, because has played before ×

0 0 0 0 0 0 2 0 2 is invalid because has played twice but × has not played at all

2 2 2 2 2 2 2 2 2 is invalid, because has binged and covered the whole board!

How can we implement these rules in APL?

Let’s start by considering the vector 0 0 0 0 0 0 1 0 2.

v  0 0 0 0 0 0 1 0 2
1 2 ∘.= v
0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1

∘.= is an equals outer product. It produces an array which shows where each value in the argument on its left is equal to each value in the argument on its right.

In this case, one argument is a 2-element vector, and the other is a 9 element vector, so the result is a 2 by 9 matrix of zeros and ones.

+/1 2 ∘.= v
1 1

+/ is a plus reduction. It sums the values along the first dimension of n array.

In this case it totals the number of 0s and the number of 1s in the original vector.

We can find the difference between the counts of 1s and 2s by applying -/. That’s a minus reduction.

-/+/1 2 ∘.= v
0

That tells us that the number of ×s and s is the same.

Let’s put that code in a function I’ve named cd (short for count difference) and try another vector.

cd  {-/+/1 2 ∘.=} 
cd 0 0 0 0 0 0 1 0 1
2

This tells us that there are two more ×s than s, which means that the position cannto occur in play.

For the position to be possible the difference must be 0 or 1.

APL has a primitive function membership, represented by , which returns a 0 for each element on its left that is’nt in the vector on its right.

1 2 0 5 1 2  0 1
1 0 1 0 1 0

We can check the result of cd using

(cd 0 0 0 0 0 0 1 0 1)  0 1 ⍝ not a possible position
0
(cd 0 0 0 0 0 0 1 2 1)  0 1 ⍝ a possible position
1

Those brackets are distracting! The code would be easier to read if the arguments to were reversed.

APL has an operator switch to do that.

0 1  cd 0 0 0 0 0 0 1 2 1
1

In APL a function returns data as its result. An operator corresponds to what other languages call a higher order function. It takes functions and/or data and returns a function as its result.

Let’s create a new function that will determine if a given position is ok (in other words, possible). Rather than using cd we will copy its code in-line

ok{0 1-+/1 2∘.=}      ⍝ do positions have counts of × and ○ that could occur in play?
ok 0 0 0 0 0 0 1 2 1
1
ok 0 0 0 0 0 0 1 0 1
0

What happens if we apply it to a matrix?

ok samples
1 1 1 0 1 0 0 0

That’s not right. The problem is that cd works on a vector but not a matrix. One small change will fix that.

We need to make sure that the minus reduction will always be applied to the first dimension of its argument.

Here’s the updated version ofcd

cd  {-+/1 2 ∘.=}
ok samples
1 1 1 0 1 0 0 0

Let’s check that the updated code has picked the possible configurations.

list samples⌿⍨ok samples
┌───┬───┬───┬───┐ │...│...│...│...│ │...│...│...│...│ │...│..×│×..│×.○│ └───┴───┴───┴───┘

Let’s explore the whole set of canonical positions.

uc  encode  canonical encode 3*9
2862 9

uc is the matrix of unique canonical positions.

Let’s see which ones look possible.

ucp  uc⌿⍨ok uc
850 9

ucp contains a matrix of the unique coanonical possible positions.

There are only 850, which is manageable.

Here are the first 19. They just fit across the page without wrapping.

list 19ucp
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │...│...│...│...│...│...│...│...│...│...│...│...│...│...│...│...│...│...│...│ │...│...│...│...│...│...│...│...│..×│..×│..×│..×│..×│..×│..×│..×│..×│..×│..×│ │...│..×│.×.│.×○│.○×│×.○│××○│×○×│.×○│.○.│.○×│×.○│×○.│×○○│○..│○.×│○×.│○×○│○○×│ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

It would be interesting to see a few ordered by move mumber.

It’s easy to find the move number for each row in ucp. It’s just the total of non-zero values in that row.

mn  +/ucp0
850

APL has a pair of sorting functions called upgrade and downgrade .

They return permutation vectors that will sort their arguments into ascending or descending order.

TODO: include some discussion of this article: https://devblogs.microsoft.com/oldnewthing/20170102-00/?p=95095

We can use upgrade on mn to reorder ucp and then display the first few plausible game positions. We’ll also show which move number they ocurr at.

mnp  mn
(list  19ucp[mnp;]) (19mn[mnp])
┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │...│...│...│...│...│...│...│...│...│...│...│...│...│...│...│..×│...│...│...│ │...│...│...│.×.│...│...│...│..×│..×│..○│.×.│.×.│.○.│.○.│×.○│...│...│...│..×│ │...│..×│.×.│...│.×○│.○×│×.○│.○.│○..│×..│..○│.○.│..×│.×.│...│○..│××○│×○×│.×○│ ├───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ │0 │1 │1 │1 │2 │2 │2 │2 │2 │2 │2 │2 │2 │2 │2 │2 │3 │3 │3 │ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘

As you can see there are only three types of first move to which there are a total of twelve repsonses.

Later in the game there will be positions in ucp that cannot happen in a real game. These are positions that could only happen after a player has won the game.

We’ll ignore that for now, as we have greatly reduced the number of positions we need to consider.

What we will do is to save the decoded (integer) values for each plausible position, as these will be useful later. We’ll display the first few values

19ucpn  decode ucp
0 1 3 5 7 11 14 16 32 33 34 38 42 44 45 46 48 50 52

Let’s save the workspace.

)save notebook3 -force
notebook3.dws saved Wed Jan 6 09:19:18 2021
)fns
canonical cd decode encode ild list m ok r see show