Adding a simple README

This commit is contained in:
2021-01-29 15:55:07 +01:00
parent 5e020810ae
commit f75c46d877
50 changed files with 15 additions and 0 deletions

17
server/TODO.org Normal file
View File

@@ -0,0 +1,17 @@
* TODO Implementa il muto/audio on
* TODO Implementa il video on/off
* TODO Stilizza i video
* TODO Stilizza i player offline
* TODO Controlla il bug quando sballi in presenza di matta
* TODO ridisegnare i game controls in un drawer
* TODO Investigate current player hand and prompt (move it to the context)
* TODO Implementare il full-screen (non funziona!)
* DONE Controlla meglio che i conti tornino sempre!
* DONE disegnare la :currentBet:
* DONE marcare il dealer
* DONE evidenziare il giocatore di turno
* DONE Brucia i quattro!
* DONE Integra la videochat

100
server/games/banco.ts Normal file
View File

@@ -0,0 +1,100 @@
import { BaseGameType, CardName, ICard, PlayingCard, RankSet, Suit } from 'typedeck';
enum SemeNapoletano {
Bastoni, // Suit.Clubs
Spade, // Suit.Spades
Coppe, // Suit.Diamonds
Denari // Suit.Hearts
};
export class CartaNapoletana extends PlayingCard {
public toString(): string {
return `${CardName[this.cardName]} of ${SemeNapoletano[this.suit]}`;
}
}
class NapoliGameType extends BaseGameType {
public cardsAllowed: ICard[] = [
new CartaNapoletana(CardName.Ace, Suit.Clubs),
new CartaNapoletana(CardName.Two, Suit.Clubs),
new CartaNapoletana(CardName.Three, Suit.Clubs),
new CartaNapoletana(CardName.Four, Suit.Clubs),
new CartaNapoletana(CardName.Five, Suit.Clubs),
new CartaNapoletana(CardName.Six, Suit.Clubs),
new CartaNapoletana(CardName.Seven, Suit.Clubs),
new CartaNapoletana(CardName.Eight, Suit.Clubs),
new CartaNapoletana(CardName.Nine, Suit.Clubs),
new CartaNapoletana(CardName.Ten, Suit.Clubs),
new CartaNapoletana(CardName.Ace, Suit.Diamonds),
new CartaNapoletana(CardName.Two, Suit.Diamonds),
new CartaNapoletana(CardName.Three, Suit.Diamonds),
new CartaNapoletana(CardName.Four, Suit.Diamonds),
new CartaNapoletana(CardName.Five, Suit.Diamonds),
new CartaNapoletana(CardName.Six, Suit.Diamonds),
new CartaNapoletana(CardName.Seven, Suit.Diamonds),
new CartaNapoletana(CardName.Eight, Suit.Diamonds),
new CartaNapoletana(CardName.Nine, Suit.Diamonds),
new CartaNapoletana(CardName.Ten, Suit.Diamonds),
new CartaNapoletana(CardName.Ace, Suit.Spades),
new CartaNapoletana(CardName.Two, Suit.Spades),
new CartaNapoletana(CardName.Three, Suit.Spades),
new CartaNapoletana(CardName.Four, Suit.Spades),
new CartaNapoletana(CardName.Five, Suit.Spades),
new CartaNapoletana(CardName.Six, Suit.Spades),
new CartaNapoletana(CardName.Seven, Suit.Spades),
new CartaNapoletana(CardName.Eight, Suit.Spades),
new CartaNapoletana(CardName.Nine, Suit.Spades),
new CartaNapoletana(CardName.Ten, Suit.Spades),
new CartaNapoletana(CardName.Ace, Suit.Hearts),
new CartaNapoletana(CardName.Two, Suit.Hearts),
new CartaNapoletana(CardName.Three, Suit.Hearts),
new CartaNapoletana(CardName.Four, Suit.Hearts),
new CartaNapoletana(CardName.Five, Suit.Hearts),
new CartaNapoletana(CardName.Six, Suit.Hearts),
new CartaNapoletana(CardName.Seven, Suit.Hearts),
new CartaNapoletana(CardName.Eight, Suit.Hearts),
new CartaNapoletana(CardName.Nine, Suit.Hearts),
new CartaNapoletana(CardName.Ten, Suit.Hearts)
];
}
class BancoRankSet extends RankSet {
public rankSet: CardName[] = [
CardName.Ace,
CardName.Two,
CardName.Three,
CardName.Four,
CardName.Five,
CardName.Six,
CardName.Seven,
CardName.Eight,
CardName.Nine,
CardName.Ten
];
}
export class BancoGameType extends NapoliGameType {
public rankSet = new BancoRankSet();
}
class SettemmezzoRankSet extends RankSet {
public rankSet: CardName[] = [
CardName.Eight,
CardName.Nine,
CardName.Ten,
CardName.Ace,
CardName.Two,
CardName.Three,
CardName.Four,
CardName.Five,
CardName.Six,
CardName.Seven
];
}
export class SettemmezzoGameType extends NapoliGameType {
public rankSet = new SettemmezzoRankSet();
}

159
server/games/state.ts Normal file
View File

@@ -0,0 +1,159 @@
import { Client } from 'colyseus';
import { type, filter, ArraySchema, Schema, MapSchema } from '@colyseus/schema';
export class CardValue extends Schema {
@type('uint8')
value: number;
@type('uint8')
suit: number;
}
export class SerializedCard extends Schema {
@type('string')
owner: string;
@type('boolean')
public: boolean;
@type(CardValue)
card: CardValue;
}
export class CardPlaceholder extends Schema {
@filter(function (
this: CardPlaceholder,
client: Client,
value: SerializedCard,
root: Schema) {
return value.public || value.owner === client.sessionId;
})
@type(SerializedCard)
card: SerializedCard;
}
export class PromptField extends Schema {
@type('string')
type: string;
@type('string')
name: string;
@type('string')
label: string;
@type('int32')
min: string;
@type('int32')
max: string;
}
export class PromptButton extends Schema {
@type('string')
name: string;
@type('string')
label: string;
@type('string')
type: string;
}
export class Prompt extends Schema {
@type('boolean')
visible: boolean;
@type([PromptField])
fields = new ArraySchema<PromptField>();
@type([PromptButton])
buttons = new ArraySchema<PromptButton>();
}
export class Player extends Schema {
@type('string')
id: string;
@type('string')
sessionId: string;
@type('string')
displayName: string;
@type('boolean')
owner: boolean;
@type('int32')
stash: number = 60;
@type('int32')
bet: number = 0;
@type('boolean')
connected: boolean;
@type('boolean')
playing: boolean;
@type('boolean')
enteringNextTurn: boolean;
@type('uint8')
seat: number;
@type('boolean')
dealer: boolean;
@type([CardPlaceholder])
hand = new ArraySchema<CardPlaceholder>();
@type(Prompt)
prompt: Prompt;
@filter(function (
this: Player,
client: Client,
value: string,
root: Schema) {
return this.sessionId === client.sessionId;
})
@type('number')
score: number = 0;
}
export class GameState extends Schema {
@type('boolean')
running: boolean = false;
@type('boolean')
paused: boolean = false;
@type('int32')
pot: number = 0;
@type('int32')
minimumBet: number = 10;
@type({ map: Player })
players = new MapSchema<Player>();
@type('number')
bg: number = 1;
@type('string')
roomOwner: string;
@type(Player)
player1: Player;
@type(Player)
player2: Player;
@type('number')
currentBet: number = 0;
@type('number')
remainingCards: number;
}

32
server/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "games-party",
"version": "1.0.0",
"description": "A WebRTC playroom",
"main": "index.js",
"scripts": {
"devel": "nodemon server.ts"
},
"author": "Domenico Testa",
"license": "MIT",
"dependencies": {
"@colyseus/command": "^0.1.6",
"@types/express": "^4.17.9",
"@types/express-session": "^1.17.3",
"@types/jest": "^26.0.19",
"@types/node": "^14.14.19",
"colyseus": "^0.14.6",
"ejs": "^3.1.5",
"express": "^4.17.1",
"express-session": "^1.17.1",
"nanoevents": "^5.1.10",
"peer": "^0.6.1",
"socket.io": "^3.0.4",
"ts-node": "^9.1.1",
"typedeck": "^1.5.2",
"typescript": "^4.1.3",
"uuid": "^8.3.2"
},
"devDependencies": {
"nodemon": "^2.0.6"
}
}

74
server/public/app.js Normal file
View File

@@ -0,0 +1,74 @@
const socket = io('/')
const videoGrid = document.getElementById('video-grid')
const myPeer = new Peer(undefined, {
host: location.hostname,
path: '/peerjs/domingo'
})
const myVideo = document.createElement('video')
myVideo.muted = true
myVideo.setAttribute("playsinline", true);
const peers = {}
var facingMode = "user";
navigator.mediaDevices.getUserMedia({
video: {
facingMode: facingMode
},
audio: true // TODO: set to true
}).then(stream => {
addVideoStream(myVideo, stream)
myPeer.on('call', call => {
call.answer(stream)
const video = document.createElement('video')
video.setAttribute("playsinline", true);
call.on('stream', userVideoStream => {
addVideoStream(video, userVideoStream)
})
})
socket.on('user-connected', userId => {
connectToNewUser(userId, stream)
})
})
socket.on('user-disconnected', userId => {
console.log("User disconnected: ", userId)
if (peers[userId]) peers[userId].close()
})
myPeer.on('open', id => {
socket.emit('join-room', ROOM_ID, id)
})
socket.on('user-connected', userId => {
console.log('User connected: ', userId)
})
function addVideoStream(video, stream) {
video.srcObject = stream
video.addEventListener('loadedmetadata', () => {
video.play()
})
videoGrid.append(video)
}
function connectToNewUser(userId, stream) {
const call = myPeer.call(userId, stream)
const video = document.createElement('video')
video.setAttribute("playsinline", true);
call.on('stream', userVideoStream => {
addVideoStream(video, userVideoStream)
})
call.on('close', () => {
video.remove()
})
peers[userId] = call
}

View File

@@ -0,0 +1,91 @@
import { Client } from 'colyseus';
import { Command } from '@colyseus/command';
import { GameState } from '../../games/state';
import { GameRoom } from '../game';
export class SetUserStashCommand extends Command<GameState, { client: Client, sessionId: string, amount: number }> {
execute({ client, sessionId, amount }) {
this.state.players[sessionId].stash = amount;
const admin = this.state.players.get(client.sessionId);
const targetClient = this.room.clients.find((client) => { return client.sessionId === sessionId });
targetClient.send('notification', { text: `${admin.displayName} ha impostato il tuo stash a ${amount}`, type: 'success' });
}
}
export class AddToUserStashCommand extends Command<GameState, { client: Client, sessionId: string, amount: number }> {
execute({ client, sessionId, amount }) {
console.log('add-to-user-stash', client, sessionId, amount);
this.state.players[sessionId].stash = this.state.players[sessionId].stash + amount;
const admin = this.state.players.get(client.sessionId);
const targetClient = this.room.clients.find((client) => { return client.sessionId === sessionId });
targetClient.send('notification', { text: `${admin.displayName} ha aggiunto ${amount} al tuo stash`, type: 'success' });
}
}
export class RemoveFromUserStashCommand extends Command<GameState, { client: Client, sessionId: string, amount: number }> {
execute({ client, sessionId, amount }) {
const admin = this.state.players.get(client.sessionId);
const targetClient = this.room.clients.find((client) => { return client.sessionId === sessionId });
targetClient.send('notification', { text: `${admin.displayName} ha rimosso ${amount} dal tuo stash`, type: 'warning' });
this.state.players[sessionId].stash = Math.max(0, this.state.players[sessionId].stash - amount);
}
}
export class GiveRoomOwnershipCommand extends Command<GameState, { client: Client, sessionId: string }> {
execute({ client, sessionId }) {
const player = this.state.players.get(sessionId);
this.state.roomOwner = player.id;
this.state.players.forEach((p, index) => {
p.owner = p.id == player.id;
});
// sends a notification to the new owner
const newOwner = this.room.clients.find((client) => { return client.sessionId === sessionId });
newOwner.send('notification', { text: 'Sei il nuovo propietario della stanza' });
}
}
export class AssignSeatCommand extends Command<GameState, { client: Client, sessionId: string }> {
execute({ client, sessionId }) {
const player = this.state.players.get(sessionId);
const minimumStash = this.state.minimumBet * 2;
if (player.stash < minimumStash) {
client.send('notification', {
type: 'warning',
text: `Al giocatore servono almeno ${minimumStash} punti per sedersi al tavolo`
});
return;
}
if (this.state.running) {
player.enteringNextTurn = true;
} else {
const seat = (<GameRoom>this.room).nextAvailableSeat();
console.log('assigning seat to user:', player.displayName, seat);
player.playing = true;
player.seat = seat;
}
}
}
export class StartGameCommand extends Command<GameState, { client: Client }> {
execute({ client }) {
if (Array.from(this.state.players.values()).filter((el) => { return el.playing; }).length < 2) {
client.send('notification', {
type: 'warning',
text: 'Servono almeno 2 giocatori per cominciare una partita'
});
return;
}
this.state.running = true;
(<GameRoom>this.room).onStartGame();
}
}
export class PauseGameCommand extends Command<GameState, { client: Client }> {
execute() {
this.state.running = false;
}
}

View File

654
server/rooms/game.ts Normal file
View File

@@ -0,0 +1,654 @@
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<GameState> {
private dispatcher = new Dispatcher(this);
private messageHandlers = createNanoEvents<OneOffEvent>();
private validateSession;
private gameType = new SettemmezzoGameType();
private deck;
private currentDealer: number = -1;
sleep = (milliseconds: number = 1000): Promise<void> => {
return new Promise<void>(resolve => {
this.clock.setTimeout(resolve, milliseconds);
});
}
getSeatedPlayers(): Player[] {
const players = Array.from<Player>(this.state.players.values());
return players.filter((p) => { return (p.playing) });
}
getPlayerBySeat(n: number): Player {
const players = Array.from<Player>(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<Player>(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] = <CartaNapoletana[]>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<PlayerAction> {
return new Promise<PlayerAction>(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<void> => {
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<void>(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 = <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);
}
}
}

181
server/server.ts Normal file
View File

@@ -0,0 +1,181 @@
import { Server } from 'colyseus';
import * as express from 'express';
import { createServer } from 'http';
import * as session from 'express-session';
import { randomBytes } from 'crypto';
import { GameRoom } from './rooms/game';
interface User {
user_id: string;
display_name: string;
}
const connected_users: { [id: string]: User } = {};
const validateSession = (user_id => {
console.log(`Is ${user_id} in ${Object.keys(connected_users)}?`);
if (Object.keys(connected_users).includes(user_id)) {
const user_info = connected_users[user_id];
console.log('Yes!', user_info);
return user_info;
}
console.log('No!');
return false;
});
// - Augments built-in Node.js IncomingMessage to include "session"
// - Lets TypeScript recognize "request.session" during onAuth()
declare module 'http' {
interface IncomingMessage {
session: session.Session
}
}
const sessionParser = session({
secret: 'any secret string',
// store: new RedisStore({ client: redisClient })
});
const port = Number(process.env.port) || 3001;
const app = express();
app.use(sessionParser);
app.use(express.json());
app.use(express.static('../client/build'))
app.post('/api/login', (req, res) => {
// login to the server (a unique display name is enough)
const username = req.body.display_name;
console.log('Login request with display name', username, connected_users);
const existing_user = Object.values(connected_users).find(user => user.display_name === username);
if (existing_user) {
console.log('oh no, no no, oh no non ononononono');
res.status(401).json({ sto: 'cazzo' });
} else {
const random_id = `player-${randomBytes(16).toString('hex')}`;
connected_users[random_id] = {
user_id: random_id,
display_name: username,
};
req.session!['user_id'] = random_id;
req.session!['display_name'] = username;
console.log('oh yeah!', connected_users);
res.json({ user_id: random_id });
}
});
app.post('/api/logout', (req, res) => {
const user_id = req.session!['user_id'];
console.log('Logging out user:', user_id);
delete connected_users[user_id];
delete req.session!['user_id'];
delete req.session!['display_name'];
res.json({ bye: 'bye' });
});
app.get('/api/session', (req, res) => {
// returns session data
res.json(req.session!);
});
const gameServer = new Server({
server: createServer(app),
verifyClient: (info, next) => {
// Make 'session' available for the websocket connection (during onAuth())
sessionParser(info.req as any, {} as any, () => next(true));
}
});
gameServer.define('game', GameRoom, { validateSession: validateSession });
gameServer.listen(port, '0.0.0.0');
// Connection broker
// const peerServerOptions = {
// debug: true,
// path: '/domingo'
// };
// const peerServer = ExpressPeerServer(server, peerServerOptions);
// app.use('/peerjs', peerServer);
// const joined_users = {};
// const rooms = {};
// io.on('connection', socket => {
// let user = null;
// let session_id = null;
// socket.on('JOIN_AS', (username) => {
// console.log('JOIN_AS', username);
// if (username === null || username in Object.values(joined_users)) {
// socket.emit('ERROR', 'Lo username non é valido oppure risulta giá in uso');
// return;
// }
// session_id = uuidV4();
// user = username;
// joined_users[session_id] = user;
// socket.emit('JOINED', session_id);
// });
// socket.on('CREATE_ROOM', (session_token) => {
// if (session_token !== session_id) {
// socket.emit('ERROR', 'Non sei autorizzato a creare una stanza');
// return;
// }
// const roomId = uuidV4();
// rooms[roomId] = {
// owner: user,
// participants: {} // maps session tokens to peer ids
// };
// console.log(`Created new room ${roomId} for user ${user}`);
// socket.emit('ROOM_CREATED', roomId);
// });
// socket.on('JOIN_ROOM', (session_token, roomId, peerId, callback) => {
// console.log('DEBUG: JOIN_ROOM', session_token, roomId, peerId);
// if (roomId in rooms === false) {
// socket.emit('ERROR', 'Stanza non trovata');
// console.log(`Cercava ${roomId} in ${rooms}`);
// return;
// }
// if (peerId === null) {
// socket.emit('ERROR', 'Peer ID mancante nella richiesta');
// return;
// }
// if (session_token in joined_users === false) {
// socket.emit('ERROR', 'La sessione non é valida');
// return;
// }
// const user = joined_users[session_token];
// console.log(`User ${user} is joining room ${roomId}`);
// // Aggiorna la stanza con il peerId
// const user_obj = {
// user_id: session_token,
// display_name: user,
// peer: peerId
// };
// rooms[roomId].participants[session_token] = user_obj;
// // Annuncia l'utente nella stanza
// socket.join(roomId);
// socket.to(roomId).broadcast.emit('USER_CONNECTED', session_token, user_obj);
// // Restituisce la lista aggiornata dei giocatori della stanza
// callback({ participants: rooms[roomId].participants });
// });
// })

10
server/tsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"declaration": true,
"experimentalDecorators": true,
"target": "es6"
},
"include": [
"**/*.ts"
]
}

2176
server/yarn.lock Normal file

File diff suppressed because it is too large Load Diff