import { Client, Room } from 'colyseus'; import { Dispatcher } from '@colyseus/command'; import { createNanoEvents } from 'nanoevents'; import { Player as CardPlayer, Hand } from 'typedeck'; import { GameState, Player, CardPlaceholder, SerializedCard, CardValue, PromptButton, PromptField, Prompt, } from '../games/state'; import { SetUserStashCommand, AddToUserStashCommand, RemoveFromUserStashCommand, GiveRoomOwnershipCommand, AssignSeatCommand, StartGameCommand, PauseGameCommand } from './commands/admin'; import { CartaNapoletana, SettemmezzoGameType } from '../games/banco'; import { assert } from 'console'; interface OneOffEvent { [action: string]: (client: Client, message: any) => void } interface PlayerAction { action: string bet: number } export class GameRoom extends Room { private dispatcher = new Dispatcher(this); private messageHandlers = createNanoEvents(); private validateSession; private gameType = new SettemmezzoGameType(); private deck; private currentDealer: number = -1; sleep = (milliseconds: number = 1000): Promise => { return new Promise(resolve => { this.clock.setTimeout(resolve, milliseconds); }); } getSeatedPlayers(): Player[] { if (this.state === undefined) { return []; } const players = Array.from(this.state.players.values()); return players.filter((p) => { return (p.playing) }); } getPlayerBySeat(n: number): Player { const players = Array.from(this.state.players.values()); return players.find((p) => { return (p.playing && p.seat === n) }); } pMap(callbackFn: (value: Player, index: number, array: Player[]) => Player, thisArg?: any): Player[] { const players = Array.from(this.state.players.values()); return players.map(callbackFn); } setDealer(n: number) { this.pMap((player) => { player.dealer = player.seat === n; return player; }); } nextAvailableSeat = (): number => { const playing = this.getSeatedPlayers(); return Math.max(...playing.map((p) => { return p.seat }), 0) + 1; } nextDealer = (): number => { const players = this.getSeatedPlayers(); this.currentDealer = (this.currentDealer + 1) % players.length; const dealerSeat = players[this.currentDealer].seat; console.log('Setting dealer seat:', dealerSeat); this.setDealer(dealerSeat); return dealerSeat; } *nextPlayerGenerator(dealer: Player) { const players = this.getSeatedPlayers(); const dealerIndex = players.indexOf(dealer); let n = 1; while (true) { let p: Player = yield players[(dealerIndex + n) % players.length]; n += 1; } } shuffleCards = () => { console.log('Hard shuffling cards!'); this.gameType = new SettemmezzoGameType(); this.deck = this.gameType.createDeck(); this.deck.shuffle(); this.state.remainingCards = 40; } dealCard(player: Player, faceUp: boolean = false) { const cardPlayer = new CardPlayer(player.displayName, new Hand()); this.deck.deal(cardPlayer.getHand(), 1); this.state.remainingCards -= 1; const [card] = cardPlayer.getHand().takeCardsFromBottom(1); console.log('Card dealt:', card, 'remaining:', this.state.remainingCards); if (card === undefined) { // FIXME: why this ever happens? console.warn('Card was undefined, deck:', this.deck); return; } const cardValue = new CardValue({ value: card.cardName, suit: card.suit }); const serializedCard = new SerializedCard({ owner: player.sessionId, public: faceUp, card: cardValue }); player.hand.push(new CardPlaceholder({ card: serializedCard })); } publishCards = (player: Player) => { player.hand.forEach(pc => { pc.card.public = true; }); } tossAllHands = () => { this.pMap((player) => { player.hand = player.hand.filter((pc) => { return false }); player.prompt = new Prompt(); return player; }); } getPlayerAction(player: Player): Promise { return new Promise(resolve => { const unbind = this.messageHandlers.on('resolve-prompt', (client, message) => { if (player.sessionId !== client.sessionId) { console.log('Waiting for a different sessionId:', player.sessionId, client.sessionId); return; } unbind(); console.log('Got player response:', message); resolve(message); }); }); } hasJolly = (hand: CardPlaceholder[]): boolean => { return undefined !== hand.find((pc) => { const value = pc.card.card.value; const suit = pc.card.card.suit; return value === 9 && suit === 3; }); } calculateScore = (hand: CardPlaceholder[]) => { let score = hand.reduce((accumulator, currentValue) => { const cardValue = currentValue.card.card.value; const cardSuit = currentValue.card.card.suit; if (cardValue == 9 && cardSuit == 3) { return accumulator + 0; } const v = (cardValue <= 6) ? cardValue + 1 : 0.5; return accumulator + v; }, 0); const withJolly = this.hasJolly(hand); if (withJolly) { score += Math.floor(7.5 - score); } return { score, withJolly }; } playHand = async (player: Player, placeBet: boolean) => { let playerLoose = false; let moreCards = true; let betPlaced = false; let score; const prompt = player.prompt = new Prompt(); while (moreCards && !playerLoose) { prompt.fields.splice(0, prompt.fields.length); prompt.buttons.splice(0, prompt.buttons.length); if (placeBet && !betPlaced) { prompt.fields.push(new PromptField({ name: 'bet', label: 'Quanto scommetti', min: this.state.minimumBet, max: Math.min(this.state.pot, player.stash), value: this.state.minimumBet })); } prompt.buttons.push(new PromptButton({ name: 'stand', label: 'Sto bene', type: 'primary' })); prompt.buttons.push(new PromptButton({ name: 'more', label: 'Carta', type: 'primary' })); // se é un quattro servito, offri la possibilitá di "bruciare" const lucio = player.hand.find((c) => { return c.card.card.value === 3 }); if (player.hand.length == 1 && lucio !== undefined) { console.log('LUCIO:', lucio, lucio.card); prompt.buttons.push(new PromptButton({ name: 'discard', label: 'Brucia!', type: 'primary' })); } prompt.visible = true; const choice = await this.getPlayerAction(player); console.log('Player choice was:', choice); if (choice.action === 'discard') { // mostra la carta this.publishCards(player); this.broadcast('notification', { type: 'info', text: `${player.displayName} brucia il quattro` }); // aspetta un paio di secondi await this.sleep(2000); // dunque svuota la mano e serve un'altra carta coperta: player.hand.pop(); this.dealCard(player, false); } else { moreCards = false; } if (choice.hasOwnProperty('bet') && choice.action !== 'discard') { player.stash -= choice.bet; this.state.currentBet += choice.bet; betPlaced = true; } if (choice.action === 'more') { this.dealCard(player, true); moreCards = true; } // il giocatore ha sballato? -> perde e passa il turno score = this.calculateScore(player.hand); console.log('Score:', score); player.score = score.score; if (score.score > 7.5) { console.log('Hai sballato fraté!'); playerLoose = true; this.publishCards(player); this.broadcast('notification', { type: 'error', text: `${player.displayName} ha sballato` }); player.prompt.visible = false; await this.sleep(3000); } else if (score.score == 7.5) { console.log('Sette e mezzo!!!!'); this.publishCards(player); moreCards = false; player.prompt.visible = false; } } prompt.visible = false; return { score, playerLoose }; } pauseGameIfPlayersDontMeetRequirements = async (): Promise => { const poorPlayers = this.getSeatedPlayers().filter((player) => { return player.stash < this.state.minimumBet * 2; }); if (poorPlayers.length > 0) { console.log('Pausing game since not all players meet requirements'); this.state.paused = true; poorPlayers.forEach((player) => { this.broadcast('notification', { type: 'warning', text: `${player.displayName} non ha abbastanza soldi per giocare` }); }); return new Promise(resolve => { const unbind = this.messageHandlers.on('resume-game', (client, message) => { console.log('RESUME GAME?'); const poorPlayers = this.getSeatedPlayers().filter((player) => { return player.stash < this.state.minimumBet * 2; }); if (poorPlayers.length > 0) { this.broadcast('notification', { type: 'warning', text: `Tutti i giocatori al tavolo devono avere almeno ${this.state.minimumBet * 2} per riprendere la partita` }); console.log('not resuming'); return; } console.log('resuming'); this.state.paused = false; unbind(); resolve(); }); }); } else { return Promise.resolve(); } } async onStartGame() { console.log('Game started'); while (this.state.running) { await this.pauseGameIfPlayersDontMeetRequirements(); // Letting in players waiting this.pMap((player) => { if (player.enteringNextTurn) { const seat = this.nextAvailableSeat(); console.log('assigning seat to user:', player.displayName, seat); player.enteringNextTurn = false; player.seat = seat; player.playing = true; }; return player; }) // collect the initial bets this.getSeatedPlayers().map((player) => { player.stash -= this.state.minimumBet; this.state.pot += this.state.minimumBet; return player; }); // shuffle cards this.shuffleCards(); this.broadcast('notification', { type: 'info', text: 'Le carte sono state mischiate perché il mazzo passa ad un nuovo mazziere' }); // set the dealer const dealerSeat = this.nextDealer(); this.state.player1 = this.getPlayerBySeat(dealerSeat); console.log('Dealer player is:', this.state.player1.displayName); this.broadcast('notification', { type: 'info', text: `${this.state.player1.displayName} é il nuovo banco` }); // await this.sleep(3000); const table = this.nextPlayerGenerator(this.state.player1); while (this.state.pot > 0) { const p: Player = table.next().value; if (p === null) { throw Error('nextPlayerGenerator didn\'t return a valid Player instance'); } if (p === this.state.player1) { const prompt = this.state.player1.prompt = new Prompt(); prompt.buttons.push(new PromptButton({ name: 'cash-out', label: 'Prendi il piatto', type: 'primary' })); prompt.buttons.push(new PromptButton({ name: 'another-round', label: 'Ancora un altro giro', type: 'secondary' })); prompt.visible = true; const choice = await this.getPlayerAction(this.state.player1); this.state.player1.prompt = new Prompt(); console.log('Player choice was:', choice); if (choice.action === 'cash-out') { this.broadcast('notification', { type: 'info', text: `Il banco (${this.state.player1.displayName}) prende ${this.state.pot} del piatto e passa la mano` }); this.state.player1.stash += this.state.pot; this.state.pot = 0; await this.sleep(3000); break; } else { this.broadcast('notification', { type: 'info', text: `Il banco (${this.state.player1.displayName}) ha deciso per un altro giro` }); await this.sleep(3000); continue; } } await this.pauseGameIfPlayersDontMeetRequirements(); this.state.player2 = p; console.log('Challenger player is:', this.state.player2.displayName); // Deal draft cards this.dealCard(this.state.player2, false); this.state.player2.score = this.calculateScore(this.state.player2.hand).score; this.dealCard(this.state.player1, false); this.state.player1.score = this.calculateScore(this.state.player1.hand).score // Let challenger play his hand let challengerWins = false; const challengerResult = await this.playHand(this.state.player2, true); console.log('Challenger:', challengerResult);; this.publishCards(this.state.player1); if (!challengerResult.playerLoose) { const dealerResult = await this.playHand(this.state.player1, false); console.log('Dealer:', dealerResult); this.publishCards(this.state.player2); if (!dealerResult.playerLoose) { challengerWins = challengerResult.score.score > dealerResult.score.score; } else { challengerWins = true; } } else { challengerWins = false; console.log('Dealer wins!'); } await this.sleep(5000); const jollyWasDealt = this.hasJolly(this.state.player1.hand) || this.hasJolly(this.state.player2.hand); console.log('La matta é stata giocata? ', jollyWasDealt); if (challengerWins) { this.broadcast('notification', { type: 'success', text: `${this.state.player2.displayName} vince (${this.state.currentBet}) dal piatto` }); this.state.player2.stash += this.state.currentBet * 2; this.state.pot -= this.state.currentBet; assert(this.state.pot >= 0); await this.sleep(3000); } else { this.broadcast('notification', { type: 'success', text: `Il banco (${this.state.player1.displayName}) vince la mano` }); this.state.pot += this.state.currentBet; } this.state.currentBet = 0; // svuota le mani this.tossAllHands(); if (jollyWasDealt) { console.log('Shuffling deck since jolly was dealt in last hand'); this.shuffleCards(); this.broadcast('notification', { type: 'info', text: 'Le carte sono state mischiate perché la matta é stata estratta nell\'ultima mano' }); await this.sleep(3000); } } } } onAuth(client, options, request) { console.log('onauth', options, request.session.user_id); const user = this.validateSession(request.session.user_id); console.log('Found user:', user); return user; } onCreate(options) { this.deck = this.gameType.createDeck(); this.deck.shuffle(); console.log('Deck created:', this.deck); console.log('GameRoom instance created', this.roomId, options); this.validateSession = options.validateSession; this.setState(new GameState()); this.state.roomOwner = options.user_id; this.onMessage('admin', (client, message) => this.onAdminCommand(client, message)); this.onMessage('webrtc', (client, message) => this.onWebRTCCommand(client, message)); this.onMessage('*', (client, type: string, message) => { console.log('Relaying message:', type, message); this.messageHandlers.emit(type, client, message); }); this.clock.start(); // DEMO: rotates the background every 60 seconds this.clock.setInterval(() => { this.state.bg = 1 + (this.state.bg + 1) % 5; }, 60000); } onJoin(client, options, user) { console.log(`User ${user.display_name} (${user.user_id}) joined room ${this.roomId}`, options); if (this.state.players.size === 0) { console.log(`User ${user.display_name} (${user.user_id}) set as room ${this.roomId} owner`); this.state.roomOwner = user.user_id; } const user_already_logged_in = Array.from(this.state.players.values()).find((item) => item.id === user.user_id); if (user_already_logged_in) { console.log('User was already in the game!', user_already_logged_in); return; } this.state.players.set(client.sessionId, new Player({ id: user.user_id, sessionId: client.sessionId, displayName: user.display_name, owner: user.user_id === this.state.roomOwner, stash: 0, connected: true, playing: false, dealer: false, hand: [] })); } async onLeave(client, consented) { console.log('Player leaving:', client.sessionId); if (this.state.players.has(client.sessionId)) { this.state.players[client.sessionId].connected = false; } try { if (consented) { throw new Error("consented leave"); } // allow disconnected client to reconnect into this room until 60 seconds await this.allowReconnection(client, 60); // client returned! let's re-activate it. this.state.players[client.sessionId].connected = true; } catch (e) { // 60 seconds expired. let's remove the client. this.state.players.delete(client.sessionId); // explicitly broadcasting the event to let WebRTC cleanup corectly this.broadcast('player-left', client.sessionId); } } onAdminCommand(client, message) { console.log('Admin command received:', message); const player = this.state.players.get(client.sessionId); if (player.id !== this.state.roomOwner) { console.warn('Received admin command from non-owner user:', client.sessionId, message); return; } const admin_commands = { // set user stash (session_id, amount) 'set-user-stash': SetUserStashCommand, // add to user stash (session_id, amount) 'add-to-user-stash': AddToUserStashCommand, // remove from user stash (session_id, amount) 'remove-from-user-stash': RemoveFromUserStashCommand, // assign seat to player (session_id) 'assign-seat': AssignSeatCommand, // demote player (session_id) // shuffle seats // give room ownership (session_id) 'give-room-ownership': GiveRoomOwnershipCommand, // start game 'start-game': StartGameCommand, // pause game 'pause-game': PauseGameCommand, // set room name (new name) // set room background (background) }; if (!admin_commands.hasOwnProperty(message.command)) { console.error("Invalid admin command", message); return; } const commandClass = admin_commands[message.command]; this.dispatcher.dispatch(new commandClass(), { ...message.payload, client }); } onWebRTCCommand(client, message) { console.log('WebRTC command received:', message); const player = this.clients.find((value) => { return value.sessionId === message.payload.targetId }); if (player === null) { console.warn('Cannot find client with sessionId', message.payload.targetId); return; } if (message.command === 'call-player') { console.log('Relaying WebRTC call:', message.payload); if (player === undefined) { console.warn('Incoming call to an unknown player'); return; } player.send('incoming-call', message.payload); } if (message.command === 'answer-call') { console.log('Relaying WebRTC call:', message.payload); player.send('answer-call', message.payload); } } }