Skip to content

Commit

Permalink
Complete bet logic
Browse files Browse the repository at this point in the history
  • Loading branch information
arssly committed Oct 12, 2024
1 parent e1166e0 commit ecefc06
Show file tree
Hide file tree
Showing 16 changed files with 355 additions and 59 deletions.
14 changes: 4 additions & 10 deletions apps/web/src/app/games/[id]/state/actions/gameEventHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ export function awaitingPlayerBetHandler({
pot,
bettingPlayer,
availableActions,
placedAction,
}: AwaitingBetEvent) {
console.log("awaiting bet", bettingRound, pot, bettingPlayer, availableActions);
state$.gameState.status.set(getGameStatus(bettingRound));
Expand All @@ -56,17 +55,14 @@ export function awaitingPlayerBetHandler({
}
state$.gameState.betState.awaitingBetFrom.set(bettingPlayer);
state$.gameState.betState.availableActions.set(availableActions);
state$.gameState.betState.placedBet.set(placedAction);
}

export function playerPlacedBetHandler({
bettingRound,
player,
potBeforeBet,
potAfterBet,
betAction,
availableActions,
placedAction,
}: PlayerPlacedBetEvent) {
console.log("placed bet", bettingRound, potAfterBet, betAction, availableActions);
state$.gameState.status.set(getGameStatus(bettingRound));
Expand All @@ -77,13 +73,11 @@ export function playerPlacedBetHandler({
if (state$.gameState.betState.awaitingBetFrom.peek() === player) {
state$.gameState.betState.awaitingBetFrom.set(undefined);
}
state$.gameState.betState.lastBet.player.set(player);
state$.gameState.betState.lastBet.action.set(betAction);
state$.gameState.betState.lastBet.amount.set(
potAfterBet.reduce((sum, a) => sum + a, 0) - potBeforeBet.reduce((sum, a) => sum + a, 0),
);
const player$ = state$.gameState.players.find((p) => p.id.peek() === player.id)!;
player$.roundAction.action.set(betAction);
//TODO: calc amount
player$.roundAction.amount.set(10);
state$.gameState.betState.availableActions.set(availableActions);
state$.gameState.betState.placedBet.set(placedAction);
}

export function receivedPublicCardsHandler({ cards, round }: ReceivedPublicCardsEvent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export const selectPot$ = () => state$.gameState.pot;
export const selectBetState$ = () => state$.gameState.betState;
export const selectAvailableActions$ = () => state$.gameState.betState?.availableActions ?? [];
export const selectAwaitingBetFrom$ = () => state$.gameState.betState?.awaitingBetFrom;
export const selectLastBet$ = () => state$.gameState.betState?.lastBet;
export const selectPublicCards$ = () => {
const flopCards = state$.gameState.flopCards.get() || [];
const turnCard = state$.gameState.turnCard.get() || [];
Expand Down
25 changes: 13 additions & 12 deletions apps/web/src/app/games/[id]/state/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,27 @@ import {
} from "@jeton/ts-sdk";
import { type Observable, observable } from "@legendapp/state";

type UIPlayer = Player & {
roundAction?: {
action: BettingActions;
amount: number;
};
};

type GameState = {
dealer?: Player;
players: (Player | null)[];
dealer?: UIPlayer;
players: (UIPlayer | null)[];
status?: GameStatus;
shufflingPlayer?: Player;
shufflingPlayer?: UIPlayer;
myCards?: [number, number];
flopCards?: [number, number, number];
turnCard?: [number];
riverCard?: [number];
pot: number[];
pot: number;
betState?: {
round: BettingRounds;
awaitingBetFrom?: Player;
awaitingBetFrom?: UIPlayer;
availableActions: PlacingBettingActions[];
lastBet?: {
player: Player;
action: BettingActions;
amount: number;
};
placedBet?: PlacingBettingActions | null;
};
};
export interface State {
Expand All @@ -44,7 +45,7 @@ export const state$: Observable<State> = observable<State>({
loading: true,
initializing: false,
gameState: {
pot: [0],
pot: 0,
players: [],
status: GameStatus.AwaitingStart,
dealer: {} as Player,
Expand Down
10 changes: 3 additions & 7 deletions packages/ts-sdk/src/Game/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,10 +310,8 @@ export class Game extends EventEmitter<GameEventMap> {
bettingRound: data.bettingRound,
player: sender,
betAction: data.action,
potBeforeBet,
potAfterBet: Array.from(this.handState.pot),
potAfterBet: this.handState.pot.reduce((sum, p) => sum + p, 0),
availableActions: this.handState.bettingManager.selfLegalActions,
placedAction: this.handState.bettingManager.placedAction || null,
});
const nextPlayer = this.handState.bettingManager.nextBettingPlayer;
const nextPublicCardRound = getNextPublicCardRound(data.bettingRound);
Expand All @@ -334,9 +332,8 @@ export class Game extends EventEmitter<GameEventMap> {
this.emit(GameEventTypes.AWAITING_BET, {
bettingRound: data.bettingRound,
bettingPlayer: nextPlayer,
pot: this.handState.pot,
pot: this.handState.pot.reduce((sum, p) => sum + p, 0),
availableActions: this.handState.bettingManager.selfLegalActions,
placedAction: this.handState.bettingManager.placedAction || null,
});
}
// i am big blind no need to get the bet from ui I just send it
Expand Down Expand Up @@ -465,9 +462,8 @@ export class Game extends EventEmitter<GameEventMap> {
this.emit(GameEventTypes.AWAITING_BET, {
bettingRound: round,
bettingPlayer: nextBettingPlayer,
pot: this.handState.pot,
pot: this.handState.pot.reduce((sum, p) => sum + p, 0),
availableActions: this.handState.bettingManager.selfLegalActions,
placedAction: this.handState.bettingManager.placedAction || null,
});
}
if (!isItMyTurn) return;
Expand Down
119 changes: 107 additions & 12 deletions packages/ts-sdk/src/Jeton/Jeton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import {
type OnChainDataSourceInstance,
OnChainEventTypes,
type OnChainPlayerCheckedInData,
type OnChainPlayerPlacedBetData,
type OnChainShuffledDeckData,
type OnChainTableObject,
} from "@src/OnChainDataSource";
import { AptosOnChainDataSource } from "@src/OnChainDataSource/AptosOnChainDataSource";
import onChainDataMapper, { covertTableInfo } from "@src/OnChainDataSource/onChainDataMapper";
import {
BettingActions,
BettingRounds,
type ChipUnits,
type GameEventMap,
GameEventTypes,
GameStatus,
type PlacingBettingActions,
PlacingBettingActions,
type Player,
PlayerStatus,
PublicCardRounds,
Expand All @@ -25,13 +28,20 @@ import {
import { type PendingMemo, createPendingMemo } from "@src/utils/PendingMemo";
import { type ZkDeckUrls, createLocalZKDeck } from "@src/utils/createZKDeck";
import { hexStringToUint8Array } from "@src/utils/unsignedInt";
import { getBettingRound, getNextBettingRound, getPublicCardRound } from "..";
import {
getBettingPlayer,
getBigBlindPlayer,
getCardIndexes,
getCardShares,
getDeck,
getNumberOfRaisesLeft,
getPlayerByAddress,
getPlayerByIndex,
getPot,
getPrivateCardsIndexes,
getShufflingPlayer,
getSmallBlindPlayer,
isActivePlayer,
} from "./helpers";

Expand Down Expand Up @@ -96,14 +106,62 @@ export class Jeton extends EventEmitter<GameEventMap> {
this.onChainDataSource.on(OnChainEventTypes.PLAYER_CHECKED_IN, this.newPlayerCheckedIn);
this.onChainDataSource.on(OnChainEventTypes.SHUFFLED_DECK, this.playerShuffledDeck);
this.onChainDataSource.on(OnChainEventTypes.CARDS_SHARES_RECEIVED, this.receivedCardsShares);
// this.onChainDataSource.on(
// OnChainEventTypes.PUBLIC_CARDS_SHARES_RECEIVED,
// this.receivedPublicCardsShares,
// );
// this.onChainDataSource.on(OnChainEventTypes.PLAYER_PLACED_BET, this.receivedPlayerBet);
this.onChainDataSource.on(OnChainEventTypes.PLAYER_PLACED_BET, this.receivedPlayerBet);
this.onChainDataSource.listenToTableEvents(this.tableInfo.id);
}

private receivedPlayerBet = async (data: OnChainPlayerPlacedBetData) => {
console.log("received Player bet", data);
const onChainTableObject = await this.pendingMemo.memoize(this.queryGameState);
const newState = onChainDataMapper.convertJetonState(onChainTableObject);
const bettingRound = getBettingRound(newState.status);
let sender: Player;
if (data.action === BettingActions.SMALL_BLIND) {
sender = onChainDataMapper.convertPlayer(getSmallBlindPlayer(onChainTableObject));
} else if (data.action === BettingActions.BIG_BLIND) {
sender = onChainDataMapper.convertPlayer(getBigBlindPlayer(onChainTableObject));
} else {
sender = onChainDataMapper.convertPlayer(
getPlayerByAddress(onChainTableObject, data.address!)!,
);
}

const pot = getPot(onChainTableObject);
this.emit(GameEventTypes.PLAYER_PLACED_BET, {
bettingRound,
player: sender,
betAction: data.action,
potAfterBet: pot,
availableActions: this.calculateAvailableActions(onChainTableObject),
});

const nextPlayer = getBettingPlayer(onChainTableObject);
console.log("next player is", nextPlayer);

if (
nextPlayer === null &&
[GameStatus.DrawFlop, GameStatus.DrawRiver, GameStatus.DrawTurn].includes(newState.status)
) {
console.log("betting round finished, sharing keys for:", newState.status);
this.createAndSharePublicKeyShares(onChainTableObject, getPublicCardRound(newState.status));
return;
}
if (nextPlayer === null) {
//TODO: showdown
return;
}
// don't send awaiting bet to ui for small and big blind
if (data.action !== BettingActions.SMALL_BLIND && nextPlayer) {
this.emit(GameEventTypes.AWAITING_BET, {
bettingRound: bettingRound,
bettingPlayer: onChainDataMapper.convertPlayer(nextPlayer),
pot,
availableActions: this.calculateAvailableActions(onChainTableObject),
});
}
this.gameState = newState;
};

private receivedCardsShares = async (data: OnChainCardsSharesData) => {
const onChainTableObject = await this.pendingMemo.memoize(this.queryGameState);
const newState = onChainDataMapper.convertJetonState(onChainTableObject);
Expand Down Expand Up @@ -161,6 +219,18 @@ export class Jeton extends EventEmitter<GameEventMap> {
this.gameState = onChainDataMapper.convertJetonState(onChainTableObject);
};

private async createAndSharePublicKeyShares(
tableObject: OnChainTableObject,
round: PublicCardRounds,
) {
const cardIndexes = getCardIndexes(tableObject, round);
const proofsAndShares = await this.createDecryptionShareProofsFor(cardIndexes, tableObject);
proofsAndShares.sort((a, b) => a.cardIndex - b.cardIndex);
const proofsToSend = proofsAndShares.map((pas) => pas.proof);
const sharesToSend = proofsAndShares.map((pas) => pas.decryptionCardShare);
this.onChainDataSource.cardsDecryptionShares(this.tableInfo.id, sharesToSend, proofsToSend);
}

private async createAndSharePrivateKeyShares(state: OnChainTableObject) {
console.log("create and share private key shares");
if (!this.zkDeck) throw new Error("zkDeck should be present");
Expand All @@ -181,11 +251,7 @@ export class Jeton extends EventEmitter<GameEventMap> {
.filter((pas) => myPrivateCardsIndexes.includes(pas.cardIndex))
.sort((a, b) => a.cardIndex - b.cardIndex)
.map((pas) => pas.decryptionCardShare) as [DecryptionCardShare, DecryptionCardShare];
this.onChainDataSource.privateCardsDecryptionShares(
this.tableInfo.id,
sharesToSend,
proofsToSend,
);
this.onChainDataSource.cardsDecryptionShares(this.tableInfo.id, sharesToSend, proofsToSend);
}

private async createDecryptionShareProofsFor(indexes: number[], state: OnChainTableObject) {
Expand Down Expand Up @@ -277,8 +343,37 @@ export class Jeton extends EventEmitter<GameEventMap> {
return this.onChainDataSource.queryGameState(this.tableInfo.id);
};

public raiseAmount(round: BettingRounds) {
return [BettingRounds.PRE_FLOP, BettingRounds.FLOP].includes(round)
? this.tableInfo.smallBlind * 2
: this.tableInfo.smallBlind * 4;
}

public placeBet(action: PlacingBettingActions) {
console.log("placeBet Called", action);
console.log("placeBet called", action);
return this.onChainDataSource.Bet(this.tableInfo.id, action);
}

public calculateAvailableActions(tableObject: OnChainTableObject): PlacingBettingActions[] {
try {
const self = onChainDataMapper.convertPlayer(getPlayerByAddress(tableObject, this.playerId)!);
const round = getBettingRound(onChainDataMapper.convertGameStatus(tableObject.state));
if (self.status === PlayerStatus.folded || self.status === PlayerStatus.sittingOut) return [];
const actions = [PlacingBettingActions.FOLD, PlacingBettingActions.CHECK_CALL];
if (self.status === PlayerStatus.allIn) return actions;
const alreadyBettedAmount = self.bet;
if (alreadyBettedAmount == null) throw new Error("should not be undefined");
const maxBet = onChainDataMapper
.convertPlayers(tableObject.roster.players, [])
.reduce((maxBet, player) => (maxBet > Number(player.bet) ? maxBet : Number(player.bet)), 0);
const expectedAmount = maxBet - self.bet! + this.raiseAmount(round);
if (getNumberOfRaisesLeft(tableObject) > 0 && expectedAmount < self.balance) {
actions.push(PlacingBettingActions.RAISE);
}
return actions;
} catch (e) {
return [];
}
}

static async createTableAndJoin(
Expand Down
32 changes: 31 additions & 1 deletion packages/ts-sdk/src/Jeton/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function getCardShares(tableObject: OnChainTableObject, indexes: number |

export function getCardIndexes(tableObject: OnChainTableObject, round: PublicCardRounds) {
const numOfPlayer = tableObject.roster.players.length;
const beforeFirstIndex = numOfPlayer * 2 + 1;
const beforeFirstIndex = numOfPlayer * 2;
if (round === PublicCardRounds.FLOP)
return [beforeFirstIndex + 1, beforeFirstIndex + 2, beforeFirstIndex + 3];
if (round === PublicCardRounds.TURN) return [beforeFirstIndex + 4];
Expand All @@ -61,6 +61,12 @@ export function getPlayerByIndex(tableObject: OnChainTableObject, index: number)
return tableObject.roster.players[index];
}

export function getPlayerByAddress(tableObject: OnChainTableObject, address: string) {
return [...tableObject.roster.players, ...tableObject.roster.waitings].find(
(player) => player.addr === address,
);
}

export function getBettingPlayer(tableObject: OnChainTableObject) {
if (tableObject.state.__variant__ !== "Playing") return null;
if (
Expand All @@ -71,3 +77,27 @@ export function getBettingPlayer(tableObject: OnChainTableObject) {

return null;
}

export function getSmallBlindPlayer(tableObject: OnChainTableObject) {
if (tableObject.state.__variant__ !== "Playing") throw new Error("must exist");
return getPlayerByIndex(tableObject, tableObject.roster.small_index)!;
}

export function getBigBlindPlayer(tableObject: OnChainTableObject) {
if (tableObject.state.__variant__ !== "Playing") throw new Error("must exist");
const numOfPlayers = tableObject.roster.players.length;
return getPlayerByIndex(tableObject, (tableObject.roster.small_index + 1) % numOfPlayers)!;
}

export function getPot(tableObject: OnChainTableObject) {
return tableObject.roster.players.reduce((pot, player) => pot + Number(player.bet._0), 0);
}

export function getNumberOfRaisesLeft(tableObject: OnChainTableObject) {
if (tableObject.state.__variant__ !== "Playing") return 0;
const phase = tableObject.state.phase;
if (!["BetPreFlop", "BetFlop", "BetTurn", "BetRiver"].includes(phase.__variant__)) {
return 0;
}
return (phase as BetPhase).raises_left;
}
Loading

0 comments on commit ecefc06

Please sign in to comment.