-
Notifications
You must be signed in to change notification settings - Fork 50
enhance gameplay logic #197
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
7530be3
8d94af7
1e9c400
21edf5c
c3534b4
74ec301
d2f921c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -292,26 +292,38 @@ 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 | ||
| let bet_spacing = params.bet_spacing; | ||
| // Only the increment after current_bet needs to be multiple of bet_spacing. @kaylahray | ||
| let raise_delta = no_of_chips - game_current_bet; | ||
| assert(raise_delta % bet_spacing.into() == 0, 'Invalid raise 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."); | ||
|
|
||
| if !self.adjust_stake(game_id, amount_to_call, ref player) { | ||
| player.chips -= total_required; | ||
| player.current_bet += total_required; | ||
| game_pot += total_required; | ||
| } | ||
|
|
||
| game_current_bet = player.current_bet; | ||
| // @Kaylahray 👇 | ||
| world | ||
| .write_member( | ||
| Model::<Game>::ptr_from_keys(game_id), | ||
| selector!("highest_staker"), | ||
| Option::Some(player.id), | ||
| ); | ||
|
|
||
| let mut updated_game_pots: Array<u256> = ArrayTrait::new(); | ||
| let mut i = 0; | ||
|
|
@@ -328,6 +340,7 @@ pub mod actions { | |
| self.after_play(player.id); | ||
| } | ||
|
|
||
|
|
||
| /// @dub_zn | ||
| fn all_in(ref self: ContractState) { | ||
| let mut world = self.world_default(); | ||
|
|
@@ -338,10 +351,28 @@ pub mod actions { | |
| let amount = player.chips; | ||
|
|
||
| let cb = selector!("current_bet"); | ||
| let mut game_current_bet = world.read_member(Model::<Game>::ptr_from_keys(game_id), cb); | ||
| let game_current_bet = world.read_member(Model::<Game>::ptr_from_keys(game_id), cb); | ||
|
|
||
| // Update player state for all-in @kaylahray | ||
| player.current_bet += amount; | ||
| player.chips = 0; | ||
|
|
||
| // @kaylahray Set highest_staker if this all-in creates new highest bet | ||
| if player.current_bet > game_current_bet { | ||
| world | ||
| .write_member( | ||
| Model::<Game>::ptr_from_keys(game_id), | ||
| selector!("highest_staker"), | ||
| Option::Some(player.id), | ||
| ); | ||
| world.write_member(Model::<Game>::ptr_from_keys(game_id), cb, player.current_bet); | ||
| } | ||
|
|
||
| // Handle side pot creation if needed | ||
| if amount < game_current_bet { | ||
| self.adjust_pot(game_id, ref player, game_current_bet); | ||
| } | ||
|
|
||
| world.write_model(@player); | ||
| self.after_play(player.id); | ||
| } | ||
|
|
@@ -797,25 +828,15 @@ pub mod actions { | |
| 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; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Write player state to storage BEFORE checking betting round completion | ||
| world.write_model(@player); | ||
|
|
||
| // Determine the next active player or resolve the round | ||
| // Determine the next active player | ||
| let next_player_option: Option<ContractAddress> = self | ||
| .find_next_active_player(@game.players, current_index, @world); | ||
|
|
||
|
|
@@ -824,6 +845,12 @@ pub mod actions { | |
| game.showdown = true; | ||
| } else { | ||
| game.next_player = next_player_option; | ||
|
|
||
| // Check if betting round is complete (more gas efficient) @kaylahray | ||
| if self.is_betting_round_complete(@game, @world) { | ||
| // Reset betting state efficiently | ||
| self.reset_betting_round(game_id, ref game, ref world); | ||
| } | ||
| } | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please why did you delete the event? 👀 |
||
| world.write_model(@game); | ||
|
|
@@ -843,6 +870,57 @@ pub mod actions { | |
| } | ||
| } | ||
|
|
||
| /// betting round completion check @kaylahray | ||
| fn is_betting_round_complete( | ||
| self: @ContractState, game: @Game, world: @dojo::world::WorldStorage, | ||
| ) -> bool { | ||
| let mut all_equal_bets = true; | ||
| let mut active_players = 0_u32; | ||
|
|
||
| // Single pass through players to check betting status @kaylahray | ||
| for player_addr in game.players.span() { | ||
| let p: Player = world.read_model(*player_addr); | ||
| if p.in_round { | ||
| active_players += 1; | ||
| if p.current_bet != *game.current_bet { | ||
| all_equal_bets = false; | ||
| break; | ||
| } | ||
| } | ||
| }; | ||
|
||
|
|
||
| // @kaylahray Betting round complete if all active players have equal bets and the game | ||
| // bet is not zero | ||
| all_equal_bets && active_players > 1 && *game.current_bet > 0 | ||
| } | ||
|
|
||
| /// @kaylahray batch reset of betting round | ||
| fn reset_betting_round( | ||
| ref self: ContractState, game_id: u64, ref game: Game, ref world: WorldStorage, | ||
| ) { | ||
| // Reset game state | ||
| game.highest_staker = Option::None; | ||
| game.current_bet = 0; | ||
|
|
||
| // Determine next phase | ||
| if game.community_cards.len() == 5 { | ||
| game.showdown = true; | ||
| } else { | ||
| game.community_dealing = true; | ||
| } | ||
|
|
||
| // Batch reset all players' current_bet using write_member for better gas efficiency | ||
| for player_addr in game.players.span() { | ||
| world | ||
| .write_member( | ||
| Model::<Player>::ptr_from_keys(*player_addr), | ||
| selector!("current_bet"), | ||
| 0_u256, | ||
| ); | ||
| } | ||
| } | ||
|
|
||
|
|
||
| fn find_player_index( | ||
| self: @ContractState, players: @Array<ContractAddress>, player_address: ContractAddress, | ||
| ) -> Option<usize> { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -342,6 +342,113 @@ mod tests { | |
| ); | ||
| } | ||
|
|
||
| // [Betting Logic Tests] - Testing highest staker and bet reset functionality @kaylahray | ||
| #[test] | ||
| fn test_highest_staker_and_bet_reset() { | ||
| // [Setup] | ||
| let contracts = array![CoreContract::Actions]; | ||
| let (mut world, systems) = deploy_contracts(contracts); | ||
| mock_poker_game(ref world); | ||
|
|
||
| // Player 1 (PLAYER_1) raises @kaylahray | ||
| let mut game: Game = world.read_model(1); | ||
| let raise_amount = game.params.small_blind * 4; // Example raise = 40 | ||
| set_contract_address(PLAYER_1()); | ||
| systems.actions.raise(raise_amount.into()); | ||
|
|
||
| // Check that highest_staker is set correctly | ||
| game = world.read_model(1); | ||
| assert!(game.highest_staker.is_some(), "Highest staker not set on raise"); | ||
| assert_eq!(game.highest_staker.unwrap(), PLAYER_1(), "Incorrect highest staker"); | ||
| assert_eq!(game.current_bet, raise_amount.into(), "Game bet not updated on raise"); | ||
|
|
||
| // Player 2 calls - should match game.current_bet | ||
| set_contract_address(PLAYER_2()); | ||
| systems.actions.call(); | ||
|
|
||
| // Check player 2 state after call | ||
| let player_2_after_call: Player = world.read_model(PLAYER_2()); | ||
| assert_eq!( | ||
| player_2_after_call.current_bet, raise_amount.into(), "Player 2 didn't call correctly", | ||
| ); | ||
|
|
||
| // Check if betting round completion would be detected at this point | ||
| game = world.read_model(1); | ||
| let player_1_mid: Player = world.read_model(PLAYER_1()); | ||
| // At this point: player1=40, player2=40, player3=0, game=40 | ||
| // So betting round should NOT be complete yet | ||
|
|
||
| // Player 3 calls - this should complete the betting round and reset state | ||
| set_contract_address(PLAYER_3()); | ||
| systems.actions.call(); | ||
|
|
||
| // Check all player states immediately after the call | ||
| game = world.read_model(1); | ||
| let player_1: Player = world.read_model(PLAYER_1()); | ||
| let player_2: Player = world.read_model(PLAYER_2()); | ||
| let player_3: Player = world.read_model(PLAYER_3()); | ||
|
|
||
| // At this point, all players should have current_bet = 40, game.current_bet = 40 | ||
| // The betting round should be complete and reset should have happened | ||
| assert!(game.highest_staker.is_none(), "Highest staker not reset"); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then the reset is complete. Check the community_dealing. As per last review on the tests, and the issue description, community dealing must be tested to. After first first round of betting, Add test: The fourth one must panic and this just means that the betting cycle you wrote above can be extracted into a separate function called There are other edge cases tests, but I'll leave it for another issue.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| assert_eq!(game.current_bet, 0, "Game current bet not reset"); | ||
| assert_eq!(player_1.current_bet, 0, "Player 1 bet not reset"); | ||
| assert_eq!(player_2.current_bet, 0, "Player 2 bet not reset"); | ||
| assert_eq!(player_3.current_bet, 0, "Player 3 bet not reset"); | ||
| } | ||
|
|
||
| // @kaylahray Testing bet spacing logic | ||
| #[test] | ||
| #[should_panic(expected: ('Invalid raise spacing', 'ENTRYPOINT_FAILED'))] | ||
| fn test_bet_spacing_fail() { | ||
| // [Setup] | ||
| let contracts = array![CoreContract::Actions]; | ||
| let (mut world, systems) = deploy_contracts(contracts); | ||
| mock_poker_game(ref world); | ||
|
|
||
| let game: Game = world.read_model(1); | ||
| // Not a multiple of bet_spacing (which is small_blind) | ||
| let raise_amount = game.params.small_blind * 2 + 1; | ||
| set_contract_address(PLAYER_1()); | ||
| systems.actions.raise(raise_amount.into()); | ||
| } | ||
| // @kaylahray Testing bet spacing success | ||
| #[test] | ||
| fn test_bet_spacing_success() { | ||
| // [Setup] | ||
| let contracts = array![CoreContract::Actions]; | ||
| let (mut world, systems) = deploy_contracts(contracts); | ||
| mock_poker_game(ref world); | ||
|
|
||
| let game: Game = world.read_model(1); | ||
| let raise_amount = game.params.small_blind * 4; // 40 is multiple of bet_spacing (20) | ||
| set_contract_address(PLAYER_1()); | ||
| systems.actions.raise(raise_amount.into()); | ||
|
|
||
| let updated_game: Game = world.read_model(1); | ||
| assert_eq!(updated_game.current_bet, raise_amount.into(), "Bet spacing success failed"); | ||
| } | ||
| // @kaylahray Testing all-in sets highest staker | ||
| #[test] | ||
| fn test_all_in_sets_highest_staker() { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As per the issue description, The aim was for all_in to work regardless of the bet_spacing. So write a new test that mocks the player's balance to not be a multiple of the bet. spacing and call the all_in. It should pass
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| // [Setup] | ||
| let contracts = array![CoreContract::Actions]; | ||
| let (mut world, systems) = deploy_contracts(contracts); | ||
| mock_poker_game(ref world); | ||
|
|
||
| // Player 1 goes all-in | ||
| set_contract_address(PLAYER_1()); | ||
| systems.actions.all_in(); | ||
|
|
||
| let game: Game = world.read_model(1); | ||
| let player_1: Player = world.read_model(PLAYER_1()); | ||
|
|
||
| assert!(game.highest_staker.is_some(), "Highest staker not set on all-in"); | ||
| assert_eq!(game.highest_staker.unwrap(), player_1.id, "Incorrect highest staker on all-in"); | ||
| assert_eq!(game.current_bet, player_1.current_bet, "Game bet not updated on all-in"); | ||
| assert_eq!(player_1.chips, 0, "Player should have 0 chips after all-in"); | ||
| } | ||
|
|
||
| // [Mocks] | ||
| fn mock_poker_game(ref world: WorldStorage) { | ||
| let game = Game { | ||
|
|
@@ -350,7 +457,7 @@ mod tests { | |
| has_ended: false, | ||
| current_round: 1, | ||
| round_in_progress: true, | ||
| current_player_count: 2, | ||
| current_player_count: 3, | ||
| players: array![PLAYER_1(), PLAYER_2(), PLAYER_3()], | ||
| deck: array![], | ||
| next_player: Option::Some(PLAYER_1()), | ||
|
|
||






There was a problem hiding this comment.
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