NAME
Game::Durak - Podkidnoy Durak, the Russian beating game
VERSION
Version 0.01
SYNOPSIS
use Game::Durak;
my $game = Game::Durak->build(seed => $thirty_two_bytes);
$game->trump; # 'H'
$game->turn; # 1 or 2
$game->phase; # 'attack', 'defend', 'pile_on' or 'over'
$game->legal($game->turn); # [ { kind => 'attack', card => 14 }, ... ]
my @events = $game->apply(1, { kind => 'attack', card => 14 });
if (ref $events[0] eq 'Game::Durak::Error') { ... }
DESCRIPTION
Two seats, a thirty-six card pack and a trump suit, played in bouts. One seat attacks with a card, the other beats it or picks it up, and the attacker may keep throwing in cards whose rank is already on the table. A bout that is beaten off goes to the discard and the defender attacks next; a bout that is taken goes into the defender's hand and the attacker attacks again.
Between bouts each seat draws back up to six from the talon, the attacker first, and the holder of the trump six may exchange it for the turned up trump while that card is still in the talon.
The game has no winner, only a loser: the seat left holding cards when the talon is empty and the other has run out is the durak, the fool. If the last bout empties both hands the deal is drawn.
It is the engine behind the Durak at https://peer2peergames.com.
The deal ends at a bout end, after the refill, and nowhere else
A seat is out when it has no cards and there is no talon to draw from, and both of those are only true at the moment a bout closes and the drawing is done. So a defender that has just picked a bout up is never out, it is holding the bout; an attacker that plays its last card is not out until the defender has answered; and a defender that beats the last attack with its last card is out on that card.
Game::Durak::Result is the whole of it, over two card counts and the size of the talon.
A seat is 1 or 2
Not p1 and p2, which is what the sibling engines in this family use, because a seat here indexes the deal that Game::Durak::Deck produces and an engine that converts between two spellings of the same thing grows a seam to get wrong. A consumer with its own names for the seats maps them once, at its own edge.
The stages of a bout, and who is on turn
phase is derived and never stored, because a stored stage is a second copy of the truth:
an attack card with no answer, in a bout nobody has taken, is
defenda bout the defender has taken is
pile_onanything else is
attack
turn follows from it: the defender defends, and the attacker does everything else. Exactly one seat is on turn at every position.
A position with no choice in it is resolved by the engine
An attacker with nothing legal to throw is not deciding anything by saying they are done, and a defender who cannot beat the card in front of them is not deciding anything by picking it up. Both are resolved inside apply, so neither costs a move, an event or a turn. The consumer sees the bout end, and the how of bout_end says which of the rules' reasons ended it.
The refill, and who draws first
After a bout is complete, all players who have fewer than six cards must
if possible replenish their hands to six by drawing sufficient cards from
the top of the talon. The attacker replenishes first ... and finally the
defender.
The order is not decoration. When the talon holds one card it goes to the seat that attacked, and the other ends the deal a card short, so the seat that beats off a bout draws first for the rest of the deal.
The seat that attacked in the bout that just ended draws first, which is not always the seat that attacks next: a defender who beats off an attack becomes the next attacker but still draws second.
refill is emitted after every bout, zeros included, because a consumer that replays a game compares event streams and a stream that omits its no-ops is a different stream. It carries counts and not cards: which cards were drawn is a function of the seed and the draws already made, so a payload that never held them cannot leak them.
The exchange, and the one place this engine departs from the page
If you are dealt the lowest trump (the six) or if you draw it from the
talon, you are allowed to exchange it for the face up trump, placing your
six of trumps under the talon and adding the turned up trump to your
hand, at any time before the talon is exhausted.
At any time is the departure. Taken literally it is a move by a seat that is not on turn, and the source goes further still, allowing the holder to demand the exchange after another seat has already drawn the turn-up. This engine offers swap only to the seat on turn, and only while the turn-up is still in the talon.
The cost is one position: the seat that draws the six in the same refill that hands the other seat the turn-up never gets its chance. The gain is that every position in the game waits on exactly one seat, which is what makes the engine usable by a consumer that has one deadline per turn.
The exchange stays a real move rather than being applied automatically. The trump six is a cheap attack card that forces the defender to spend a trump on it, so a seat with a strong trump holding may rationally keep it, and "declining is never right" is not true here.
The trump six has a pedigree
The six of trumps can only be exchanged by its original holder; if you
acquire it from another player (as one of the cards you pick up when
attacked) you cannot exchange it.
six_owner is the seat that was dealt the six or drew it from the talon. It is not cleared when the six is played: a seat that attacks with it, is beaten, and picks the whole bout back up is holding the card it was dealt, and may still exchange it. It is cleared for good the moment the other seat comes to hold it, and after the exchange itself.
can_swap then asks three questions: is this seat the owner, does it still hold the six, and is the turn-up still in the talon.
If the turn-up is the six of trumps, which is one deal in nine, nobody was dealt it and nobody can draw it before the talon is empty, so six_owner is undef for the whole deal and the exchange never arises. An engine that offered it would exchange the card for itself.
Errors are returned, not thrown
apply returns either a list of events or a single Game::Durak::Error. A state that cannot be reached dies instead: the defending stage with nothing to beat, a bout opened against an empty hand, a card that is not a card.
EVENTS
apply returns the events its move caused, in order, and appends them to history. A player's move is one event with a seat; everything the table did in consequence has none.
{ kind => 'attack', seat => 1, card => 14 }
{ kind => 'beat', seat => 2, card => 17 }
{ kind => 'take', seat => 2 }
{ kind => 'done', seat => 1 }
{ kind => 'swap', seat => 1 }
{ kind => 'resign', seat => 1 }
{ kind => 'bout_end', taken => 0, how => 'done',
cards => 4, discard => 4, next_attacker => 2 }
{ kind => 'refill', drawn => { 1 => 2, 2 => 0 }, talon => 18 }
{ kind => 'out', seat => 1 }
{ kind => 'game_end', outcome => 'fool', fool => 2,
places => { 1 => 1, 2 => 2 } }
swap carries no card. Both of them are public: everyone can see the turn-up, and the only card that may be exchanged for it is the trump six.
how is one of:
done, the attacker said soexhausted, the attacker had nothing legal left to throwcapped, the attack reached six cards or the defender's handspent, the defender beat everything and has nothing lefttaken, the defender picked the bout up
There is no deal event. The deal is a pure function of the seed and the deal number, so a consumer that stores one stores it as the seed it already has.
METHODS
build
Game::Durak->build(seed => $seed, number => 1, seats => 2);
A dealt game, or an error: no_seed unless the seed is exactly 32 bytes, bad_deal for a deal number that does not count from one, bad_seats for anything but two. The first bout is open when it returns.
seed, number, trump, trump_card
What the deal was made from and what it turned up. The trump card is face up and is still the last card of the talon. It is the one card of the deal that can change: an exchange puts the trump six there instead.
six_of_trumps, turn_up_left, six_owner, can_swap
The id of the six of the trump suit; whether the turn-up is still in the talon; the seat that may exchange that six, or undef; and whether a given seat may do it now.
hands, talon, discard, bout, over, result, history
The state. hands is a hashref of seat to a sorted arrayref of card ids, talon is the draw order with the turn-up last, discard is a count and never a list, bout is the open Game::Durak::Bout or undef, result is undef until the deal ends and then the hashref Game::Durak::Result built, and history is every event so far.
A result is set once. Setting a second one dies rather than overwriting the first, because a deal that ends twice is a bug in the caller and not a state worth carrying.
The discard is a count because the rules say a player may not look through it, and an engine that offers the list invites a consumer to show it.
hand_of, count_of, talon_left, seats
A seat's cards, how many it holds, how many cards are left to draw, and the seat numbers.
phase, turn
The stage of the bout and the seat it is waiting on. Both are derived.
view
my $view = $game->view($seat);
Everything that seat can see, and nothing else: its own hand, the bout laid out as attack and answer pairs, the trump, the turn-up while it is in the talon, the size of the talon and of the heap, how many cards each seat holds, whether this seat may exchange, its legal moves, and the result once there is one.
What it never carries is the other hand, the talon in order, which card the turn-up is once it has been drawn, the contents of the heap, the seed, or whether the other seat may exchange, which would say that seat holds the trump six.
It also carries ply, the number of events so far, which is what lets a bot draw a reproducible word for the position without being handed the history.
This is the structure Game::Durak::Bot and Game::Durak::Search take, and it is the one a consumer sends to a screen.
legal
$game->legal($seat);
What that seat may do now, as an arrayref of { kind, card }, and empty for the seat that is not on turn. take is offered only when a beat is also possible, and done only when a throw is also possible, because a position with one answer is not a decision and the engine has already resolved it.
An available exchange is a second answer, and it opens both of them again. A seat that cannot beat but may exchange is offered the exchange and the take, not the exchange alone: otherwise the rules would compel a player to give up the trump six, which they are nowhere required to do.
apply
my @events = $game->apply($seat, { kind => 'attack', card => 14 });
The move, or an error. Refusals: game_over, not_your_turn, not_legal for a move this engine does not have, wrong_phase for one that does not fit the stage, card_not_held, bout_full, rank_not_in_bout, beats_nothing, must_attack, not_the_six for an exchange that is not this seat's to make, and talon_shut for one asked after the turn-up has been drawn.
An exchange does not end the turn: the seat that made it still has the attack or the defence in front of it.
resign is the one move either seat may make at any time, on turn or not, because a person who has stopped playing is not waiting for their turn to say so. It is not in legal, which answers what the rules of durak offer the seat on turn, and giving up is not one of them: the seat that resigns is the fool, whatever it was holding, and the cards are left where they lay so that a finished deal can still be read back.
replay
my $out = Game::Durak->replay(seed => $seed, moves => $moves);
$out->{game}; # the deal, played out
$out->{events}; # everything that happened, in order
The move log is the canonical serialisation of a deal, not the position: a seed and a list of { seat, kind, card } is the whole truth, and everything else in the event stream is recomputed from it rather than stored.
Takes what "build" takes, plus moves. Returns an error at the first move the rules refuse, which is what makes the log trustworthy: a consumer cannot store a move this engine would not have made, so a forged card comes back as card_not_held rather than as a different game.
moves_of, is_move
Game::Durak->moves_of($game->history); # just the moves
Game::Durak->is_move('bout_end'); # 0
Which events are somebody's move and which are what the table did in consequence. The six that are moves are the six a consumer stores.
SEE ALSO
Game::Durak::Card, Game::Durak::Deck, Game::Durak::Bout, Game::Durak::Rules, Game::Durak::Result, Game::Durak::Search, Game::Durak::Bot, Game::Durak::Terminal, Game::Durak::Error.
The rules: https://www.pagat.com/beating/podkidnoy_durak.html.
AUTHOR
LNATION, <email@lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION.
This is free software, licensed under the Artistic License 2.0.