Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions poker-texas-hold-em/contract/Scarb.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
cairo-version = "=2.10.1"
name = "poker"
version = "1.1.0"
edition = "2023_10"

[cairo]
sierra-replace-ids = true
Expand Down
1 change: 1 addition & 0 deletions poker-texas-hold-em/contract/src/models/game.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub struct GameParams {
kicker_split: bool,
min_amount_of_chips: u256,
blind_spacing: u16,
bet_spacing: u256, // Added for bet amount validation
showdown_type: ShowdownType,
}

Expand Down
207 changes: 150 additions & 57 deletions poker-texas-hold-em/contract/src/systems/actions.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -293,19 +293,23 @@ pub mod actions {
no_of_chips > game_current_bet, "Raise amount is less than the game's current bet.",
);

// Validate bet spacing - raise amount must be in multiples of bet_spacing @kaylahray
assert!(
no_of_chips % params.bet_spacing.into() == 0,
"Raise amount must be in multiples of bet_spacing",
);

// adjust this pot accordingly
let mut game_pot = *game_pots.at(game_pots.len() - 1);

let amount_to_call = game_current_bet - player.current_bet;
let total_required = amount_to_call + no_of_chips;
if game_pot == params.small_blind.into() {
assert!(
no_of_chips > game_pot * 2, "Raise amount should be > twice the small blind.",
);
assert!(no_of_chips > game_pot * 2, "Raise must be > 2x blind.");
}

assert!(no_of_chips > 0, "Raise amount must be greater than zero.");
assert!(player.chips >= total_required, "You don't have enough chips to raise.");
assert!(no_of_chips > 0, "Raise must be > 0.");
assert!(player.chips >= total_required, "Insufficient chips to raise.");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see good English. Welldone


if !self.adjust_stake(game_id, amount_to_call, ref player) {
player.chips -= total_required;
Expand All @@ -314,6 +318,15 @@ pub mod actions {
}
game_current_bet = player.current_bet;

// Set the player as highest staker after successful raise @kaylahray
let highest_staker_selector = selector!("highest_staker");
world
.write_member(
Model::<Game>::ptr_from_keys(game_id),
highest_staker_selector,
Option::Some(player.id),
);

let mut updated_game_pots: Array<u256> = ArrayTrait::new();
let mut i = 0;
while i != game_pots.len() - 1 {
Expand All @@ -340,10 +353,31 @@ pub mod actions {

let cb = selector!("current_bet");
let mut game_current_bet = world.read_member(Model::<Game>::ptr_from_keys(game_id), cb);

if amount < game_current_bet {
self.adjust_pot(game_id, ref player, game_current_bet);
world.write_model(@player);
} else {
// If all-in amount is >= current bet, player becomes highest staker @kaylahray
let new_bet = player.current_bet + amount;
if new_bet > game_current_bet {
let highest_staker_selector = selector!("highest_staker");
world
.write_member(
Model::<Game>::ptr_from_keys(game_id),
highest_staker_selector,
Option::Some(player.id),
);
// Update game's current bet @kaylahray
world.write_member(Model::<Game>::ptr_from_keys(game_id), cb, new_bet);
}

// Update player state @kaylahray
player.current_bet += amount;
player.chips = 0;
world.write_model(@player);
}
world.write_model(@player);

self.after_play(player.id);
}

Expand Down Expand Up @@ -532,6 +566,88 @@ pub mod actions {
self.world(@"poker")
}

/// @Kaylahray Checks if betting round has concluded by verifying all active players have
/// equal bets
fn is_betting_round_concluded(
self: @ContractState, game_id: u64, world: @WorldStorage,
) -> bool {
let game: Game = world.read_model(game_id);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You read the whole game model just to read the players value. Why not read the players "member" instead using world.read_member()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

let game_players = game.players.span();

let mut highest_bet: u256 = 0;
let mut active_player_count: u32 = 0;

// First pass: find the highest bet among active players
let mut i: u32 = 0;
while i < game_players.len() {
let player_addr = *game_players.at(i);
let player: Player = world.read_model(player_addr);

if player.in_round {
active_player_count += 1;
if player.current_bet > highest_bet {
highest_bet = player.current_bet;
}
}
i += 1;
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this would work out. I think the former implementation was just fine. after_play is called after each player has played, and checks Line588 for that particular player. You looping through all the players after each player has played just for the line588 is just gas.

if active_player_count <= 1 {
return true;
}

// Second pass: check if all active players who are not all-in have matched the highest
// bet
let mut all_matched = true;
let mut j: u32 = 0;
while j < game_players.len() {
let player_addr = *game_players.at(j);
let player: Player = world.read_model(player_addr);

if player.in_round {
// Players who are all-in (0 chips) are exempt from matching
if player.chips > 0 && player.current_bet != highest_bet {
all_matched = false;
break;
}
}
j += 1;
};

all_matched
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't loop per player. If you can use an extra variable to keep track, please do.. or if the current player's bet matches the highest staker, and the next player is the highest staker


/// @kaylahray Resets betting values when a round concludes
fn reset_betting_round_values(self: @ContractState, game_id: u64, ref world: WorldStorage) {
let game: Game = world.read_model(game_id);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, and read the member, not the full Game.

let game_players = game.players.span();

let mut i = 0;
while i < game_players.len() {
let player_addr = *game_players.at(i);
// Directly write to the player's current_bet field
world
.write_member(
Model::<Player>::ptr_from_keys(player_addr),
selector!("current_bet"),
0_u256,
);
i += 1;
};

// Reset game's current_bet and highest_staker
world
.write_member(
Model::<Game>::ptr_from_keys(game_id), selector!("current_bet"), 0_u256,
);
world
.write_member(
Model::<Game>::ptr_from_keys(game_id),
selector!("highest_staker"),
Option::<ContractAddress>::None,
);
}

fn generate_id(self: @ContractState, target: felt252) -> u64 {
let mut world = self.world_default();
let mut game_id: Id = world.read_model(target);
Expand Down Expand Up @@ -776,74 +892,51 @@ pub mod actions {
);
}

/// @Reentrancy, @Birdmannn
fn after_play(ref self: ContractState, caller: ContractAddress) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please replace all the comments you deleted in this function because I don't consider this function as one that has been finalized. And replace the Birdmannn and that of Reentrancy's.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replaced Sensei

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

let mut world = self.world_default();
let mut player: Player = world.read_model(caller);
let player: Player = world.read_model(caller);
let (is_locked, game_id) = player.locked;

// Ensure the player is in a game
assert(is_locked, 'Player not in game');

let mut game: Game = world.read_model(game_id);

// Check if all community cards are dealt (5 cards in Texas Hold'em)
if game.community_cards.len() == 5 {
game.showdown = true;
}
let betting_concluded = self.is_betting_round_concluded(game_id, @world);

// Find the caller's index in the players array
let current_index_option: Option<usize> = self.find_player_index(@game.players, caller);
assert(current_index_option.is_some(), 'Caller not in game');
let current_index: usize = OptionTrait::unwrap(current_index_option);

// Update game state with the player's action

// TODO: Crosscheck after_play, and adjust... may not be needed.
if player.current_bet > game.current_bet {
game.current_bet = player.current_bet; // Raise updates the current bet
game.highest_staker = Option::Some(caller);
} else if let Option::Some(highest_staker) = game.highest_staker {
if highest_staker == caller {
// bet has gone round
if game.community_cards.len() == 5 {
game.showdown = true;
} else {
game.community_dealing = true;
}
if betting_concluded {
self.reset_betting_round_values(game_id, ref world);
game = world.read_model(game_id); // Reload game state

if game.community_cards.len() == 5 {
game.showdown = true;
} else {
game.community_dealing = true;
}
}

world.write_model(@player);

// Determine the next active player or resolve the round
let next_player_option: Option<ContractAddress> = self
.find_next_active_player(@game.players, current_index, @world);

if next_player_option.is_none() {
// No active players remain, resolve the round
game.showdown = true;
// Now, set the next player
if game.showdown {
game.next_player = Option::None;
} else {
game.next_player = next_player_option;
let current_index_option = self.find_player_index(@game.players, caller);
assert(current_index_option.is_some(), 'Caller not in game');
let current_index = OptionTrait::unwrap(current_index_option);
let next_player_option = self
.find_next_active_player(@game.players, current_index, @world);

if let Option::Some(next_player_addr) = next_player_option {
game.next_player = Option::Some(next_player_addr);
} else {
// If no next player, something is wrong, or round should end.
// `is_betting_round_concluded` should have caught this.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would I know? Did you test it? 😂

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀🙇‍♀️

game.showdown = true;
game.next_player = Option::None;
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please why did you delete the event? 👀

world.write_model(@game);

if game.showdown {
let timestamp = get_block_timestamp();
let round_number = game.round_count;
let no_of_players = game.current_player_count;
let event = RoundEnded { game_id, timestamp, round_number, no_of_players };
world
.write_member(
Model::<GameStats>::ptr_from_keys(game_id),
selector!("round_end_time"),
timestamp,
);
world.emit_event(@event);
}
}


fn find_player_index(
self: @ContractState, players: @Array<ContractAddress>, player_address: ContractAddress,
) -> Option<usize> {
Expand Down
1 change: 1 addition & 0 deletions poker-texas-hold-em/contract/src/tests/setup.cairo
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ mod setup {
use poker::models::game::m_Game;
use poker::models::hand::m_Hand;
use poker::models::player::m_Player;
use core::zeroable::Zeroable;


#[starknet::interface]
Expand Down
Loading
Loading