Lightweight TicTacToe in F# This is more of a 'model' of TicTacToe, as you'll see it's not "gameified" in the sense that it will allow for inputs and outputs nor will it record any "active" state of the game but those things are trivial to implement.

I'm cheating a little bit on the functional paradigm in my function to rotate the grid, but this is on purpose, it's just easier to rotate the grid by mutation than by going pure functional style, in the end, the state is only manipulated in a local function anyhow so I think it's neglible.

Also there are many times where I have ignored optimizations over conciseness. One might imagine that this would not be an area where optimal performance is key so I have prioritized getting as concise and readable code as possible.

module TicTacToe

type Mark = None | Cross | Nought 
type Outcome = None | NoughtWon | CrossWon | Draw

let emptyGrid = (None, Array2D.init 3 3 (fun _ _ -&gt; Mark.None))

let flatten (x: 'a [,]) = x |&gt; Seq.cast&lt;'a&gt;

let rotateCw (grid: 'a [,]) =
    let out = Array2D.zeroCreate 3 3
    for y in 0..2 do
        for x in 0..2 do
            out.[x,y] &lt;- grid.[0 + y, 2 - x]
    out

let checkOutcome grid = 
    let checkSeg seg =
        seq { for i in 0..2 do
                let count = 
                    seg i |&gt; Array.sumBy 
                        (function Cross -&gt; 1 | Nought -&gt; 2 | _ -&gt; 0)
                if count = 6 || count = 3 then 
                    yield (if count = 6 then NoughtWon else CrossWon) }
        |&gt; Seq.tryFind ((&lt;&gt;)None)
    let vertical =
        let rot = grid |&gt; rotateCw
        (fun x -&gt; rot.[x, 0..2])
    let horizontal = (fun y -&gt; grid.[0..2, y])
    let diagonal (m:_[,]) = (fun _ -&gt; [| for i in 0..2 -&gt; m.[i,i] |])
    let diagonal2 = grid |&gt; rotateCw |&gt; diagonal
    let checks = 
        [checkSeg vertical; checkSeg horizontal; 
         checkSeg (diagonal grid); checkSeg diagonal2]
    if checks |&gt; List.exists (Option.isSome) then
        checks |&gt; List.find (Option.isSome) |&gt; Option.get
    elif grid |&gt; flatten |&gt; Seq.forall ((&lt;&gt;)Mark.None) then Draw
    else None

let placeMark pos mark (_, grid: Mark [,]) = 
    let res = 
        Array2D.init 3 3 (fun x y -&gt; 
            if (x,y) = pos then mark else grid.[x,y])
    (checkOutcome res, res)
let emptyGrid = (None, Array2D.init 3 3 (fun _ _ -&gt; Mark.None))



This can be simplified using Array2D.create:

let emptyGrid = None, Array2D.create 3 3 Mark.None




In rotateCw and placeMark, x is used as the first index and y as the second. This is confusing, as the usual order is row then column. For instance,

let test = Array2D.create 3 3 Mark.None
test.[2, 1] &lt;- Cross
printfn "%A" test

[[None; None; None]
 [None; None; None]
 [None; Cross; None]]




rotateCw can be simplified (assumes the indexing changes in the previous paragraph)

let rotateCw (grid : 'a[,]) = Array2D.init 3 3 (fun y x -&gt; grid.[2 - x, y])




I find this version of placeMark a bit more readable

let placeMark (x, y) mark (_, grid : Mark[,]) =
    let res = Array2D.copy grid
    res.[y, x] &lt;- mark
    (checkOutcome res, res)