Sitemap
Mitchenko

Mitchenko

PART#2: Write “Durak”on Leo language with me!

10 min readFeb 17, 2025

--

Press enter or click to view image in full size

1. Introduction to Leo

Leo is a domain-specific language designed for building privacy-preserving applications on the Aleo blockchain. It takes inspiration from Rust in its syntax but includes specialized features for zero-knowledge proofs.

Key Leo Features

  • Rust-Like Syntax: Familiar variables, functions, control flow, etc.
  • Static Typing: Helps catch errors at compile time.
  • Privacy-Focused: Integrates with Aleo’s zero-knowledge system.
  • Package Manager: The leo CLI manages build, test, and deploy tasks.

2. Prerequisites-

  • Installed Leo CLI: Aleo developer documentation explains how to install leo.
  • Familiarity with Rust-Like Syntax: Basic knowledge of variables, conditionals, enumerations, etc.
  • Understanding of Durak: Know the fundamentals of the Durak card game (36-card deck, trump suit, attacker/defender flow, etc.).

3. Creating a Leo Project

Use the CLI to initialize a new Leo project:

leo new durak
cd durak

This creates:

  • ./durak/src/main.leo: Main Leo file.
  • ./durak/Leo.toml: Configuration file.
  • ./durak/tests/: Directory for tests.

4. Leo Syntax Basics

4.1 Variables and Types

Leo variables are declared with let. By default, they are immutable:

let x = 5u32;            // x is immutable
let mut y = 10u32; // y is mutable
y = y + 5u32; // Allowed since y is mutable

Basic types include:

  • bool: Boolean values.
  • Unsigned integers: u8, u16, u32, etc.
  • Arrays: [T; N].
  • Option: Some(T) or None().
  • Structs and enums.

4.2 Functions

Functions start with function, and you specify parameters and return types:

function sum(a: u32, b: u32) -> u32 {
return a + b;
}

4.3 Conditionals and Loops

if condition {
// ...
} else if other_condition {
// ...
} else {
// ...
}
for i in 0..5u8 {
// i goes from 0 to 4
}while some_condition {
// ...
}

4.4 Structs and Enums

Struct:

struct Player {
id: u8,
name: u32, // placeholder for name
}

Enum:

enum Suit {
Clubs,
Diamonds,
Hearts,
Spades
}

5. Designing Durak

5.1 Deck Representation

Durak traditionally uses a 36-card deck (ranks 6 through Ace, suits Clubs, Diamonds, Hearts, Spades). We can store it as a fixed-size array [Card; 36].

5.2 Card Ranks and Suits

We’ll define an enum for suits and an enum for ranks (6 through Ace). Each Card is (rank, suit). We must also track the trump suit for a given round.

5.3 Players and Game State

Each Player has:

  • A hand (list of cards).
  • A status (maybe number of cards in hand, or whether they’re still in the game).

The Game struct tracks:

  • The deck (the 36 cards).
  • The trump suit (one of the four suits).
  • A list of players (2–6 typically).
  • The current attacker and defender indices.
  • Possibly a discard pile for beaten cards.

5.4 Basic Game Flow

  1. Deal each player 6 cards, place the leftover in a draw pile, reveal the bottom card (determines trump suit).
  2. The attacker lays down one or more cards of the same rank.
  3. The defender must beat each attacking card with either a higher-ranked same-suit card or any trump if the attacking card is non-trump.
  4. If the defender cannot or chooses not to beat the card, they pick up all the cards on the table.
  5. If the defender beats all cards, those cards go to a discard pile and the roles switch.
  6. After a round, each player draws from the deck until they have 6 cards (if available).
  7. The game ends when only one player still has cards (that player is the “fool”).

6. Implementing Durak in Leo

Open src/main.leo and start defining the data structures and logic.

6.1 Data Structures

// Suit
enum Suit {
Clubs,
Diamonds,
Hearts,
Spades
}
// Ranks from 6..Ace
enum Rank {
Six,
Seven,
Eight,
Nine,
Ten,
Jack,
Queen,
King,
Ace
}// Card = rank + suit
struct Card {
rank: Rank,
suit: Suit
}// Each player has a hand array (up to 36, realistically never that big, but we’ll allocate).
struct Player {
hand: [Option<Card>; 36],
hand_size: u8,
still_in_game: bool,
}// The entire game state
struct Game {
deck: [Card; 36], // the 36 cards
deck_top: u8, // index to draw the next card from the deck
trump_suit: Suit,
players: [Player; 4], // up to 4 players for demonstration
num_players: u8,
attacker: u8, // index of attacker
defender: u8, // index of defender
}

Get Mitchenko’s stories in your inbox

Join Medium for free to get updates from this writer.

6.2 Initialization and Shuffl

Creating the Deck

function create_deck() -> [Card; 36] {
let suits = [
Suit::Clubs(), Suit::Diamonds(), Suit::Hearts(), Suit::Spades()
];
let ranks = [
Rank::Six(), Rank::Seven(), Rank::Eight(), Rank::Nine(), Rank::Ten(),
Rank::Jack(), Rank::Queen(), Rank::King(), Rank::Ace()
];
let mut deck: [Card; 36] = [
Card { rank: Rank::Six(), suit: Suit::Clubs() }; 36
];
let mut index = 0u8; for s in 0..4u8 {
for r in 0..9u8 {
deck[index as usize] = Card {
rank: ranks[r as usize],
suit: suits[s as usize],
};
index = index + 1u8;
}
} return deck;
}

Shuffling the Deck

For demonstration, we’ll do a simple pseudo-shuffle:

function shuffle_deck(deck: &mut [Card; 36]) {
let mut i = 35u8;
while i > 0u8 {
// Pseudo-random index j
let j = (i + 13u8) % 36u8;
let temp = deck[i as usize];
deck[i as usize] = deck[j as usize];
deck[j as usize] = temp; i = i - 1u8;
}
}

Initial Game Setu

We can define init_game to create and shuffle the deck, determine trump from the bottom card (index 35), and fill players’ hands with 6 cards each.

function init_game(num_players: u8) -> Game {
let mut deck = create_deck();
shuffle_deck(&mut deck);
// Trump suit is from the last card in the deck
let trump_card = deck[35usize];
let trump_suit = trump_card.suit; // Initialize players
let mut players: [Player; 4] = [
Player { hand: [None(); 36], hand_size: 0u8, still_in_game: true },
Player { hand: [None(); 36], hand_size: 0u8, still_in_game: true },
Player { hand: [None(); 36], hand_size: 0u8, still_in_game: true },
Player { hand: [None(); 36], hand_size: 0u8, still_in_game: true },
]; let mut game = Game {
deck,
deck_top: 0u8, // next card to draw is at index 0
trump_suit,
players,
num_players,
attacker: 0u8, // the first attacker (e.g., index 0)
defender: 1u8, // next player is defender
}; // Deal 6 cards to each player
for _ in 0..6u8 {
for p in 0..num_players {
draw_card(&mut game, p);
}
} return game;
}

6.3 Dealing Cards

function draw_card(game: &mut Game, player_index: u8) -> bool {
if game.deck_top >= 36u8 {
return false; // no cards left
}
let mut player = &mut game.players[player_index as usize];
if player.hand_size >= 36u8 {
return false;
}
let card = game.deck[game.deck_top as usize];
game.deck_top = game.deck_top + 1u8; player.hand[player.hand_size as usize] = Some(card);
player.hand_size = player.hand_size + 1u8;
return true;
}

6.4 Attacking and Defending

Comparing Two Cards

A card can beat (or cover) another if:

  1. Same suit and higher rank, or
  2. Trump if the attack card is non-trump (and the defending card is a trump with a higher or equal rank if suits are both trump).

We need a rank comparison function (6 is lowest, Ace is highest). Let’s define a numeric mapping for rank:

function rank_value(rank: Rank) -> u8 {
match rank {
Rank::Six() => 0u8,
Rank::Seven() => 1u8,
Rank::Eight() => 2u8,
Rank::Nine() => 3u8,
Rank::Ten() => 4u8,
Rank::Jack() => 5u8,
Rank::Queen() => 6u8,
Rank::King() => 7u8,
Rank::Ace() => 8u8
}
}
// Returns true if `defender_card` beats `attacker_card` given `trump_suit`
function can_beat(attacker_card: Card, defender_card: Card, trump_suit: Suit) -> bool {
// If same suit, check rank
if attacker_card.suit == defender_card.suit {
return rank_value(defender_card.rank) > rank_value(attacker_card.rank);
}
// If the defender card is trump and the attacker card is not trump
if defender_card.suit == trump_suit && attacker_card.suit != trump_suit {
return true;
} return false;
}

Attacking/Defending Actions

We assume a minimal approach: the attacker lays down 1 card from their hand, the defender must pick a card that can beat it.

// Attempt to attack with `attack_card_index` in attacker's hand
function attack(game: &mut Game, attack_card_index: u8) -> bool {
let atk_idx = game.attacker;
let mut attacker_player = &mut game.players[atk_idx as usize];
if attack_card_index >= attacker_player.hand_size {
return false;
}
// Retrieve the attack card
let card_opt = attacker_player.hand[attack_card_index as usize];
match card_opt {
Some(card) => {
// Mark that card as used (or remove it from hand temporarily?)
// For simplicity, we’ll just store it in a “table” variable. In a real game,
// we’d have a separate data structure for the table cards. // We remove it from the attacker's hand
attacker_player.hand[attack_card_index as usize] = None();
attacker_player.hand_size = attacker_player.hand_size - 1u8;
// Return success
return true;
}
None() => {
return false;
}
}
}// Attempt to defend with `defend_card_index` in defender's hand
// `attacking_card` is the card we must beat
function defend(game: &mut Game, defend_card_index: u8, attacking_card: Card) -> bool {
let def_idx = game.defender;
let mut defender_player = &mut game.players[def_idx as usize]; if defend_card_index >= defender_player.hand_size {
return false;
} let def_card_opt = defender_player.hand[defend_card_index as usize];
match def_card_opt {
Some(def_card) => {
if can_beat(attacking_card, def_card, game.trump_suit) {
// Defender used this card successfully
// Remove from defender's hand
defender_player.hand[defend_card_index as usize] = None();
defender_player.hand_size = defender_player.hand_size - 1u8;
return true;
}
return false;
}
None() => {
return false;
}
}
}

Note: In an actual Durak game, multiple cards can be attacked at once, defenders can cover each, and the attacker can “toss in” more cards of the same ranks on the table. This example shows only the fundamental logic of one attack card and one defend card.

6.5 Ending a Round and Checking for a Winner

After an attack/defend exchange:

  1. If the defender cannot or chooses not to defend, they take the attacker’s card(s) into their hand.
  2. If the defender beats all attacking cards, those cards go to a discard (for demonstration, we won’t store them — just remove them from the players’ hands).
  3. Players draw from the deck until they have 6 cards (if deck has enough).
  4. The roles switch (attacker/defender) if the defender successfully defended, otherwise the defender stays the same and the next player becomes the attacker, etc.

We also check if a player’s hand is empty (they are safe for the rest of the game). If only one player remains with cards, the game ends.

function refill_hands(game: &mut Game) {
// Each player draws until they have 6 (or deck is empty)
for p in 0..game.num_players {
let mut player = &mut game.players[p as usize];
while player.hand_size < 6u8 && game.deck_top < 36u8 {
draw_card(game, p);
}
}
}
function check_for_winner(game: &mut Game) -> Option<u8> {
// If only one player still has cards, that player is the “fool”.
// If multiple have cards, no winner yet.
// If exactly one has cards and others are empty, that one is the loser, so no standard “winner”.
// Some versions define the last with cards is the loser, rest are winners.
// For demonstration, we’ll return Some(index) if the game is definitely over, or None if still ongoing. let mut players_in_game = 0u8;
let mut last_player_idx = 255u8; // sentinel for i in 0..game.num_players {
let pl = game.players[i as usize];
if pl.still_in_game {
// Check if they have any cards
let has_cards = pl.hand_size > 0u8;
if has_cards {
players_in_game = players_in_game + 1u8;
last_player_idx = i;
}
}
} // If 0 or 1 players with cards, game is over
if players_in_game <= 1u8 {
return Some(last_player_idx);
}
return None();
}

6.6 Main Function Example

An example main function that initializes the game, performs a single attack/defend, and logs the outcome:

function main() {
let mut game = init_game(2u8); // 2 players for demonstration
// Attacker attempts to play the first card in their hand (index 0)
let attack_success = attack(&mut game, 0u8);
if !attack_success {
console.log("Attacker failed to attack (no card?).");
return;
} // Suppose the defender tries to defend with the first card in their hand
// We need the attacking card for the defend logic. In a real game, you'd store the card on the table.
// We'll simulate it here by retrieving it from a temporary variable for demonstration.
// Let’s assume the attacker’s card was removed, so we’ll just pick any card from the deck to represent it.
let attacking_card = Card {
rank: Rank::Six(),
suit: game.trump_suit
}; let defend_success = defend(&mut game, 0u8, attacking_card);
if defend_success {
console.log("Defender successfully defended!");
// Cards are “discarded”
} else {
console.log("Defender failed, must pick up the cards!");
// In real Durak, the attacker’s card would be added to the defender’s hand
// We'll skip that detail in this simplified example
} // Refill players' hands
refill_hands(&mut game); // Check if the game ended
let winner_opt = check_for_winner(&mut game);
match winner_opt {
Some(idx) => {
console.log("Game over. Last player with cards is player: ", idx);
}
None() => {
console.log("Game continues.");
}
}
}

Compile and run:

leo run

7. Testing

Leo supports testing with a built-in framework. Create a file in ./tests/ (e.g., durak_test.leo):

import "../src/main.leo";
function test_init_game() {
let game = init_game(2u8);
testing.assert(game.num_players == 2u8, "Should have 2 players.");
testing.assert_eq(game.deck_top, 12u8, "After dealing 6 cards each to 2 players = 12 cards drawn.");
}function test_draw_card() {
let mut game = init_game(2u8);
let success = draw_card(&mut game, 0u8);
testing.assert(success, "Should be able to draw a card.");
}

Run tests:

leo test

8. Deployment on Aleo

To deploy your Durak game on the Aleo network (for zero-knowledge proofs and on-chain logic):

  1. Build the project:
  • leo build
  1. Publish or deploy to your Aleo account:
  • leo publish
  1. Invoke your functions with the Aleo CLI, passing arguments (like which card to attack with, etc.), and the network will handle zero-knowledge proofs of correctness.

Refer to the Aleo Docs for detailed steps on account setup and on-chain transactions.

9. Conclusion

In this tutorial, you:

  1. Learned Leo basics (variables, functions, control flow, and data structures).
  2. Explored Durak game fundamentals: 36-card deck, suits, ranks, dealing, attacking, and defending.
  3. Implemented a simplified Durak in Leo, including how to shuffle, deal, compare cards, and simulate an attack/defend round.
  4. Learned about testing in Leo and how to deploy your application on the Aleo network.

To fully replicate Durak’s complexity, you can:

  • Implement multiple attacking cards.
  • Allow attacker to “toss in” more cards of the same ranks already on the table.
  • Let other players join in attacking (depending on Durak variant).
  • Enforce picking up or discarding logic more precisely.

With this foundation in place, you can expand your zero-knowledge Durak application into a fully playable on-chain card game. Good luck creating and refining your private, trust-minimized Durak game in Leo!

--

--