Loading the Kaya editor…
// Tic-Tac-Toe with a face: board.html draws the board, and this program
// owns every rule. You are X; the program answers as O by taking the first
// open square, so every game is deterministic and no model is called.
//
// faces.json declares the face: it reads ttt.state and sends ttt.move
// {cell}. Each move the program consumes is answered by exactly one
// ttt.state, so the face can lock the board while a move is in flight.

// Any session client can post ttt.move straight to the event door, past the
// face's schema, so map the raw value to a board index or -1.
function cellIndex(raw: any): number {
  for k in [0, 1, 2, 3, 4, 5, 6, 7, 8] {
    if raw == k {
      return k
    }
  }
  return -1
}

function firstOpen(cells: string[]): number {
  for k in [0, 1, 2, 3, 4, 5, 6, 7, 8] {
    if cells[k] == "" {
      return k
    }
  }
  return -1
}

function hasLine(cells: string[], mark: string): boolean {
  let lines = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]
  for line in lines {
    if cells[line[0]] == mark && cells[line[1]] == mark && cells[line[2]] == mark {
      return true
    }
  }
  return false
}

mount(component: "tic-tac-toe")

"You are X. Click a square in the Preview and I'll answer as O."

let cells = ["", "", "", "", "", "", "", "", ""]
let over = false
let status = "Your move"
emitEvent(eventType: "ttt.state", data: { cells: cells, status: status, over: over })

while !over {
  let played = awaitEvent(eventType: "ttt.move")
  let i = cellIndex(raw: played.content[0].data?.cell ?? -1)
  status = "Your move"
  if i < 0 {
    status = "That is not a square. Your move"
  } else if cells[i] != "" {
    status = "That square is taken. Your move"
  } else {
    cells[i] = "X"
    if hasLine(cells: cells, mark: "X") {
      status = "You win!"
      over = true
    } else {
      let o = firstOpen(cells: cells)
      if o < 0 {
        status = "Draw"
        over = true
      } else {
        cells[o] = "O"
        // Bind calls first: an else-if condition cannot contain a call.
        let oWins = hasLine(cells: cells, mark: "O")
        let left = firstOpen(cells: cells)
        if oWins {
          status = "I win!"
          over = true
        } else if left < 0 {
          status = "Draw"
          over = true
        }
      }
    }
  }
  emitEvent(eventType: "ttt.state", data: { cells: cells, status: status, over: over })
}

"Game over: {{status}}"