Stn.rb
STN (State Transition Notation) for Ruby — a small, pure, functional core to describe position deltas (board, hands/reserve, and active player toggle) in a rule-agnostic way.
- Functional & Immutable: no side effects, no in-place mutation.
- Object-oriented surface: simple module + value object (
Transition
). - Spec-accurate: strictly follows the STN specification.
- Minimalist: no JSON (de)serialization inside the gem.
What is STN?
STN encodes the net difference between two positions in abstract strategy games:
board
: map of CELL →QPI or nil
(final state per cell)hands
: map of QPI →Integer delta
(non-zero)toggle
:true
when the active player switches, elsefalse
STN is rule-agnostic: it does not prescribe legal moves or game rules; it only describes what changes.
This gem builds upon:
JSON (de)serialization is intentionally out of scope: keep it at your app boundary.
Installation
Add to your Gemfile
:
gem "sashite-stn"
Then:
bundle install
This gem depends on:
gem "sashite-cell"
gem "sashite-qpi"
Bundler will install them automatically when you use sashite-stn
.
STN format at a glance
{
"board" => { "e2" => nil, "e4" => "C:P" }, # e2 empties, e4 now has white pawn
"hands" => { "c:p" => 1 }, # add one black pawn to reserve
"toggle" => true # switch active player
}
- All top-level keys are optional.
- Empty object
{}
means “no changes”.
Quick start
require "sashite/stn"
# Validate a payload (Hash) or an instance (Transition)
Sashite::Stn.valid?({ "board" => { "e2" => nil, "e4" => "C:P" }, "toggle" => true })
# => true
# Parse into an immutable Transition (raises on invalid)
tr = Sashite::Stn.parse({ "board" => { "e2" => nil, "e4" => "C:P" }, "toggle" => true })
tr.toggle? # => true
tr.board_changes # => { "e2" => nil, "e4" => "C:P" }
# Construct directly (keywords)
castle = Sashite::Stn.transition(
board: { "e1" => nil, "g1" => "C:K", "h1" => nil, "f1" => "C:R" },
toggle: true
)
# Compose transitions (left → right)
op_reply = Sashite::Stn.transition(board: { "e7" => nil, "e5" => "c:p" }, toggle: true)
combined = Sashite::Stn.combine(castle, op_reply)
combined.toggle? # => false (true XOR true)
# Canonical helpers
Sashite::Stn.empty.to_h # => {}
Sashite::Stn.pass.to_h # => { :toggle=>true }
API
Module: Sashite::Stn
Sashite::Stn.valid?(data) → Boolean
Validate a payload (Hash
) or aTransition
.Sashite::Stn.parse(data) → Transition
Parse a payload (Hash
) or return the sameTransition
. RaisesSashite::Stn::Error::Validation
on invalid input.Sashite::Stn.transition(board: {}, hands: {}, toggle: false) → Transition
Build a transition from keyword args. Keys are normalized to strings.Sashite::Stn.empty → Transition
Canonical empty transition (no board/hands changes, no toggle).Sashite::Stn.pass → Transition
Canonical pass transition (toggle only).Sashite::Stn.combine(*transitions) → Transition
(alias:compose
) Compose left-to-right using STN semantics:- board: last write wins per cell
- hands: sum deltas; drop zero results
- toggle: XOR across the sequence
Class: Sashite::Stn::Transition
Construction & parsing
Transition.new(board: {}, hands: {}, toggle: false)
Validates and freezes the instance.Transition.parse(hash) → Transition
Parses a top-level Hash with"board"
,"hands"
,"toggle"
.Transition.valid?(hash) → Boolean
True/false wrapper overparse
.
Accessors & queries
#board_changes → Hash{String=>String|nil}
#hand_changes → Hash{String=>Integer}
#toggle? → Boolean
#empty? → Boolean
#pass_move? → Boolean
#board_change(cell) → String|nil
#hand_change(qpi) → Integer|nil
#has_board_change?(cell) → Boolean
#has_hand_change?(qpi) → Boolean
Transformations (return new instances)
#with_board_change(cell, value) → Transition
#with_hand_change(qpi, delta) → Transition
#with_toggle(bool) → Transition
#without_board_change(cell) → Transition
#without_hand_change(qpi) → Transition
Composition & inversion
#combine(other) → Transition
STN composition semantics (board last-write, summed hands, XOR toggle).#invert → Transition
Invert hands and keep toggle as is (board left untouched).#invert_board_against(previous_board:) → Transition
Build a board inverse using the provided previous snapshot. Also inverts hands and keeps toggle.
Conversion & equality
#to_h → Hash
— omits empty fields; top-level keys are symbols#==
,#eql?
,#hash
— structural equality
Error handling
All exceptions are scoped under Sashite::Stn::Error
:
Sashite::Stn::Error
(base class)Sashite::Stn::Error::Validation
— structural/semantic validation failuresSashite::Stn::Error::Coordinate
— invalid CELL keys inboard
Sashite::Stn::Error::Piece
— invalid QPI values/keys inboard
/hands
Sashite::Stn::Error::Delta
— invalid hands deltas (must be non-zero integers)
begin
tr = Sashite::Stn.parse({ "board" => { "a0" => "C:P" } })
rescue Sashite::Stn::Error::Coordinate => e
warn "Invalid CELL: #{e.}"
rescue Sashite::Stn::Error::Piece => e
warn "Invalid QPI: #{e.}"
rescue Sashite::Stn::Error::Delta => e
warn "Invalid delta: #{e.}"
rescue Sashite::Stn::Error::Validation => e
warn "STN validation failed: #{e.}"
end
Design properties
- Rule-agnostic: independent from game rules and engines
- Pure & Immutable: no mutation of inputs; instances are frozen
- Composable: transitions merge cleanly and predictably
- Minimal surface: no JSON (de)serialization built-in
- CELL/QPI-strict: delegates coordinate/piece validation to their specs
Development
# Clone
git clone https://github.com/sashite/stn.rb.git
cd stn.rb
# Install
bundle install
# Run smoke tests
ruby test.rb
# Generate YARD docs
yard doc
Contributing
- Fork the repository
- Create a feature branch:
git checkout -b feat/my-change
- Add tests covering your changes
- Ensure everything is green (lint, tests, docs)
- Commit with a conventional message
- Push and open a Pull Request
License
Open source under the MIT License.
About
Maintained by Sashité — promoting chess variants and sharing the beauty of board-game cultures.