Implemented AI (“Rando”), refactored card rendering.
This commit is contained in:
@@ -4,6 +4,7 @@ import * as rules from "../../../games/rules";
|
||||
import { Handler } from "../../handler";
|
||||
import * as configure from "../configure";
|
||||
import * as houseRuleChanged from "../../../events/lobby-event/configured/house-rule-changed";
|
||||
import * as rando from "../../../games/rules/rando";
|
||||
|
||||
/**
|
||||
* Set the hand size for the lobby.
|
||||
@@ -24,24 +25,40 @@ export const is = (action: Action): action is ChangeHouseRule =>
|
||||
action.action === name;
|
||||
|
||||
export const handle: Handler<ChangeHouseRule> = (auth, lobby, action) => {
|
||||
const houseRules = lobby.config.rules.houseRules;
|
||||
const hr = lobby.config.rules.houseRules;
|
||||
const events = [];
|
||||
let changed = false;
|
||||
switch (action.change.houseRule) {
|
||||
case "PackingHeat":
|
||||
houseRules.packingHeat = action.change.settings;
|
||||
if (hr.packingHeat !== action.change.settings) {
|
||||
hr.packingHeat = action.change.settings;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
case "Rando":
|
||||
houseRules.rando = action.change.settings;
|
||||
const userEvents = rando.change(lobby, hr.rando, action.change.settings);
|
||||
if (userEvents !== null) {
|
||||
events.push(...userEvents);
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
case "Reboot":
|
||||
houseRules.reboot = action.change.settings;
|
||||
if (hr.reboot !== action.change.settings) {
|
||||
hr.reboot = action.change.settings;
|
||||
changed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
lobby.config.version += 1;
|
||||
|
||||
return {
|
||||
lobby,
|
||||
events: [
|
||||
if (changed) {
|
||||
lobby.config.version += 1;
|
||||
events.push(
|
||||
event.targetAll(houseRuleChanged.of(action.change, lobby.config.version))
|
||||
]
|
||||
};
|
||||
);
|
||||
return {
|
||||
lobby,
|
||||
events
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as card from "../../games/cards/card";
|
||||
import * as round from "../../games/game/round";
|
||||
import * as publicRound from "../../games/game/round/public";
|
||||
|
||||
/**
|
||||
@@ -11,10 +12,10 @@ export interface GameStarted {
|
||||
}
|
||||
|
||||
export const of = (
|
||||
round: publicRound.Playing,
|
||||
startedRound: round.Playing,
|
||||
hand?: card.Response[]
|
||||
): GameStarted => ({
|
||||
event: "GameStarted",
|
||||
round,
|
||||
round: round.censor(startedRound),
|
||||
...(hand !== undefined ? { hand } : {})
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as card from "../../games/cards/card";
|
||||
import { Round } from "../../games/game/round";
|
||||
import * as user from "../../user";
|
||||
import * as round from "../../games/game/round";
|
||||
|
||||
/**
|
||||
* Indicates a new round has started.
|
||||
@@ -14,17 +14,11 @@ export interface RoundStarted {
|
||||
drawn?: card.Response[];
|
||||
}
|
||||
|
||||
export const of = (
|
||||
id: round.Id,
|
||||
czar: user.Id,
|
||||
players: user.Id[],
|
||||
call: card.Call,
|
||||
drawn?: card.Response[]
|
||||
): RoundStarted => ({
|
||||
export const of = (round: Round, drawn?: card.Response[]): RoundStarted => ({
|
||||
event: "RoundStarted",
|
||||
id: id.toString(),
|
||||
czar,
|
||||
players,
|
||||
call,
|
||||
id: round.id.toString(),
|
||||
czar: round.czar,
|
||||
players: Array.from(round.players.keys()),
|
||||
call: round.call,
|
||||
...(drawn === undefined ? {} : { drawn })
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { User } from "../../user";
|
||||
import * as user from "../../user";
|
||||
|
||||
/**
|
||||
@@ -15,12 +16,16 @@ interface Base {
|
||||
export interface Joined extends Base {
|
||||
event: "Joined";
|
||||
name: user.Name;
|
||||
privilege?: user.Privilege;
|
||||
control?: user.Control;
|
||||
}
|
||||
|
||||
export const joined = (user: user.Id, name: user.Name): Joined => ({
|
||||
export const joined = (id: user.Id, user: User): Joined => ({
|
||||
event: "Joined",
|
||||
user,
|
||||
name
|
||||
user: id,
|
||||
name: user.name,
|
||||
...(user.privilege !== "Unprivileged" ? { privilege: user.privilege } : {}),
|
||||
...(user.control !== "Human" ? { control: user.control } : {})
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
+103
-5
@@ -1,7 +1,16 @@
|
||||
import * as event from "../event";
|
||||
import * as gameStarted from "../events/game-event/game-started";
|
||||
import * as playSubmitted from "../events/game-event/play-submitted";
|
||||
import * as roundStarted from "../events/game-event/round-started";
|
||||
import { ServerState } from "../server-state";
|
||||
import * as finishedPlaying from "../timeout/finished-playing";
|
||||
import { User } from "../user";
|
||||
import * as user from "../user";
|
||||
import * as util from "../util";
|
||||
import * as card from "./cards/card";
|
||||
import { Decks } from "./cards/decks";
|
||||
import * as decks from "./cards/decks";
|
||||
import * as play from "./cards/play";
|
||||
import * as round from "./game/round";
|
||||
import { Round } from "./game/round";
|
||||
import * as publicRound from "./game/round/public";
|
||||
@@ -10,6 +19,8 @@ import * as player from "./player";
|
||||
import { Player } from "./player";
|
||||
import * as rules from "./rules";
|
||||
import { Rules } from "./rules";
|
||||
import * as lobby from "../lobby";
|
||||
import * as timeout from "../timeout";
|
||||
import wu from "wu";
|
||||
|
||||
/**
|
||||
@@ -45,7 +56,6 @@ export const censor: (game: Game) => Public = game => ({
|
||||
|
||||
const newPlayerForUser = (decks: Decks, rules: Rules): Player => ({
|
||||
hand: decks.responses.draw(rules.handSize),
|
||||
control: "Human",
|
||||
score: 0
|
||||
});
|
||||
|
||||
@@ -79,8 +89,96 @@ export const start = (
|
||||
};
|
||||
};
|
||||
|
||||
export const nextCzar = (game: Game): user.Id => {
|
||||
const current = game.round.czar;
|
||||
const index = game.playerOrder.findIndex(id => id === current) + 1;
|
||||
return game.playerOrder[index >= game.playerOrder.length ? 0 : index];
|
||||
export const atStartOfRound = (
|
||||
server: ServerState,
|
||||
first: boolean,
|
||||
game: Game & { round: round.Playing }
|
||||
): {
|
||||
game: Game & { round: round.Playing };
|
||||
events?: Iterable<event.Distributor>;
|
||||
timeouts?: Iterable<timeout.TimeoutAfter>;
|
||||
} => {
|
||||
const slotCount = card.slotCount(game.round.call);
|
||||
|
||||
const events = [];
|
||||
if (
|
||||
slotCount > 2 ||
|
||||
(slotCount === 2 && game.rules.houseRules.packingHeat !== undefined)
|
||||
) {
|
||||
const responseDeck = game.decks.responses;
|
||||
const drawnByPlayer = new Map();
|
||||
for (const [id, playerState] of game.players) {
|
||||
if (player.role(game, id) === "Player") {
|
||||
const drawn = responseDeck.draw(slotCount - 1);
|
||||
drawnByPlayer.set(id, { drawn });
|
||||
playerState.hand.push(...drawn);
|
||||
}
|
||||
}
|
||||
if (!first) {
|
||||
events.push(
|
||||
event.additionally(roundStarted.of(game.round), drawnByPlayer)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (!first) {
|
||||
events.push(event.targetAll(roundStarted.of(game.round)));
|
||||
}
|
||||
}
|
||||
|
||||
if (first) {
|
||||
events.push(
|
||||
event.playerSpecificAddition(
|
||||
gameStarted.of(game.round),
|
||||
(id, user, player) => ({
|
||||
hand: player.hand
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const ais = game.rules.houseRules.rando.current;
|
||||
for (const ai of ais) {
|
||||
const player = game.players.get(ai) as Player;
|
||||
const plays = game.round.plays;
|
||||
const playId = play.id();
|
||||
plays.push({
|
||||
id: playId,
|
||||
play: player.hand.slice(0, slotCount),
|
||||
playedBy: ai,
|
||||
revealed: false
|
||||
});
|
||||
events.push(event.targetAll(playSubmitted.of(ai)));
|
||||
}
|
||||
|
||||
const timeouts = [];
|
||||
const timeout = finishedPlaying.ifNeeded(game.round);
|
||||
if (timeout !== undefined) {
|
||||
timeouts.push({
|
||||
timeout: timeout,
|
||||
after: server.config.timeouts.finishedPlayingDelay
|
||||
});
|
||||
}
|
||||
|
||||
return { game, events, timeouts };
|
||||
};
|
||||
|
||||
const canBeCzar = (user: User): boolean =>
|
||||
user.control !== "Computer" &&
|
||||
user.presence === "Joined" &&
|
||||
user.role === "Player";
|
||||
|
||||
const czarAfter = (lobby: lobby.WithActiveGame, nextIndex: number): user.Id => {
|
||||
const game = lobby.game;
|
||||
const potentialCzar =
|
||||
game.playerOrder[nextIndex >= game.playerOrder.length ? 0 : nextIndex];
|
||||
return canBeCzar(lobby.users.get(potentialCzar) as User)
|
||||
? potentialCzar
|
||||
: czarAfter(lobby, nextIndex + 1);
|
||||
};
|
||||
|
||||
export const nextCzar = (lobby: lobby.WithActiveGame): user.Id => {
|
||||
const game = lobby.game;
|
||||
const current = game.round.czar;
|
||||
const nextIndex = game.playerOrder.findIndex(id => id === current) + 1;
|
||||
return czarAfter(lobby, nextIndex);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@ import { Game } from "./game";
|
||||
*/
|
||||
export interface Player {
|
||||
hand: Hand;
|
||||
control: Control;
|
||||
score: Score;
|
||||
}
|
||||
|
||||
@@ -15,7 +14,6 @@ export interface Player {
|
||||
* A player containing only state all users can see.
|
||||
*/
|
||||
export interface Public {
|
||||
control: Control;
|
||||
score: Score;
|
||||
}
|
||||
|
||||
@@ -24,11 +22,6 @@ export interface Public {
|
||||
*/
|
||||
export type Role = "Czar" | "Player";
|
||||
|
||||
/**
|
||||
* Who controls the player.
|
||||
*/
|
||||
export type Control = "Human" | "Computer";
|
||||
|
||||
/**
|
||||
* How many points the player has scored.
|
||||
* @TJS-type integer
|
||||
@@ -40,7 +33,6 @@ export type Score = number;
|
||||
* Produce a public version of the given player.
|
||||
*/
|
||||
export const censor = (player: Player): Public => ({
|
||||
control: player.control,
|
||||
score: player.score
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/** The rules for a standard game.*/
|
||||
import { HouseRules } from "./rules/houseRules";
|
||||
import * as houseRules from "./rules/houseRules";
|
||||
import * as rando from "./rules/rando";
|
||||
|
||||
/** The rules for a standard game.
|
||||
*/
|
||||
export interface Rules {
|
||||
/**
|
||||
* The number of cards in each player's hand.
|
||||
@@ -21,13 +26,7 @@ export interface Rules {
|
||||
export interface Public {
|
||||
handSize: number;
|
||||
scoreLimit?: number;
|
||||
houseRules: HouseRules;
|
||||
}
|
||||
|
||||
export interface HouseRules {
|
||||
packingHeat?: PackingHeat;
|
||||
reboot?: Reboot;
|
||||
rando?: Rando;
|
||||
houseRules: houseRules.Public;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,13 +35,12 @@ export interface HouseRules {
|
||||
export const create = (): Rules => ({
|
||||
handSize: 10,
|
||||
scoreLimit: 25,
|
||||
houseRules: {}
|
||||
houseRules: houseRules.create()
|
||||
});
|
||||
|
||||
/**
|
||||
* Configuration for the "Packing Heat" house rule.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface PackingHeat {}
|
||||
|
||||
/**
|
||||
@@ -60,18 +58,9 @@ export interface Reboot {
|
||||
cost: number;
|
||||
}
|
||||
|
||||
export interface Rando {
|
||||
/**
|
||||
* The number of AI players to add to the game.
|
||||
* @TJS-type integer
|
||||
* @minimum 1
|
||||
* @maximum 10
|
||||
*/
|
||||
number: number;
|
||||
}
|
||||
|
||||
export const censor = (rules: Rules): Public => ({
|
||||
...rules
|
||||
...rules,
|
||||
houseRules: houseRules.censor(rules.houseRules)
|
||||
});
|
||||
|
||||
export interface ChangeBase<Name extends string, HouseRule> {
|
||||
@@ -80,7 +69,7 @@ export interface ChangeBase<Name extends string, HouseRule> {
|
||||
}
|
||||
|
||||
export type ChangePackingHeat = ChangeBase<"PackingHeat", PackingHeat>;
|
||||
export type ChangeRando = ChangeBase<"Rando", Rando>;
|
||||
export type ChangeRando = ChangeBase<"Rando", rando.Public>;
|
||||
export type ChangeReboot = ChangeBase<"Reboot", Reboot>;
|
||||
|
||||
export type Change = ChangePackingHeat | ChangeRando | ChangeReboot;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { PackingHeat, Reboot, Rules } from "../rules";
|
||||
import { Rando } from "./rando";
|
||||
import * as rando from "./rando";
|
||||
|
||||
/**
|
||||
* Non-standard rules that can be applied to a game.
|
||||
*/
|
||||
export interface HouseRules {
|
||||
packingHeat?: PackingHeat;
|
||||
reboot?: Reboot;
|
||||
rando: Rando;
|
||||
}
|
||||
|
||||
/**
|
||||
* The public view of the internal model.
|
||||
*/
|
||||
export interface Public {
|
||||
packingHeat?: PackingHeat;
|
||||
reboot?: Reboot;
|
||||
rando?: rando.Public;
|
||||
}
|
||||
|
||||
export const create = (): HouseRules => ({
|
||||
rando: rando.create()
|
||||
});
|
||||
|
||||
export const censor = (houseRules: HouseRules): Public => ({
|
||||
...houseRules,
|
||||
rando: rando.censor(houseRules.rando)
|
||||
});
|
||||
@@ -0,0 +1,149 @@
|
||||
import { RegisterUser } from "../../action/initial/register-user";
|
||||
import * as event from "../../event";
|
||||
import * as presenceChanged from "../../events/lobby-event/presence-changed";
|
||||
import { Lobby } from "../../lobby";
|
||||
import * as lobby from "../../lobby";
|
||||
import { User } from "../../user";
|
||||
import * as util from "../../util";
|
||||
import * as user from "../../user";
|
||||
|
||||
/**
|
||||
* The maximum number of AI players allowed in a single game.
|
||||
*/
|
||||
const max = 10;
|
||||
|
||||
/**
|
||||
* The default name for an AI.
|
||||
*/
|
||||
export const aiName = "Rando Cardrissian";
|
||||
|
||||
/**
|
||||
* Apart from the one inherited from Cards against Humanity (a reference to
|
||||
* Star Wars), we use some references to famous AIs and computers from films
|
||||
* and games.
|
||||
*/
|
||||
export const aiNames = new Set([
|
||||
"HAL 9000", // 2001: A Space Odyssey
|
||||
"GLaDOS", // Portal
|
||||
"Wheatley", // Portal
|
||||
"TEC-XX", // Paper Mario
|
||||
"EDI", // Mass Effect
|
||||
"343 Guilty Spark", // Halo
|
||||
"Cortana", // Halo
|
||||
"J.A.R.V.I.S.", // MCU
|
||||
"Deep Thought", // HHGTTG
|
||||
"Gibson", // Hackers
|
||||
"Skynet", // Terminator
|
||||
"Project 2501", // GITS
|
||||
"SHODAN", // System Shock
|
||||
"Mr. House" // Fallout: New Vegas
|
||||
]);
|
||||
|
||||
/**
|
||||
* The internal model of the Rando house rule.
|
||||
*/
|
||||
export interface Rando {
|
||||
/**
|
||||
* The ids of active AI players in the game.
|
||||
*/
|
||||
current: user.Id[];
|
||||
/**
|
||||
* The names or ids of potential AI players that are not currently active in
|
||||
* the game.
|
||||
*/
|
||||
unused: (user.Id | RegisterUser)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the initial model.
|
||||
*/
|
||||
export const create = (): Rando => ({
|
||||
current: [],
|
||||
unused: [aiName]
|
||||
.concat(util.shuffled(aiNames).slice(0, max - 1))
|
||||
.map(name => ({ name }))
|
||||
});
|
||||
|
||||
/**
|
||||
* The public view of the Rando house rule.
|
||||
*/
|
||||
export interface Public {
|
||||
/**
|
||||
* The number of AI players to add to the game.
|
||||
* @TJS-type integer
|
||||
* @minimum 1
|
||||
* @maximum 10
|
||||
*/
|
||||
number: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public view of the given internal model.
|
||||
*/
|
||||
export const censor = (rando: Rando): Public | undefined =>
|
||||
rando.current.length > 0 ? { number: rando.current.length } : undefined;
|
||||
|
||||
const isId = (ai: user.Id | RegisterUser): ai is user.Id =>
|
||||
typeof ai === "string";
|
||||
|
||||
export const createIfNeeded = (
|
||||
inLobby: Lobby,
|
||||
ai: user.Id | RegisterUser
|
||||
): { user: user.Id; events: Iterable<event.Distributor> } => {
|
||||
if (isId(ai)) {
|
||||
return {
|
||||
user: ai,
|
||||
events: [
|
||||
event.targetAll(
|
||||
presenceChanged.joined(ai, inLobby.users.get(ai) as User)
|
||||
)
|
||||
]
|
||||
};
|
||||
} else {
|
||||
return lobby.addUser(inLobby, ai, user => ({
|
||||
...user,
|
||||
control: "Computer"
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the internal model to match the given public representation.
|
||||
*/
|
||||
export function* change(
|
||||
inLobby: Lobby,
|
||||
config: Rando,
|
||||
changeTo?: Public
|
||||
): Iterable<event.Distributor> | null {
|
||||
const want = changeTo !== undefined ? changeTo.number : 0;
|
||||
const have = config.current.length;
|
||||
const eventsCollection = [];
|
||||
if (want === have) {
|
||||
return null;
|
||||
}
|
||||
if (want > have) {
|
||||
const toAdd = want - have;
|
||||
const added = config.unused
|
||||
.splice(0, toAdd)
|
||||
.map(ai => createIfNeeded(inLobby, ai));
|
||||
for (const { user, events } of added) {
|
||||
const userData = inLobby.users.get(user) as User;
|
||||
userData.presence = "Joined";
|
||||
config.current.push(user);
|
||||
eventsCollection.push(...events);
|
||||
}
|
||||
} else if (have > want) {
|
||||
const toRemove = have - want;
|
||||
const removed = config.current.splice(
|
||||
config.current.length - toRemove,
|
||||
toRemove
|
||||
);
|
||||
for (const ai of removed) {
|
||||
const user = inLobby.users.get(ai) as User;
|
||||
user.presence = "Left";
|
||||
eventsCollection.push(event.targetAll(presenceChanged.left(ai)));
|
||||
}
|
||||
config.unused.splice(0, 0, ...removed);
|
||||
}
|
||||
yield* eventsCollection;
|
||||
}
|
||||
@@ -126,7 +126,7 @@ async function main(): Promise<void> {
|
||||
return {
|
||||
change: {
|
||||
lobby,
|
||||
events: [event.targetAll(presenceChanged.joined(id, newUser.name))],
|
||||
events: [event.targetAll(presenceChanged.joined(id, newUser))],
|
||||
timeouts: [
|
||||
{
|
||||
timeout: userDisconnect.of(id),
|
||||
|
||||
+22
-1
@@ -1,4 +1,7 @@
|
||||
import { CreateLobby } from "./action/initial/create-lobby";
|
||||
import { RegisterUser } from "./action/initial/register-user";
|
||||
import * as event from "./event";
|
||||
import * as presenceChanged from "./events/lobby-event/presence-changed";
|
||||
import * as game from "./games/game";
|
||||
import { Game } from "./games/game";
|
||||
import * as rules from "./games/rules";
|
||||
@@ -8,7 +11,6 @@ import { GameCode } from "./lobby/game-code";
|
||||
import * as user from "./user";
|
||||
import { User } from "./user";
|
||||
import * as util from "./util";
|
||||
import * as token from "./user/token";
|
||||
|
||||
/**
|
||||
* A game lobby.
|
||||
@@ -132,3 +134,22 @@ export const censor = (lobby: Lobby): Public => ({
|
||||
config: config.censor(lobby.config),
|
||||
...(lobby.game === undefined ? {} : { game: game.censor(lobby.game) })
|
||||
});
|
||||
|
||||
export const addUser = (
|
||||
lobby: Lobby,
|
||||
registration: RegisterUser,
|
||||
change?: (user: User) => User
|
||||
): {
|
||||
user: user.Id;
|
||||
events: Iterable<event.Distributor>;
|
||||
} => {
|
||||
const newUser = user.create(registration);
|
||||
const changedUser = change === undefined ? newUser : change(newUser);
|
||||
const id = lobby.nextUserId.toString();
|
||||
lobby.nextUserId += 1;
|
||||
lobby.users.set(id, changedUser);
|
||||
return {
|
||||
user: id,
|
||||
events: [event.targetAll(presenceChanged.joined(id, changedUser))]
|
||||
};
|
||||
};
|
||||
|
||||
@@ -15,12 +15,16 @@ export abstract class TaskBase<T> implements Task {
|
||||
}
|
||||
|
||||
protected abstract async begin(server: ServerState): Promise<T>;
|
||||
protected abstract resolve(lobby: Lobby, work: T): Change;
|
||||
protected abstract resolve(
|
||||
lobby: Lobby,
|
||||
work: T,
|
||||
server: ServerState
|
||||
): Change;
|
||||
|
||||
public async handle(server: ServerState): Promise<void> {
|
||||
const work = await this.begin(server);
|
||||
await change.apply(server, this.gameCode, lobby =>
|
||||
this.resolve(lobby, work)
|
||||
this.resolve(lobby, work, server)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import wu from "wu";
|
||||
import * as event from "../event";
|
||||
import * as gameStarted from "../events/game-event/game-started";
|
||||
import * as decks from "../games/cards/decks";
|
||||
import * as source from "../games/cards/source";
|
||||
import * as sources from "../games/cards/sources";
|
||||
import * as game from "../games/game";
|
||||
import * as round from "../games/game/round";
|
||||
import { Lobby } from "../lobby";
|
||||
import { Change } from "../lobby/change";
|
||||
import { GameCode } from "../lobby/game-code";
|
||||
@@ -28,20 +25,23 @@ export class StartGame extends task.TaskBase<decks.Templates[]> {
|
||||
);
|
||||
}
|
||||
|
||||
protected resolve(lobby: Lobby, work: decks.Templates[]): Change {
|
||||
protected resolve(
|
||||
lobby: Lobby,
|
||||
work: decks.Templates[],
|
||||
server: ServerState
|
||||
): Change {
|
||||
if (lobby.game !== undefined) {
|
||||
return {};
|
||||
}
|
||||
const lobbyGame = game.start(work, lobby.users.keys(), lobby.config.rules);
|
||||
const gameRound = round.censor(lobbyGame.round);
|
||||
const baseEvent = gameStarted.of(gameRound);
|
||||
const events = [
|
||||
event.playerSpecificAddition(baseEvent, (id, user, player) => ({
|
||||
hand: player.hand
|
||||
}))
|
||||
];
|
||||
lobby.game = lobbyGame;
|
||||
return { lobby, events };
|
||||
|
||||
const atStartOfRound = game.atStartOfRound(server, true, lobbyGame);
|
||||
lobby.game = atStartOfRound.game;
|
||||
return {
|
||||
lobby,
|
||||
events: atStartOfRound.events,
|
||||
timeouts: atStartOfRound.timeouts
|
||||
};
|
||||
}
|
||||
|
||||
// This is super unlikely timing-wise, and if it happens, the user just has
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import wu from "wu";
|
||||
import * as event from "../event";
|
||||
import * as roundStarted from "../events/game-event/round-started";
|
||||
import { Game } from "../games/game";
|
||||
import * as game from "../games/game";
|
||||
import * as lobby from "../lobby";
|
||||
import { Playing } from "../games/game/round";
|
||||
import * as timeout from "../timeout";
|
||||
import * as card from "../games/cards/card";
|
||||
import * as player from "../games/player";
|
||||
|
||||
/**
|
||||
* Indicates that the round should start if it is still appropriate to do so.
|
||||
@@ -21,15 +20,14 @@ export const handle: timeout.Handler<RoundStart> = (
|
||||
server,
|
||||
timeout,
|
||||
gameCode,
|
||||
lobby
|
||||
inLobby
|
||||
) => {
|
||||
const lobbyGame = lobby.game;
|
||||
if (lobbyGame !== undefined) {
|
||||
if (lobby.hasActiveGame(inLobby)) {
|
||||
const lobbyGame = inLobby.game;
|
||||
const gameRound = lobbyGame.round;
|
||||
if (gameRound.stage === "Complete") {
|
||||
const czar = game.nextCzar(lobbyGame);
|
||||
const czar = game.nextCzar(inLobby);
|
||||
const [call] = lobbyGame.decks.calls.replace(gameRound.call);
|
||||
const slotCount = card.slotCount(call);
|
||||
const roundId = gameRound.id + 1;
|
||||
const playersInRound = new Set(
|
||||
wu(lobbyGame.playerOrder).filter(id => id !== czar)
|
||||
@@ -37,37 +35,24 @@ export const handle: timeout.Handler<RoundStart> = (
|
||||
lobbyGame.decks.responses.discard(
|
||||
gameRound.plays.flatMap(play => play.play)
|
||||
);
|
||||
lobbyGame.round = {
|
||||
stage: "Playing",
|
||||
id: roundId,
|
||||
czar: czar,
|
||||
players: playersInRound,
|
||||
call: call,
|
||||
plays: []
|
||||
};
|
||||
const playersArray = Array.from(playersInRound);
|
||||
const baseEvent = roundStarted.of(roundId, czar, playersArray, call);
|
||||
|
||||
let events;
|
||||
if (
|
||||
slotCount > 2 ||
|
||||
(slotCount === 2 &&
|
||||
lobbyGame.rules.houseRules.packingHeat !== undefined)
|
||||
) {
|
||||
const responseDeck = lobbyGame.decks.responses;
|
||||
const drawnByPlayer = new Map();
|
||||
for (const [id, playerState] of lobbyGame.players) {
|
||||
if (player.role(lobbyGame, id) === "Player") {
|
||||
const drawn = responseDeck.draw(slotCount - 1);
|
||||
drawnByPlayer.set(id, { drawn });
|
||||
playerState.hand.push(...drawn);
|
||||
}
|
||||
const updatedGame: Game & { round: Playing } = {
|
||||
...lobbyGame,
|
||||
round: {
|
||||
stage: "Playing",
|
||||
id: roundId,
|
||||
czar: czar,
|
||||
players: playersInRound,
|
||||
call: call,
|
||||
plays: []
|
||||
}
|
||||
events = [event.additionally(baseEvent, drawnByPlayer)];
|
||||
} else {
|
||||
events = [event.targetAll(baseEvent)];
|
||||
}
|
||||
return { lobby, events };
|
||||
};
|
||||
const atStartOfRound = game.atStartOfRound(server, false, updatedGame);
|
||||
inLobby.game = atStartOfRound.game;
|
||||
return {
|
||||
inLobby,
|
||||
events: atStartOfRound.events,
|
||||
timeouts: atStartOfRound.timeouts
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
|
||||
+17
-15
@@ -6,6 +6,7 @@ export interface User {
|
||||
presence: Presence;
|
||||
connection: Connection;
|
||||
privilege: Privilege;
|
||||
control: Control;
|
||||
role: Role;
|
||||
}
|
||||
|
||||
@@ -17,6 +18,7 @@ export interface Public {
|
||||
presence: Presence;
|
||||
connection: Connection;
|
||||
privilege: Privilege;
|
||||
control: Control;
|
||||
role: Role;
|
||||
}
|
||||
|
||||
@@ -52,6 +54,11 @@ export type Connection = "Connected" | "Disconnected";
|
||||
*/
|
||||
export type Role = "Spectator" | "Player";
|
||||
|
||||
/**
|
||||
* Who/what is controlling the player—a human or the computer?
|
||||
*/
|
||||
export type Control = "Human" | "Computer";
|
||||
|
||||
/**
|
||||
* If the user is playing.
|
||||
*/
|
||||
@@ -69,26 +76,21 @@ export const isSpectating: (user: User) => boolean = user =>
|
||||
* @param registration The details of the user to create.
|
||||
* @param privilege The level of privilege the user has.
|
||||
*/
|
||||
export function create(
|
||||
export const create = (
|
||||
registration: RegisterUser,
|
||||
privilege: Privilege = "Unprivileged"
|
||||
): User {
|
||||
return {
|
||||
name: registration.name,
|
||||
presence: "Joined",
|
||||
connection: "Connected",
|
||||
privilege: privilege,
|
||||
role: "Player"
|
||||
};
|
||||
}
|
||||
): User => ({
|
||||
name: registration.name,
|
||||
presence: "Joined",
|
||||
connection: "Connected",
|
||||
privilege: privilege,
|
||||
control: "Human",
|
||||
role: "Player"
|
||||
});
|
||||
|
||||
/**
|
||||
* Gives a version of the user with only publicly visible properties.
|
||||
*/
|
||||
export const censor: (user: User) => Public = user => ({
|
||||
name: user.name,
|
||||
presence: user.presence,
|
||||
connection: user.connection,
|
||||
privilege: user.privilege,
|
||||
role: user.role
|
||||
...user
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user