NAME
Game::Oware - the African sow and capture game, Abapa rules
VERSION
Version 0.02
SYNOPSIS
use Game::Oware;
my $game = Game::Oware->new(seed => $bytes);
while ($game->status eq 'active') {
my ($seat) = $game->waiting_on;
my $house = $game->legal($seat)->[0];
$game->play($seat, $house);
}
print $game->result->stringify;
DESCRIPTION
Twelve houses, two stores and forty-eight seeds. Two seats, no hidden information, and no randomness at all.
It is the engine behind the oware at https://peer2peergames.com.
Refusals are returned, never thrown
play gives back a Game::Oware::Move or a Game::Oware::Error, and the error is an object with a flag on it. Nothing a player can do causes this module to die.
die is for programmer error: a house index outside 0 to 11, an unknown variant, a seat that does not exist, a log that does not replay. A consumer validates its own input at its own boundary and this engine assumes it did.
The move log is the canonical serialisation, not the position
Two identical boards can differ in how many plies have passed since a capture and in how many times the position has already occurred, and under the cycle rule both decide the result. So a board cannot resume a game, and that is a fact about these rules rather than a preference about event sourcing.
A sow event carries the house and what the move did with it. On replay the house is applied and everything else is recomputed and compared, so a log claiming a capture the rules would not have made is refused rather than believed.
The cycle rule is a HOUSE RULE and is not the rule of Oware
The published rule is:
"If both players agree that the game has been reduced to an endless cycle,
the game ends when each player has seeds in their holes and then each
player captures the seeds on their side of the board."
"If both players agree" has no mechanism where one seat is a program and there is a deadline. So the ending is kept and the trigger is replaced, and anything presenting this game to a player has to say so in as many words rather than implying a book says it.
Two triggers, either of which fires, both ending in "sweep_split" in Game::Oware::Scoring:
the same position, with the same seat to move, for the third time;
plies_without_captureplies with no seed entering either store.
Why both triggers, and why the table can be cleared
Seeds go into a store and never come out, so a capture changes the stores monotonically and a position can only ever repeat inside a capture-free stretch. Three things follow, and the second is the one that makes this cheap:
repetition is strictly contained in the ply counter's window, so it is the fast path and the counter is the outer bound;
the repetition table can be cleared on every capture, so it never grows past the cap and costs nothing to keep;
a shuffle that never repeats a position is exactly what repetition cannot see, which is what the counter is for.
Without that argument written down the second trigger reads as belt and braces and somebody deletes it.
There are four natural endings and two result tokens
A store reaching twenty-five, both stores reaching twenty-four, the seat on turn being unable to feed a starved opponent, and the cycle rule. The first and third and fourth are all score or draw; which one happened is "reason" in Game::Oware::Result.
The order they are checked in after a move is fixed and matters: target, then the level draw, then the ply counter, then the turn advances, then repetition, then the failed feed. The sweeps run before the result is decided, because a sweep can carry a store past twenty-five and that is a win rather than whatever the score was a moment earlier.
The seed is stored, published at the end, and never read
Oware has no randomness in it: no deal, no dice, no shuffle. The seed is held because a consumer that hands every game some bytes and publishes them when it finishes, so that a completed game can be checked, would otherwise have one game for which that page is empty.
Do not remove it on the grounds that nothing reads it. seed returns undef until the game is over, and the value lives in a private property so that gate is the only way to it.
PROPERTIES
variant
abapa by default. See Game::Oware::Variant.
board
The fourteen cells. See Game::Oware::Board.
turn
The seat to move, p1 or p2. p1 opens.
status
active or finished.
winner
p1, p2, or undef.
result
A Game::Oware::Result once finished.
log
The events, oldest first.
no_capture
How many plies have passed with no seed entering a store. Part of the position for the cycle rule, which is why it is here and not on the board.
seen
How many times each position has occurred, cleared by every capture.
METHODS
seats
p1 and p2, in order.
events
A copy of the log.
other
The other seat.
captured
The running totals.
score
The official result, or undef while the game is active.
scores
{ p1 => { current => N }, p2 => { current => N } }, which is the shape a scoreboard wants.
places
The finishing order, or undef while the game is running. It is not a live standing: a player with forty seeds in their row has captured nothing.
seed
The seed, once the game has finished.
legal
An arrayref of the houses a seat may play. Empty off turn, and empty once the game is over.
waiting_on
The seat on turn, or nothing.
play
my $out = $game->play('p1', 4);
A Game::Oware::Move, or a Game::Oware::Error.
timeout
Finishes the game with the other seat as winner. The clock is the caller's.
abandon
Finishes the game with no winner.
resign
Finishes the game with the other seat as winner.
clone
A deep enough copy to play on independently.
to_text
The game as a transcript of house letters.
from_text
Game::Oware->from_text('EcAb', seed => $bytes);
A game played out from a transcript. Croaks if the transcript does not play.
replay
Rebuilds this game from a log. Only the player events are applied; every sys event is regenerated from the position, and the resulting log must then match what was handed in.
So a log carrying a capture the rules would not have made, a sweep at a position where a feeding move existed, or a cycle ending before the counter reached the cut, is refused. Those are the cheapest possible cheats in this game, because each of them awards seeds.
SEE ALSO
Game::Oware::Board, Game::Oware::Rules, Game::Oware::Variant, Game::Oware::Result, Game::Oware::Error
AUTHOR
LNATION <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION <email@lnation.org>.
This is free software, licensed under the Artistic License 2.0.