tic-tac-toe-magnus-rb ๐ฆ๐
A Tic Tac Toe game engine with a Rust core and a clean Ruby API, packaged with Nix.
Exists mostly to practice Ruby + Rust gem packaging with Nix.
X โ ยท โ O
โโโโผโโโโผโโโ
ยท โ X โ ยท
โโโโผโโโโผโโโ
ยท โ ยท โ X
Table of Contents
- What's inside
- Getting started
- Install the gem
- With Nix (recommended)
- Without Nix
- Try it in the REPL
- Releasing a new version
- Nix architecture
- Vendoring Cargo dependencies
- Gem pinning with bundix
- Ruby API
- Project layout
- How the Rust extension works
- Test suite
- License
What's inside
| Layer | Technology | Purpose |
|---|---|---|
| Game logic | Rust | Board state, move validation, win/draw detection, minimax AI |
| Ruby bindings | Magnus 0.7 | Zero-cost safe Rust โ Ruby bridge |
| Gem extension | rb_sys + rake-compiler | Drives cargo build from extconf.rb
|
| Dev shell | Nix (classical) | Reproducible Ruby + Rust toolchain, no version drift |
| Gem deps | bundlerEnv + bundix | SHA-256-pinned Ruby gems inside the Nix sandbox |
Getting started
Install the gem
gem install tic_tac_toe_magnusWith Nix (recommended)
git clone https://github.com/kisp/tic-tac-toe-magnus-rb
cd tic-tac-toe-magnus-rb
nix-shell # enter the reproducible dev shell
bundle install # install Ruby gems
bundix # pin gems โ gemset.nix (first time only)
bundle exec rake compile # build the Rust extension
bundle exec rake test # run the test suite (98 tests)With direnv + nix-direnv:
direnv allow # shell activates automatically on every cd into the projectWithout Nix
You need Ruby โฅ 3.1 and Rust stable on your PATH:
bundle install
bundle exec rake compile
bundle exec rake testTry it in the REPL
After compiling the extension, start an IRB session with the gem pre-loaded:
bundle exec rake irbirb(main):001> TicTacToe::Game.new
=>
ยท โ ยท โ ยท
โโโโผโโโโผโโโ
ยท โ ยท โ ยท
โโโโผโโโโผโโโ
ยท โ ยท โ ยท
Current player : X
State : playing
Valid moves : 0, 1, 2, 3, 4, 5, 6, 7, 8
irb(main):002> g = TicTacToe::Game.new
=>
ยท โ ยท โ ยท
...
irb(main):003> g.current_player
=> "x"
irb(main):004> g.valid_moves
=> [0, 1, 2, 3, 4, 5, 6, 7, 8]
irb(main):005> g.make_move 3
=> nil
irb(main):006> g
=>
ยท โ ยท โ ยท
โโโโผโโโโผโโโ
X โ ยท โ ยท
โโโโผโโโโผโโโ
ยท โ ยท โ ยท
Current player : O
State : playing
Valid moves : 0, 1, 2, 4, 5, 6, 7, 8
irb(main):007> exit
Releasing a new version
A single Rake task bumps the version, commits, tags, builds the gem, and
prints the gem push command so you can review before publishing:
bundle exec rake release # patch bump (e.g. 0.1.0 โ 0.1.1)
bundle exec rake release[minor] # minor bump (e.g. 0.1.0 โ 0.2.0)
bundle exec rake release[major] # major bump (e.g. 0.1.0 โ 1.0.0)The task will:
- Increment the version in
lib/tictactoe/version.rb. - Create a git commit (
Bump version to X.Y.Z) and an annotated tag (vX.Y.Z). - Build the gem (
tic_tac_toe_magnus-X.Y.Z.gem). - Print the exact
gem pushcommand to publish it.
Nix architecture
shell.nix / default.nix
โ
โโโ shell.nix # `nix-shell`
โ โโโ ruby_3_3 # from nixpkgs
โ โโโ rustc / cargo # stable Rust toolchain from nixpkgs
โ โโโ bundlerEnv # all Gemfile gems, SHA-256 locked
โ โโโ bundix # gems โ gemset.nix helper
โ โโโ shellHook # sets RUBY_ROOT, RUBYLIB, greets you
โ
โโโ default.nix # `nix-build`
โโโ mkDerivation # compiles the Rust ext in the sandbox
# (requires vendored Cargo deps โ see below)
Vendoring Cargo dependencies
The Nix sandbox has no network access, so Cargo crates must be vendored
before nix-build can compile the extension. Three approaches are documented
in nix/vendor-cargo-deps.nix:
| Approach | How |
|---|---|
importCargoLock |
Commit Cargo.lock; Nix fetches at eval time (easiest) |
fetchCargoTarball |
Explicit SHA-256 hash in default.nix
|
| Committed vendor dir |
cargo vendor + [source.vendored-sources] (fully offline) |
Gem pinning with bundix
Gemfile โ bundle install โ Gemfile.lock โ bundix โ gemset.nix
Both Gemfile.lock and gemset.nix are committed โ they are the Nix
equivalents of a lockfile. See nix/bundix-workflow.md
for the full update procedure.
Ruby API
require "tic_tac_toe_magnus"
g = TicTacToe::Game.new
puts TicTacToe::Game.position_legend
# =>
# 0 โ 1 โ 2
# โโโโผโโโโผโโโ
# 3 โ 4 โ 5
# โโโโผโโโโผโโโ
# 6 โ 7 โ 8
g.valid_moves # => [0,1,2,3,4,5,6,7,8]
g.make_move(4) # X takes centre
g.current_player # => "o"
g.state # => "playing" | "x_wins" | "o_wins" | "draw"
g.best_move # => Integer (minimax) or nil if game over
puts g # ASCII board
g.winner # => :x | :o | nil
g.over? # => true/false
g.reset # reuse the same Ruby objectmake_move raises ArgumentError on out-of-range positions, occupied cells,
or moves after the game is over.
Project layout
tic-tac-toe-magnus-rb/
โโโ shell.nix # Nix dev shell (nix-shell)
โโโ default.nix # Nix package derivation (nix-build)
โโโ .envrc # direnv โ nix-shell
โโโ .gitignore
โโโ nix/
โ โโโ vendor-cargo-deps.nix # how to vendor Cargo crates for nix-build
โ โโโ bundix-workflow.md # gem pinning cookbook
โ
โโโ tic_tac_toe_magnus.gemspec
โโโ Gemfile / Gemfile.lock # committed โ Nix lockfile for Ruby deps
โโโ gemset.nix # generated by bundix, also committed
โโโ Rakefile # compile + test tasks
โ
โโโ ext/tictactoe/
โ โโโ extconf.rb # create_rust_makefile(...)
โ โโโ Cargo.toml # cdylib + magnus 0.7
โ โโโ src/lib.rs # ALL game logic + Magnus bindings
โ
โโโ lib/
โ โโโ tictactoe.rb # loads .so, adds Ruby sugar
โ โโโ tictactoe/version.rb
โ
โโโ test/
โ โโโ test_helper.rb
โ โโโ test_initial_state.rb
โ โโโ test_make_move.rb
โ โโโ test_win_detection.rb # all 8 winning lines ร 2 players = 16 tests
โ โโโ test_draw_detection.rb
โ โโโ test_minimax_ai.rb
โ โโโ test_rendering.rb
โ โโโ test_reset.rb
โ โโโ test_integration.rb
โ
โโโ bin/console # IRB session with gem pre-loaded
โโโ examples/demo.rb # full feature showcase
How the Rust extension works
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Ruby caller โ
โ TicTacToe::Game.new / make_move โฆ โ
โโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Magnus โ safe Rust โ Ruby, no GC pressure
โโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Game (magnus::wrap โ Mutex<Inner>) โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ GameInner (pure Rust) โ โ
โ โ โข board: [Cell; 9] โ โ
โ โ โข current_player: Player โ โ
โ โ โข valid_moves() โ โ
โ โ โข make_move(pos) โ โ
โ โ โข state() / check_winner() โ โ
โ โ โข minimax() / best_move() โ โ
โ โ โข to_ascii() โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Mutex<GameInner> wrapper makes the Ruby object safe to share across
Ractor/thread boundaries.
Test suite
bundle exec rake test98 tests across 8 files โ initial state, move mechanics, all 8 winning lines for both players, draw detection, AI correctness (winning moves, blocking moves, perfect-play draw), rendering, reset, and integration scenarios including thread safety.
License
MIT