Resolve #129: Allow liking plays during revealing.

This commit is contained in:
Gareth Latty
2020-05-16 16:00:24 +01:00
parent 492d439929
commit d3d0d997df
21 changed files with 478 additions and 214 deletions
@@ -29,6 +29,7 @@ class LikeActions extends Actions.Implementation<
if (
lobby.game.round.verifyStage<Round.Revealing | Round.Judging>(
action,
"Revealing",
"Judging"
)
) {
@@ -1,9 +1,11 @@
import * as Play from "../../games/cards/play";
import * as StartRevealing from "./start-revealing";
import * as Card from "../../games/cards/card";
/**
* Indicates the czar has finished revealing the plays and is now picking a winner.
*/
export interface StartJudging {
export interface StartJudging extends StartRevealing.AfterPlaying {
event: "StartJudging";
/**
* The plays that are to be judged. If the revealing stage was played, this won't be included as the data will have
@@ -12,7 +14,13 @@ export interface StartJudging {
plays?: Play.Revealed[];
}
export const of = (plays?: Play.Revealed[]): StartJudging => ({
export const of = (
plays?: Play.Revealed[],
played?: Play.Id,
drawn?: Card.Response[]
): StartJudging => ({
event: "StartJudging",
plays,
played,
drawn,
});
@@ -5,17 +5,32 @@ import * as Card from "../../games/cards/card";
* Indicates players have finished playing into the round and now the czar
* should reveal the plays.
*/
export interface StartRevealing {
export interface StartRevealing extends AfterPlaying {
event: "StartRevealing";
plays: Play.Id[];
}
/**
* Details in an event after finishing playing.
*/
export interface AfterPlaying {
/**
* The id of the play the user receiving this event played, if they did play one.
*/
played?: Play.Id;
/**
* The cards drawn by the player receiving this event.
*/
drawn?: Card.Response[];
}
export const of = (
plays: Play.Id[],
played?: Play.Id,
drawn?: Card.Response[]
): StartRevealing => ({
event: "StartRevealing",
plays,
...(drawn !== undefined ? { drawn } : {}),
played,
drawn,
});
+5 -1
View File
@@ -1,6 +1,7 @@
import * as Card from "../../games/cards/card";
import { Hand } from "../../games/cards/hand";
import * as Lobby from "../../lobby";
import { LikeDetail } from "../../games/game/round/public";
/**
* Synchronise the game state.
@@ -10,17 +11,20 @@ export interface Sync {
state: Lobby.Public;
hand?: Hand;
play?: Card.Id[];
likeDetail?: LikeDetail;
gameTime: number;
}
export const of = (
state: Lobby.Public,
hand?: Hand,
play?: Card.Id[]
play?: Card.Id[],
likeDetail?: LikeDetail
): Sync => ({
event: "Sync",
state,
...(hand !== undefined ? { hand } : {}),
...(play !== undefined ? { play } : {}),
...(likeDetail !== undefined ? { likeDetail } : {}),
gameTime: Date.now(),
});
+95 -17
View File
@@ -11,7 +11,9 @@ import * as RoundStageTimerDone from "../../timeout/round-stage-timer-done";
import * as Timeout from "../../timeout";
import * as Event from "../../event";
import * as StartJudging from "../../events/game-event/start-judging";
import * as StartRevealing from "../../events/game-event/start-revealing";
import * as Rules from "../rules";
import * as Game from "../game";
export type Round = Playing | Revealing | Judging | Complete;
@@ -45,12 +47,13 @@ export abstract class Base<TStage extends Stage> {
*/
public verifyStage<TRound extends Round>(
action: Action,
expected: TRound["stage"]
...expected: TRound["stage"][]
): this is TRound {
if (this.stage !== expected) {
throw new IncorrectRoundStageError(action, this.stage, expected);
if (expected.some((n) => n == this.stage)) {
return true;
} else {
throw new IncorrectRoundStageError(action, this.stage, ...expected);
}
return true;
}
}
@@ -182,7 +185,8 @@ export class Judging extends Base<"Judging"> implements Timed {
public start(
rules: Rules.Rules,
previouslyRevealed: boolean
previouslyRevealed: boolean,
newCardsAndPlayedByPlayer: Map<User.Id, StartRevealing.AfterPlaying>
): {
timeouts?: Iterable<Timeout.After>;
events?: Iterable<Event.Distributor>;
@@ -191,7 +195,15 @@ export class Judging extends Base<"Judging"> implements Timed {
const plays = previouslyRevealed
? undefined
: Array.from(this.revealedPlays());
const event = Event.targetAll(StartJudging.of(plays));
if (previouslyRevealed) {
for (const [_, v] of newCardsAndPlayedByPlayer) {
delete v.played;
}
}
const event = Event.additionally(
StartJudging.of(plays),
newCardsAndPlayedByPlayer
);
return {
timeouts: Util.asOptionalIterable(timeout),
events: Util.asOptionalIterable(event),
@@ -271,9 +283,30 @@ export class Revealing extends Base<"Revealing"> implements Timed {
this.timedOut = timedOut;
}
public start(
game: Game.Game
): {
events?: Iterable<Event.Distributor>;
timeouts?: Iterable<Timeout.After>;
} {
const playsToBeRevealed = Array.from(
wu(game.round.plays).map((play) => play.id)
);
const events = Util.asOptionalIterable(
Event.additionally(
StartRevealing.of(playsToBeRevealed),
this.getAfterPlayingDetails(game)
)
);
const timeouts = Util.asOptionalIterable(
RoundStageTimerDone.ifEnabled(game.round, game.rules.stages)
);
return { events, timeouts };
}
public advance(
rules: Rules.Rules,
previouslyRevealed = true
game: Game.Game,
previouslyRevealed: boolean
):
| {
round: Judging;
@@ -283,7 +316,11 @@ export class Revealing extends Base<"Revealing"> implements Timed {
| undefined {
if (StoredPlay.allRevealed(this)) {
const judging = new Judging(this.id, this.czar, this.call, this.plays);
const start = judging.start(rules, previouslyRevealed);
const start = judging.start(
game.rules,
previouslyRevealed,
this.getAfterPlayingDetails(game)
);
return {
round: judging,
...start,
@@ -293,6 +330,36 @@ export class Revealing extends Base<"Revealing"> implements Timed {
}
}
private getAfterPlayingDetails(
game: Game.Game
): Map<User.Id, StartRevealing.AfterPlaying> {
const slotCount = Card.slotCount(game.round.call);
const extraCards =
slotCount > 2 ||
(slotCount === 2 && game.rules.houseRules.packingHeat !== undefined)
? slotCount - 1
: 0;
const newCardsAndPlayedByPlayer = new Map<
User.Id,
StartRevealing.AfterPlaying
>();
for (const play of game.round.plays) {
const idSet = new Set(play.play.map((c) => c.id));
const player = game.players[play.playedBy];
if (player !== undefined) {
player.hand = player.hand.filter((card) => !idSet.has(card.id));
const toDraw = play.play.length - extraCards;
const drawn = game.decks.responses.draw(toDraw);
newCardsAndPlayedByPlayer.set(play.playedBy, {
drawn,
played: play.id,
});
player.hand.push(...drawn);
}
}
return newCardsAndPlayedByPlayer;
}
public waitingFor(): Set<User.Id> | null {
return new Set(this.czar);
}
@@ -363,31 +430,42 @@ export class Playing extends Base<"Playing"> implements Timed {
this.timedOut = timedOut;
}
public advance(): Revealing {
return new Revealing(
public advance(
game: Game.Game,
doNotStart = false
): {
round: Revealing;
events?: Iterable<Event.Distributor>;
timeouts?: Iterable<Timeout.After>;
} {
const revealing = new Revealing(
this.id,
this.czar,
this.call,
Util.shuffled(this.plays)
);
return {
round: revealing,
...(doNotStart ? {} : revealing.start(game)),
};
}
public skipToJudging(
rules: Rules.Rules
game: Game.Game
): {
round: Judging;
timeouts?: Iterable<Timeout.After>;
events?: Iterable<Event.Distributor>;
} {
const revealing = this.advance();
for (const play of revealing.plays) {
const advanceRevealing = this.advance(game, true);
for (const play of advanceRevealing.round.plays) {
play.revealed = true;
}
const advance = revealing.advance(rules, false);
if (advance === undefined) {
const advanceJudging = advanceRevealing.round.advance(game, false);
if (advanceJudging === undefined) {
throw new Error("All plays should have been revealed automatically.");
}
return advance;
return advanceJudging;
}
public waitingFor(): Set<User.Id> | null {
+5
View File
@@ -23,6 +23,11 @@ export interface Playing extends Base, Timed {
played: User.Id[];
}
export interface LikeDetail {
liked: Play.Id[];
played?: Play.Id;
}
export interface Revealing extends Base, Timed {
stage: "Revealing";
plays: Play.PotentiallyRevealed[];
+12 -1
View File
@@ -139,6 +139,7 @@ export class SocketManager {
await Change.apply(server, auth.gc, (lobby) => {
let hand = undefined;
let play = undefined;
let likeDetail = undefined;
if (lobby.game !== undefined) {
const player = lobby.game.players[uid];
if (player !== undefined) {
@@ -150,6 +151,16 @@ export class SocketManager {
if (potentialPlay !== undefined) {
play = potentialPlay.play.map((card) => card.id);
}
const round = lobby.game.round;
const stage = round.stage;
if (stage === "Revealing" || stage === "Judging") {
const liked = round.plays
.filter((p) => p.likes.some((l) => l === uid))
.map((p) => p.id);
const played = round.plays.find((p) => p.playedBy === uid)
?.id;
likeDetail = { played, liked };
}
}
const user = lobby.users[uid];
@@ -158,7 +169,7 @@ export class SocketManager {
lobby,
events: [
Event.targetOnly(
Sync.of(Lobby.censor(lobby), hand, play),
Sync.of(Lobby.censor(lobby), hand, play, likeDetail),
uid
),
],
+13 -52
View File
@@ -7,6 +7,8 @@ import * as Timeout from "../timeout";
import * as Util from "../util";
import * as RoundStageTimerDone from "./round-stage-timer-done";
import * as Rules from "../games/rules";
import * as User from "../user";
import * as Game from "../games/game";
/**
* Indicates that the round should start the revealing phase if it is appropriate
* to do so.
@@ -49,59 +51,18 @@ export const handle: Timeout.Handler<FinishedPlaying> = (
// We discard the plays first so if the deck runs out, all the cards get
// rotated in.
const responses = game.decks.responses;
for (const play of game.round.plays) {
responses.discard(play.play);
game.decks.responses.discard(play.play);
}
const slotCount = Card.slotCount(game.round.call);
const extraCards =
slotCount > 2 ||
(slotCount === 2 && game.rules.houseRules.packingHeat !== undefined)
? slotCount - 1
: 0;
const newCardsByPlayer = new Map();
for (const play of game.round.plays) {
const idSet = new Set(play.play.map((c) => c.id));
const player = game.players[play.playedBy];
if (player !== undefined) {
player.hand = player.hand.filter((card) => !idSet.has(card.id));
const toDraw = play.play.length - extraCards;
const drawn = responses.draw(toDraw);
newCardsByPlayer.set(play.playedBy, { drawn });
player.hand.push(...drawn);
}
}
if (game.rules.stages.revealing === undefined) {
const { round, events, timeouts } = game.round.skipToJudging(game.rules);
game.round = round;
return {
lobby,
events,
timeouts,
};
} else {
game.round = game.round.advance();
const playsToBeRevealed = Array.from(
wu(game.round.plays).map((play) => play.id)
);
const events = [
Event.additionally(
StartRevealing.of(playsToBeRevealed),
newCardsByPlayer
),
];
const timeouts = Util.asOptionalIterable(
RoundStageTimerDone.ifEnabled(game.round, game.rules.stages)
);
return {
lobby,
events: events,
timeouts: timeouts,
};
}
const { round, events, timeouts } =
game.rules.stages.revealing === undefined
? game.round.skipToJudging(game)
: game.round.advance(game);
game.round = round;
return {
lobby,
events,
timeouts,
};
};
+1 -1
View File
@@ -30,7 +30,7 @@ export const handle: Timeout.Handler<FinishedRevealing> = (
if (round.stage !== "Revealing") {
return {};
}
const advanced = round.advance(game.rules);
const advanced = round.advance(game, true);
if (advanced === undefined) {
return {};
}