Skip to content

Commit

Permalink
Merge pull request #48 from JetonDAO/show-down
Browse files Browse the repository at this point in the history
show down
  • Loading branch information
arssly authored Oct 14, 2024
2 parents 92bad10 + 8b56f56 commit e165a28
Show file tree
Hide file tree
Showing 13 changed files with 180 additions and 43 deletions.
2 changes: 1 addition & 1 deletion apps/web/src/app/games/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export default function PlayPage({ params }: { params: { id: string } }) {
const privateCards = reorderedPlayers.reduce(
(acc, player, seat) => {
if (player) {
acc[seat + 1] = getRandomCards();
acc[seat + 1] = player.cards ?? [];
}
return acc;
},
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/app/games/[id]/state/actions/gameActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
privateCardsDecryptionHandler,
receivedPrivateCardHandler,
receivedPublicCardsHandler,
receivedShowDown,
} from "./gameEventHandlers";

import { decryptCardShareZkey, shuffleEncryptDeckZkey } from "@jeton/zk-deck";
Expand Down Expand Up @@ -66,6 +67,7 @@ function setGameEventListeners(game: Jeton) {
game.addListener?.(GameEventTypes.AWAITING_BET, awaitingPlayerBetHandler);
game.addListener?.(GameEventTypes.PLAYER_PLACED_BET, playerPlacedBetHandler);
game.addListener?.(GameEventTypes.RECEIVED_PUBLIC_CARDS, receivedPublicCardsHandler);
game.addListener?.(GameEventTypes.SHOW_DOWN, receivedShowDown);
console.log("set game event listeners end");
}

Expand Down
36 changes: 35 additions & 1 deletion apps/web/src/app/games/[id]/state/actions/gameEventHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { stat } from "fs";
import {
type AwaitingBetEvent,
GameStatus,
Expand All @@ -9,6 +8,7 @@ import {
getGameStatus,
type playerShufflingEvent,
} from "@jeton/ts-sdk";
import type { ShowDownEvent } from "@jeton/ts-sdk";
import type { ReceivedPublicCardsEvent } from "@jeton/ts-sdk";
import { PublicCardRounds } from "@jeton/ts-sdk";
import { state$ } from "../state";
Expand All @@ -21,12 +21,26 @@ export function newPlayerCheckedInHandler(player: Player) {
}

export function handStartedHandler({ dealer }: HandStartedEvent) {
console.log("hand started handler", dealer);
const gameState$ = state$.gameState;
gameState$.dealer.set(dealer);
gameState$.status.set(GameStatus.Shuffle);
gameState$.players.forEach((player) => {
player.cards.set(undefined);
player.bet.set(0);
player.winAmount.set(undefined);
player.roundAction.set(undefined);
});
gameState$.myCards.set(undefined);
gameState$.flopCards.set(undefined);
gameState$.riverCard.set(undefined);
gameState$.turnCard.set(undefined);
gameState$.pot.set(0);
gameState$.betState.set(undefined);
}

export function playerShufflingHandler(player: playerShufflingEvent) {
console.log("player shuffling handler");
state$.gameState.shufflingPlayer.set(player);
state$.gameState.status.set(GameStatus.Shuffle);
}
Expand Down Expand Up @@ -83,8 +97,15 @@ export function playerPlacedBetHandler({
state$.gameState.betState.availableActions.set(availableActions);
}

export function clearPlayersRoundAction() {
state$.gameState.players.forEach((player) => {
player.roundAction.set(undefined);
});
}

export function receivedPublicCardsHandler({ cards, round }: ReceivedPublicCardsEvent) {
console.log("received public cards", round, cards);
clearPlayersRoundAction();
if (round === PublicCardRounds.FLOP) {
state$.gameState.flopCards.set(cards);
} else if (round === PublicCardRounds.RIVER) {
Expand All @@ -93,3 +114,16 @@ export function receivedPublicCardsHandler({ cards, round }: ReceivedPublicCards
state$.gameState.turnCard.set(cards);
}
}

export function receivedShowDown(data: ShowDownEvent) {
console.log("received showdown event", data);
state$.gameState.players.forEach((player) => {
const eventPlayer = data[player.id.get()!];
if (eventPlayer) {
player.cards.set(eventPlayer.cards);
player.balance.set(eventPlayer.player.balance);
player.bet.set(0);
player.winAmount.set(eventPlayer.winAmount);
}
});
}
4 changes: 3 additions & 1 deletion apps/web/src/app/games/[id]/state/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {
} from "@jeton/ts-sdk";
import { type Observable, observable } from "@legendapp/state";

type UIPlayer = Player & {
export type UIPlayer = Player & {
roundAction?: {
action: BettingActions;
amount: number;
};
cards?: number[];
winAmount?: number;
};

type GameState = {
Expand Down
10 changes: 5 additions & 5 deletions apps/web/src/utils/seat.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import type { Player } from "@jeton/ts-sdk";
import type { UIPlayer } from "@src/app/games/[id]/state/state";

export const orderPlayersSeats = (
players: (Player | null)[],
players: (UIPlayer | null)[],
mainPlayerId: string,
): (Player | null)[] => {
): (UIPlayer | null)[] => {
const mainPlayerIndex = players.findIndex((player) => player?.id === mainPlayerId);

const mainPlayer = players[mainPlayerIndex];
if (!mainPlayer) {
return players;
}

const playersAfterMainPlayer: (Player | null)[] = players.slice(mainPlayerIndex + 1);
const playersBeforeMainPlayer: (Player | null)[] = players.slice(0, mainPlayerIndex);
const playersAfterMainPlayer: (UIPlayer | null)[] = players.slice(mainPlayerIndex + 1);
const playersBeforeMainPlayer: (UIPlayer | null)[] = players.slice(0, mainPlayerIndex);
return [mainPlayer, ...playersAfterMainPlayer, ...playersBeforeMainPlayer];
};
3 changes: 3 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
"useNodejsImportProtocol": "off",
"useEnumInitializers": "off",
"noNonNullAssertion": "off"
},
"complexity": {
"noForEach": "off"
}
}
},
Expand Down
75 changes: 60 additions & 15 deletions packages/ts-sdk/src/Jeton/Jeton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
OnChainEventTypes,
type OnChainPlayerCheckedInData,
type OnChainPlayerPlacedBetData,
type OnChainShowDownData,
type OnChainShuffledDeckData,
type OnChainTableObject,
} from "@src/OnChainDataSource";
Expand All @@ -14,6 +15,7 @@ import onChainDataMapper, { covertTableInfo } from "@src/OnChainDataSource/onCha
import {
BettingActions,
BettingRounds,
type CardShareAndProof,
type ChipUnits,
type GameEventMap,
GameEventTypes,
Expand All @@ -23,6 +25,7 @@ import {
PlayerStatus,
type PublicCardRounds,
type ReceivedPublicCardsEvent,
type ShowDownEvent,
type TableInfo,
} from "@src/types";
import { type PendingMemo, createPendingMemo } from "@src/utils/PendingMemo";
Expand All @@ -37,7 +40,6 @@ import {
getDeck,
getNumberOfRaisesLeft,
getPlayerByAddress,
getPlayerByIndex,
getPot,
getPrivateCardsIndexes,
getShufflingPlayer,
Expand Down Expand Up @@ -68,7 +70,7 @@ export class Jeton extends EventEmitter<GameEventMap> {
private secretKey: bigint;
private publicKey: Uint8Array;
public gameState?: JGameState;
private myPrivateCardsShares?: [DecryptionCardShare, DecryptionCardShare];
private myPrivateCardsShares?: [CardShareAndProof, CardShareAndProof];
private pendingMemo: PendingMemo;

constructor(config: JetonConfigs) {
Expand Down Expand Up @@ -107,9 +109,31 @@ export class Jeton extends EventEmitter<GameEventMap> {
this.onChainDataSource.on(OnChainEventTypes.SHUFFLED_DECK, this.playerShuffledDeck);
this.onChainDataSource.on(OnChainEventTypes.CARDS_SHARES_RECEIVED, this.receivedCardsShares);
this.onChainDataSource.on(OnChainEventTypes.PLAYER_PLACED_BET, this.receivedPlayerBet);
this.onChainDataSource.on(OnChainEventTypes.SHOW_DOWN, this.receivedShowDown);
this.onChainDataSource.listenToTableEvents(this.tableInfo.id);
}

private receivedShowDown = async (data: OnChainShowDownData) => {
if (!this.gameState) throw new Error(" must exist");
const players = this.gameState.players;
const eventData: ShowDownEvent = {};
for (const [index, player] of players.entries()) {
eventData[player.id] = {
player,
winAmount: data.winningAmounts[index]!,
cards: [data.privateCards[index * 2]!, data.privateCards[index * 2 + 1]!],
};
}
this.emit(GameEventTypes.SHOW_DOWN, eventData);

const onChainTableObject = await this.pendingMemo.memoize(this.queryGameState);
this.gameState = onChainDataMapper.convertJetonState(onChainTableObject);
// TODO: timeout is not a good solution
setTimeout(() => {
this.gameStarted(onChainTableObject);
}, 3000);
};

private receivedPlayerBet = async (data: OnChainPlayerPlacedBetData) => {
console.log("received Player bet", data);
const onChainTableObject = await this.pendingMemo.memoize(this.queryGameState);
Expand Down Expand Up @@ -147,7 +171,7 @@ export class Jeton extends EventEmitter<GameEventMap> {
return;
}
if (nextPlayer === null) {
//TODO: showdown
this.createAndShareSelfPrivateKeyShares(onChainTableObject);
return;
}
// don't send awaiting bet to ui for small and big blind
Expand Down Expand Up @@ -199,20 +223,22 @@ export class Jeton extends EventEmitter<GameEventMap> {

// if the new player is active player then the game has just started
if (newPlayer.status !== PlayerStatus.sittingOut) {
const dealerIndex = newJState.dealerIndex;
this.emit(GameEventTypes.HAND_STARTED, { dealer: newJState.players[dealerIndex]! });
const shufflingPlayer = getShufflingPlayer(onChainTableObject);
if (shufflingPlayer)
this.emit(
GameEventTypes.PLAYER_SHUFFLING,
onChainDataMapper.convertPlayer(shufflingPlayer),
);
this.checkForActions(onChainTableObject);
this.gameStarted(onChainTableObject);
}

this.gameState = newJState;
};

private gameStarted(state: OnChainTableObject) {
const newJState = onChainDataMapper.convertJetonState(state);
const dealerIndex = newJState.dealerIndex;
this.emit(GameEventTypes.HAND_STARTED, { dealer: newJState.players[dealerIndex]! });
const shufflingPlayer = getShufflingPlayer(state);
if (shufflingPlayer)
this.emit(GameEventTypes.PLAYER_SHUFFLING, onChainDataMapper.convertPlayer(shufflingPlayer));
this.checkForActions(state);
}

private playerShuffledDeck = async (data: OnChainShuffledDeckData) => {
console.log("player shuffled deck");
const onChainTableObject = await this.pendingMemo.memoize(this.queryGameState);
Expand Down Expand Up @@ -260,7 +286,20 @@ export class Jeton extends EventEmitter<GameEventMap> {
this.myPrivateCardsShares = proofsAndShares
.filter((pas) => myPrivateCardsIndexes.includes(pas.cardIndex))
.sort((a, b) => a.cardIndex - b.cardIndex)
.map((pas) => pas.decryptionCardShare) as [DecryptionCardShare, DecryptionCardShare];
.map((pas) => pas) as [CardShareAndProof, CardShareAndProof];
this.onChainDataSource.cardsDecryptionShares(this.tableInfo.id, sharesToSend, proofsToSend);
}

private async createAndShareSelfPrivateKeyShares(state: OnChainTableObject) {
if (!this.myPrivateCardsShares) {
this.myPrivateCardsShares = (await this.createDecryptionShareProofsFor(
getPrivateCardsIndexes(state, this.playerId),
state,
)) as [CardShareAndProof, CardShareAndProof];
}

const proofsToSend = this.myPrivateCardsShares.map((pas) => pas.proof);
const sharesToSend = this.myPrivateCardsShares.map((pas) => pas.decryptionCardShare);
this.onChainDataSource.cardsDecryptionShares(this.tableInfo.id, sharesToSend, proofsToSend);
}

Expand Down Expand Up @@ -303,8 +342,14 @@ export class Jeton extends EventEmitter<GameEventMap> {

if (!this.myPrivateCardsShares) throw new Error("must be available");
const [firstCardIndex, secondCardIndex] = getPrivateCardsIndexes(state, this.playerId);
const firstCardShares = [getCardShares(state, firstCardIndex), this.myPrivateCardsShares[0]];
const secondCardShares = [getCardShares(state, secondCardIndex), this.myPrivateCardsShares[1]];
const firstCardShares = [
getCardShares(state, firstCardIndex),
this.myPrivateCardsShares[0].decryptionCardShare,
];
const secondCardShares = [
getCardShares(state, secondCardIndex),
this.myPrivateCardsShares[1].decryptionCardShare,
];
const firstPrivateCard = this.zkDeck.decryptCard(firstCardIndex, deck, firstCardShares);
const secondPrivateCard = this.zkDeck.decryptCard(secondCardIndex, deck, secondCardShares);

Expand Down
7 changes: 5 additions & 2 deletions packages/ts-sdk/src/Jeton/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@ export function isActivePlayer(tableObject: OnChainTableObject, playerId: string
return !!tableObject.roster.players.find((p) => p.addr === playerId);
}

export function getPrivateCardsIndexes(tableObject: OnChainTableObject, playerId: string) {
export function getPrivateCardsIndexes(
tableObject: OnChainTableObject,
playerId: string,
): [number, number] {
const playerPosition = tableObject.roster.players.findIndex((p) => p.addr === playerId);
if (playerPosition === -1) throw new Error("player does not exist");
return [playerPosition * 2, playerPosition * 2 + 1] as const;
return [playerPosition * 2, playerPosition * 2 + 1];
}

export function getDeck(tableObject: OnChainTableObject) {
Expand Down
16 changes: 14 additions & 2 deletions packages/ts-sdk/src/OnChainDataSource/AptosOnChainDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@ import {
contractCheckedInEventType,
contractFoldEventType,
contractRaiseEventType,
contractShowDownEventType,
contractShuffleEventType,
contractSmallBlindEventType,
} from "@src/contracts/contractData";
import { createTableInfo } from "@src/contracts/contractDataMapper";
import {
type ChainEvents,
type ShowDownEvent,
callCallContract,
callCheckInContract,
callDecryptCardShares,
Expand Down Expand Up @@ -49,7 +52,7 @@ export class AptosOnChainDataSource
this.address = address;
}

private publishEvent(event: { data: { sender_addr: string }; indexed_type: string }) {
private publishEvent(event: ChainEvents) {
console.log("publish event", event);
switch (event.indexed_type) {
case contractCheckedInEventType:
Expand Down Expand Up @@ -99,6 +102,15 @@ export class AptosOnChainDataSource
address: event.data.sender_addr,
});
break;
case contractShowDownEventType: {
console.log("publishing", OnChainEventTypes.SHOW_DOWN);
const data = (event as ShowDownEvent).data;
this.emit(OnChainEventTypes.SHOW_DOWN, {
publicCards: data.public_cards.map((p) => Number(p)),
privateCards: data.private_cards.map((p) => Number(p)),
winningAmounts: data.winning_amounts.map((a) => Number(a)),
});
}
}
}

Expand All @@ -117,7 +129,7 @@ export class AptosOnChainDataSource
// TODO: parse events and emit
const pollingTable = this.pollingTables[tableId];
if (pollingTable) {
pollingTable.lastEventBlockHeight = events[0].transaction_block_height;
pollingTable.lastEventBlockHeight = events[0]!.transaction_block_height;
pollingTable.timerId = setTimeout(this.pollTableEvents.bind(this, tableId), POLLING_INTERVAL);
}
};
Expand Down
10 changes: 9 additions & 1 deletion packages/ts-sdk/src/OnChainDataSource/onChainEvents.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import type { BettingActions, BettingRounds, PublicCardRounds } from "../types";
export enum OnChainEventTypes {
PLAYER_CHECKED_IN = "player-checked-in",
SHUFFLED_DECK = "shuffled-deck",
CARDS_SHARES_RECEIVED = "private-cards-shares",
CARDS_SHARES_RECEIVED = "cards-shares",
PLAYER_PLACED_BET = "player-placed-bet",
SHOW_DOWN = "show-down",
}

export type OnChainPlayerCheckedInData = {
Expand All @@ -24,9 +25,16 @@ export type OnChainPlayerPlacedBetData = {
address?: string;
};

export type OnChainShowDownData = {
privateCards: number[];
publicCards: number[];
winningAmounts: number[];
};

export type OnChainEventMap = {
[OnChainEventTypes.PLAYER_CHECKED_IN]: [OnChainPlayerCheckedInData];
[OnChainEventTypes.SHUFFLED_DECK]: [OnChainShuffledDeckData];
[OnChainEventTypes.CARDS_SHARES_RECEIVED]: [OnChainCardsSharesData];
[OnChainEventTypes.PLAYER_PLACED_BET]: [OnChainPlayerPlacedBetData];
[OnChainEventTypes.SHOW_DOWN]: [OnChainShowDownData];
};
Loading

0 comments on commit e165a28

Please sign in to comment.