dwayne/elm-xo/1.0.0

XO

XO is a reference to Xs and Os which is an alternative name for Tic-tac-toe.

Player

type Player
= X
| O

A player is either an X or an O.

Get the next player.

nextPlayer X == O


nextPlayer O == X

Game

type Game

Keeps track of the internal state of a game.

This state includes: who played first, who's the current player, and the configuration of the board.

Initialize a game with the given player having the first move.

Play

type alias Position =
( Int, Int )

A (row, column) position. Both coordinates are 0-based and should range from 0 to 2.

= OutOfBounds Position
| Occupied Position
| GameAlreadyEnded

The possible errors that could occur while playing a game.

play : Position -> Game -> Result PlayError Game

Make a move at the given position with the current player of a game.

type alias Rules =
{ winnerPlaysFirst : Bool
, takeTurnsOnDraw : Bool
}

Various rules that help determine how subsequent games are played.

defaultRules : Rules

By default, the winner plays first and when a game ends in a draw the other player who didn't play first, gets to play first in the next game.

playAgain : Rules -> Game -> Game

Play another game and initialize it using the given rules.

State

type alias State =
{ first : Player
, turn : Player
, outcome : Outcome
}

The external state of a game.

  • Who played first?
  • Whose turn is it?
  • What's the outcome?
type Outcome
= Win Player (List Line)
| Draw Player
| Undecided

Either there is a winner, the game ended in a draw, or it is undecided because the game is still in progress.

type alias Line =
( Position, Position, Position )

The three positions on the board that represent the winning line.

toState : Game -> State

Get the state of a game.

openPositions : Game -> List Position

Get the unoccupied positions on the board of a game.

Transform

type alias Tile =
Maybe Player

Represents which player, if any, is at a position on a Tic-tac-toe board.

map : (Position -> Tile -> a) -> Game -> List a

There are 9 positions/tiles on a Tic-tac-toe board. Apply a function over those positions/tiles in row-major order.

map Tuple.pair (start X)
    == [ ( ( 0, 0 ), Nothing )
       , ( ( 0, 1 ), Nothing )
       , ( ( 0, 2 ), Nothing )
       , ( ( 1, 0 ), Nothing )
       , ( ( 1, 1 ), Nothing )
       , ( ( 1, 2 ), Nothing )
       , ( ( 2, 0 ), Nothing )
       , ( ( 2, 1 ), Nothing )
       , ( ( 2, 2 ), Nothing )
       ]
toString : Game -> String

A representation of the layout of the board for a game in row-major order.

toString (start X) == "........."


let
    game0 =
        start X


    game1 =
        play ( 0, 0 ) game0 |> Result.withDefault game0


    game2 =
        play ( 1, 1 ) game1 |> Result.withDefault game1
in
toString game2 == "x...o...."

Analyze

type alias Report =
{ optimum : Optimum
, possibilities : List ( Position, Int )
}

The report tells you the best outcome that can be achieved, assuming both players play perfectly.

possibilities is a list of pairs of position and number of moves. Each pair represents the position to play, to achieve the best outcome, and the number of moves it takes to achieve it.

type Optimum
= W
| D
| L

The best outcome that can be achieved.

  • W means you can win
  • D means you can draw at best
  • L means no matter what position is played you will lose
= NoOpenPositions
| NoGameInProgress

Not all games can be analyzed. For e.g. if all positions are occupied or the game is over then there's nothing to analyze.

Find the good unoccupied positions on the board of a game for the current player.

Example 1

Suppose O has the next turn in a game, g1, with a board layout of x.o.x...., then

--
--    0   1   2
-- 0  x |   | o
--   ---+---+---
-- 1    | x |
--   ---+---+---
-- 2    |   |
--
findGoodPositions g1
    == { optimum = D
       , possibilities = [ ( ( 2, 2 ), 6 ) ]
       }

It means that O can draw at best, in 6 moves, if O is played at (2, 2).

Example 2

Suppose O has the next turn in a game, g2, with a board layout of xo..x...., then

--
--    0   1   2
-- 0  x | o |
--   ---+---+---
-- 1    | x |
--   ---+---+---
-- 2    |   |
--
findGoodPositions g2
    == { optimum = L
       , possibilities =
            [ ( ( 0, 2 ), 2 )
            , ( ( 1, 0 ), 2 )
            , ( ( 1, 2 ), 2 )
            , ( ( 2, 0 ), 2 )
            , ( ( 2, 1 ), 2 )
            , ( ( 2, 2 ), 4 )
            ]
       }

It means that no matter the position O plays, O will lose. However, if O is played at (2, 2), in order to block X from the immediate win, then O can extend the game a little more.

Example 3

Suppose X has the next turn in a game, g3, with a board layout of x.ox...o., then

--
--    0   1   2
-- 0  x |   | o
--   ---+---+---
-- 1  x |   |
--   ---+---+---
-- 2    | o |
--
findGoodPositions g3
    == { optimum = W
       , possibilities =
            [ ( ( 1, 1 ), 3 )
            , ( ( 1, 2 ), 3 )
            , ( ( 2, 0 ), 1 )
            , ( ( 2, 2 ), 3 )
            ]
       }

It means that X can win. X can either win immediately if X is played at (2, 0), or X can have some fun and win in 3 moves if X is played at (1, 1), (1, 2), or (2, 2).

Example 4

Suppose X has the next turn in a game, g4, with a board layout of x.o...x.o, then

--
--    0   1   2
-- 0  x |   | o
--   ---+---+---
-- 1    |   |
--   ---+---+---
-- 2  x |   | o
--
findGoodPositions g4
    == { optimum = W
       , possibilities = [ ( ( 1, 0 ), 1 ) ]
       }

It means that X can win immediately if X is played at (1, 0). Notice that winning is favoured over blocking O at (1, 2).

N.B. All the analysis above assumes perfect play by both players.