qi.ex

Hex VersionHex DocsCILicense

A minimal, format-agnostic position model for two-player board games.

Overview

Qi provides an immutable Qi.Position struct that represents the state of a two-player, turn-based board game as defined by the Sashité Game Protocol.

A position encodes exactly four things:

Field Type Description
board nested list (1D to 3D) Board structure and occupancy
hands%{first: list, second: list} Off-board pieces held by each player
styles%{first: term, second: term} Player style for each side
turn:first or :second The active player's side

Piece and style representations are intentionally opaqueQi validates structure, not semantics. This makes the library reusable across FEEN, PON, or any other encoding that shares the same positional model.

Implementation Constraints

Constraint Value Rationale
Max dimensions 3 Covers 1D, 2D, 3D boards
Max dimension size 255 Fits in 8-bit integer; covers 255×255×255
Board non-empty n ≥ 1 A board must contain at least one square
Piece cardinality p ≤ n Pieces cannot exceed the number of squares

Installation

# In your mix.exs
def deps do
  [
    {:qi, "~> 1.0"}
  ]
end

Dependencies

None. Qi is a zero-dependency library.

Usage

Creating a Position

# Chess starting position
board = [
  [:r, :n, :b, :q, :k, :b, :n, :r],
  [:p, :p, :p, :p, :p, :p, :p, :p],
  [nil, nil, nil, nil, nil, nil, nil, nil],
  [nil, nil, nil, nil, nil, nil, nil, nil],
  [nil, nil, nil, nil, nil, nil, nil, nil],
  [nil, nil, nil, nil, nil, nil, nil, nil],
  [:P, :P, :P, :P, :P, :P, :P, :P],
  [:R, :N, :B, :Q, :K, :B, :N, :R]
]

{:ok, position} = Qi.new(
  board,
  %{first: [], second: []},
  %{first: "C", second: "c"},
  :first
)

Accessing Fields

position.board   #=> [[:r, :n, :b, ...], ...]
position.hands   #=> %{first: [], second: []}
position.styles  #=> %{first: "C", second: "c"}
position.turn    #=> :first

Bang Variant

# Raises ArgumentError on invalid input
position = Qi.new!(board, hands, styles, :first)

Tagged Tuple Variant

# Returns {:ok, position} or {:error, %ArgumentError{}}
case Qi.new(board, hands, styles, :first) do
  {:ok, position} -> position
  {:error, error} -> handle_error(error)
end

Pieces as Arbitrary Terms

Pieces are not restricted to any specific format. You can use atoms, strings (EPIN tokens), tuples, or any non-nil Elixir term:

# Atoms
Qi.new!([:k, :p, nil, :P, :K], %{first: [], second: []}, %{first: "C", second: "c"}, :first)

# EPIN strings
Qi.new!([["K^", nil], [nil, "k^"]], %{first: [], second: []}, %{first: "C", second: "c"}, :first)

# Tuples
Qi.new!(
  [[{:king, :first, true}, nil], [nil, {:king, :second, true}]],
  %{first: [], second: []},
  %{first: :chess, second: :chess},
  :first
)

Multi-dimensional Boards

# 1D board
Qi.new!([:a, nil, :b], %{first: [], second: []}, %{first: "G", second: "g"}, :first)

# 2D board (standard)
Qi.new!([[nil, nil], [nil, nil]], %{first: [], second: []}, %{first: "C", second: "c"}, :first)

# 3D board (2 layers × 2 ranks × 2 files)
board_3d = [
  [[:a, :b], [:c, :d]],
  [[:A, :B], [:C, :D]]
]
Qi.new!(board_3d, %{first: [], second: []}, %{first: "R", second: "r"}, :first)

Hands with Captured Pieces

# Shogi-like position with pieces in hand
Qi.new!(
  [[nil, nil, nil], [nil, "K^", nil], [nil, nil, nil]],
  %{first: ["P", "P", "B"], second: ["p"]},
  %{first: "S", second: "s"},
  :first
)

Validation Errors

Error message Cause
"board must be a list" Board is not a list
"board must not be empty" Board is []
"board exceeds 3 dimensions (got N)" More than 3 nesting levels
"dimension size N exceeds maximum of 255" A dimension has more than 255 elements
"non-rectangular board: ..." Sub-arrays at the same level differ in length
"inconsistent board structure: mixed lists and non-lists at same level" Mixed lists and non-lists at the same nesting level
"inconsistent board structure: expected flat squares at this level" A list found where a leaf square was expected
"hands must be a map with keys :first and :second" Hands is not a map
"hands must have exactly keys :first and :second" Map has missing or extra keys
"each hand must be a list" Hand value is not a list
"hand pieces must not be nil"nil found in a hand list
"styles must be a map with keys :first and :second" Styles is not a map
"styles must have exactly keys :first and :second" Map has missing or extra keys
"first player style must not be nil" First style value is nil
"second player style must not be nil" Second style value is nil
"turn must be :first or :second" Invalid turn value
"too many pieces for board size (P pieces, N squares)" Piece cardinality violation

Design Principles

Related Specifications

License

Available as open source under the Apache License 2.0.