Feb 16, 2018: Ordering

Here is the documentation for Alloy's seq library.

A note from Kathi's guest lecture on Wednesday: overloading of terminology in computer science. In formal mathematics, the 'thing' that satisfies a set of constraints is called a model. However, we like to talk about 'modeling' a system, so we use the term instance to describe what satisfies our model.

Pro tip: You can run a specific predicate/check/etc. in Alloy by using the 'Execute' menu and selecting the thing you'd like Alloy to run. No need to comment out!

Tic-Tac-Toe

Previously, we talked about how to model a board and what it meant to win. Tim also filled in some additional predicates since we last worked on Tic-tac-toe.

What else do we need to model in Tic-Tac-Toe besides just the board? Games! We want to model a sequence of moves that ends in a win.

We'll use Alloy's util/ordering library:

open util/ordering[Board]

This library gives us a notion of ordering over Boards, with some nice keywords:

What constraints might you add here to force our Tic-Tac-Toe model to be games, rather than just a sequence of boards? One idea: Constrain that adjacent boards in the sequence are different only by a valid move.

fact ValidGame {
  all b: Board | {
    some mover, movec: Index, movep: Player |
      move[b, mover, movec, movep, b.next]
  }
}

Running this gives us unsatisfiable. How did we over-constrain this? The last board doesn't have a next, but we've said that the it must have a valid move that transitions to a valid board. last.next evaluates to {}. To correct for this, we update our fact to:

fact ValidGame {
  all b: Board - last | {
    some mover, movec: Index, movep: Player |
      move[b, mover, movec, movep, b.next]
  }
}

Question: Can you explain again where next and last came from?
Answer: We get this from the ordering library that we opened. If you look at the module source, you can see functions like first, next, and last that produce objects of the type we order over.

What else do we need to constrain for a valid game? We need to say that the first board is empty.

fact Initial {
  -- first board empty!
  no first.places
}

Let's see what Alloy can find us with this: run findWinner { some m: Player | winning[last, m] }

Question: Did we give Alloy enough Boards?
Answer: The default bound is 3, and this predicate is unsatisfiable, so we need more Boards.

A small note about util/ordering: it enforces that Alloy uses exactly the number of atoms given. So here we give Alloy exactly 9 boards: run findWinner { some m: Player | winning[last, m] } for 9 Board

Question: Does this mean we'll only ever get sequences where the 9th board is a win state?
Answer: For this execution, yes.

This is annoying. Does it mean you can't win until the 9th move - no it doesn't. It isn't such a bad thing that all games have 9 boards; we can let Alloy create boards past the winning one. For example, let's get an instance where someone wins on move 5: run findWinner5 { some m : Player | winning[first.next.next.next.next, m] } for 9 Board. This is unsatisfiable, even if we change to use only 5 Board. Any idea what's going on here?

First, let's try running the empty predicate run findWinner {} for 9 Board and see if we can get an instance where a player wins before the ninth move. On the first instance we use the evaluator to find that winning[last,X] = true meaning X wins. We also get that winning[last.prev,X] = true, so we know our Tic-Tac-Toe is winnable in 8 moves. Let's see if we can do it in 7. We'll switch back to the predicate (so we don't have to click the 'Next' button in the visualizer a bunch of times) and do run findWinner7 {some m: Player | winning[last.prev.prev, m] }. This is satisfiable.

But why was findWinner5 unsatisfiable? Because we said the first board was empty! No player can win using only two moves. So instead we should be running for 10 boards. We modify our original run predicate to be:


run findWinner { some m: Player | winning[last, m] } for 10 Board

We allowed the spec to keep making moves after a win. What would happen if we forced the game to end on a win? Then we would only get instances where a player won on the last of the exactly 10 boards we gave Alloy. So it is important to allow the spec to have states beyond the ending one and then give Alloy enough atoms to run with. Making a weak spec like this is actually better.

Question: Could we have a state where both X and O are winners?
Answer: Yes, and we could check this with Alloy.

Tim's first move conjecture

We'd like to get something out of this. We want to learn something about the system we're modeling. Tim wants to prove a conjecture he had when he was younger: that if you are X and you play in the center first, then you can't lose.

pred lossAlthoughPlayedMiddle {
  first.next.places[Two][Two] = X
  winning[last,O]
}
run lossAlthoughPlayedMiddle for 10 Board

We get that this is satisfiable, so young Tim was wrong.

Question: But doesn't this only lead to an instance when X plays unintelligently?
Answer: Yes, because we haven't encoded any strategy. You'll get a chance to try encoding strategy in models in your next lab!

Question: Don't you also need to say that X isn't winning?
Answer: Yes, we should have included that.
Note: We got unsatisfiable when we added this. This is because any completely filled board with 3 O's in a row and X in the middle must also contain 3 X's in a row. So alloy considers both O and X to be winning in the last state. This is one of the downsides to having to play out the game past a win in our models. However, with more time we could have written a predicate that finds games where X starts in the middle and O wins before X does.

Events

To help us model systems with states, we're going to add the notion of an Event

abstract sig Event {
  before, after: Board
}
sig MoveEvent extends Event {
  mover: Index,
  movec: Index,
  movep: Player
}
face doMoveEvent {
  all e: MoveEvent |
    move[e.before, e.mover, e.movec, e.movep, e.after]
}

And we'll update ValidGame to use the Event idiom

fact ValidGame {
  all b: Board - last | {
    some e: MoveEvent | e.before = b and e.after = b.next
  }
}

run findWinner {some m: Player | winning[last, m] } for 10 Board, 9 Event

The visualizer for this looks a little crazy, so we project over Board to make the moves a bit more clear

Sequences

For the last part of class we're going to look at another way of creating the ordering of Boards. Let's make a game be it's own sig. We won't use util/ordering and will instead use seq:

one sig Game {
  boards: seq Board
}

A sequence does not need a pre-set ordering and instead creates relation between natural numbers and objects. It is more expensive for Alloy to process than util/ordering

fact ValidGame {
  all i: game.boards.inds - Game.boards.lastIdx | {
    let i' = add[1, i] |
      some mover, movec: Index, movep: Player |
        move[Game.boards[i], mover, movec, movep, Game.boards[i']]
  }
}

We can continue to remodel our spec with sequences, but it will be slower to run and more annoying for us to model. In general, use sequences when you specifically need the indexed ordering and other options they provide. Otherwise, ordering is the better option. For the case of Tic-Tac-Toe, util/ordering is the better option; we tried using seq as an exercise to introduce the library.

seq is relatively new in Alloy and not included in the textbook, so we'll send out some material documenting how sequences are used in Alloy.