Chomp 2026-10-25
An alternating grid-removal game in which choosing an occupied cell removes it and every occupied cell above and to its right. The player who removes the designated lower-left poison cell loses. Reachable occupied-cell sets are order ideals for coordinatewise order. A nonempty reachable position always contains the poison cell, and removing that cell empties the board. The monotone boundary representation of a grid order ideal supports compact board updates.
For an order ideal in a finite rectangular grid with coordinatewise partial order, occupied cells in each row form a left prefix. Their lengths form a nonincreasing sequence . Conversely, any nonincreasing sequence of valid row lengths specifies exactly one such order ideal, giving an abstraction function with no redundant per-cell information.
Removing all occupied cells with and replaces by for and leaves earlier rows unchanged. The new lengths remain nonincreasing: affected rows preserve their mutual order, and the first affected row only decreases. For a legal removal, the boundary immediately below it was already at least . A cached sum of row lengths can be updated by subtracting the decreases. This yields constant-time membership and count queries, linear storage, and an update linear in the number of affected rows.
Let the abstract state be the set of occupied coordinate pairs. For the Boolean array, the abstraction function is . Its representation invariant requires an Boolean array and the downward-closure property
Thus is an order ideal for the coordinatewise partial order. Removing an upper-right rectangle preserves this property. The empty set is allowed after the losing move.
For the row-boundary array, the abstraction function and representation invariant are
The array has length . Removing at replaces each row(j) for by . Earlier rows are unchanged. This both implements the abstract removal and preserves the nonincreasing boundary.
Without a cached count, the Boolean representation takes for Count, for Legal, worst-case for Move, and for a full-grid Display. The boundary representation takes for Count, for Legal, worst-case for Move, and for the same full-grid Display. Their space complexities are respectively and . A scalar cached total makes Count in either representation if each move updates it consistently. The implementation below uses that cache with the boundary array, so it achieves constant-time Count and Legal, linear worst-case Move, and linear storage. Full-grid output still takes quadratic time because it emits quadratic many cell positions.
Keep the same player until an occupied, in-range cell is supplied:
val board = new Chomps(6)
var player = 1
while (board.Count() > 0) {
  board.Display()
  var x = -1
  var y = -1
  while (!board.Legal(x, y)) {
    println("Player " + player + ": enter x, then y")
    try {
      x = scala.io.StdIn.readInt()
      y = scala.io.StdIn.readInt()
    } catch {
      case _: NumberFormatException =>
        x = -1
        y = -1
    }
    if (!board.Legal(x, y)) println("Choose an occupied cell")
  }
  board.Move(x, y)
  if (board.Count() == 0)
    println("Player " + player + " loses; player " + (3 - player) + " wins")
  else player = 3 - player
}
The outer loop invariant is that player is the next player's number, either one or two, and the board is the result of the accepted moves. The inner loop invariant says that the board and player have not changed; rejected inputs do not consume a turn. Assuming the input source continues and eventually supplies a legal coordinate pair, the inner loop finishes. No program can guarantee that a user stops entering illegal moves, so that interaction assumption is necessary.
Every accepted move removes at least its selected token, so the outer loop variant is Count(), a decreasing nonnegative integer. In a nonempty order ideal, is occupied. Removing it removes the entire board, whereas a move at any other cell leaves it. Therefore the player making the final move is exactly the loser; the code reports that player before changing the turn. At most legal moves occur.
For the flattened Boolean array, the abstraction function selects the true cells. A suitable representation invariant requires the occupied cells to form a order ideal: an occupied implies occupancy of every with . Deleting a northeast rectangle preserves that property.
For column heights, the invariant is , and the abstraction function is . A legal move replaces every for by , preserving the invariant. This is an order-ideal board representation.
For a fair comparison, allow either representation a scalar cached token count: Count and Legal then take in both. Without that cache, counting needs Boolean inspections versus height additions. A move takes in the worst case in the flattened representation and with heights. Displaying the full square takes character positions in either. Heights use array entries rather than , so they are superior for the requested operations; their initialization also takes only time.
A flattened Boolean array stores the whole board in entries, with cell at position y*N + x. For the pictured board, its bottom-to-top rows are 111111 | 111100 | 111100 | 110000 | 000000 | 000000.
A column-height array uses only entries: column contains exactly the cells below height . The pictured board is , with 16 occupied cells. The alternatives are essentially different because the first stores every cell and the second encodes the boundary of an order ideal.
Use column heights and update the cached count by the number of removed cells:
class Chomps(N: Int) {
  require(N > 0 && N.toLong * N.toLong <= Int.MaxValue)
  private val height = Array.fill(N)(N)
  private var remaining = (N.toLong * N.toLong).toInt

  def Count(): Int = remaining

  def Legal(x: Int, y: Int): Boolean =
    0 <= x && x < N && 0 <= y && y < height(x)

  def Move(x: Int, y: Int): Unit = {
    require(Legal(x, y), "illegal move")
    var i = x
    while (i < N && height(i) > y) {
      remaining -= height(i) - y
      height(i) = y
      i += 1
    }
  }

  def Display(): Unit = {
    require(N < 10, "single-digit display labels required")
    var y = N - 1
    while (y >= 0) {
      print(y + "  ")
      var x = 0
      while (x < N) {
        print(if (y < height(x)) "* " else "  ")
        x += 1
      }
      println()
      y -= 1
    }
    print("   ")
    var x = 0
    while (x < N) { print(x + " "); x += 1 }
    println()
  }
}
The strengthened representation invariant includes . The move loop's processed columns have their final truncated heights, unprocessed columns retain their old heights, and each subtraction removes exactly the corresponding height reduction. Because heights are nonincreasing, once a column is no higher than y, neither is any later column; stopping there is valid. A legal move removes at least one token and leaves a order ideal.
The constructor computes in Long and checks representability before converting, avoiding integer overflow in the cached Int count. Legal rejects out-of-range coordinates before indexing. The methods meet the bounds in part (a); Display additionally checks its stated single-digit-label assumption.
Retry invalid inputs without switching players; after a legal move, check whether the board has become empty:
val game = new Chomps(6)
var player = 1
while (game.Count() > 0) {
  game.Display()
  var accepted = false
  var x = 0
  var y = 0
  while (!accepted) {
    try {
      println("Player " + player + ": enter x, then y")
      x = scala.io.StdIn.readInt()
      y = scala.io.StdIn.readInt()
      accepted = game.Legal(x, y)
      if (!accepted) println("That cell is not a legal move; try again.")
    } catch {
      case _: NumberFormatException => println("Please enter integer coordinates.")
    }
  }
  game.Move(x, y)
  if (game.Count() == 0)
    println("Player " + player + " loses; player " + (3 - player) + " wins.")
  else player = 3 - player
}
Only a move at can remove the poisoned bottom-left token, and that move removes the entire remaining order ideal, so the empty-board test identifies the losing player correctly. Any other legal move preserves that token. Every accepted move strictly decreases the finite token count, giving at most 36 accepted moves. Invalid attempts do not change the board or the turn. As usual for this interactive fragment, the user is assumed eventually to supply another readable input rather than permanently ending the input stream or retrying forever.