Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/client/game/action_summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function ActionSummary() {
return <StLuciaTurnOrder />;
case Phase.GOVERNMENT_BUILD:
return <GovernmentBuild />;
case Phase.MEXICO_ROLE_SELECTION:
case Phase.ROLE_SELECTION:
case Phase.GOODS_GROWTH:
case Phase.INCOME:
case Phase.EXPENSES:
Expand Down
6 changes: 3 additions & 3 deletions src/engine/state/phase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ export enum Phase {
// Montreal Metro
GOVERNMENT_BUILD = 16,

// Mexico
MEXICO_ROLE_SELECTION = 17,
// Mexico, Cyprus
ROLE_SELECTION = 17,
}

export const PhaseZod = z.nativeEnum(Phase);
Expand Down Expand Up @@ -69,7 +69,7 @@ export function getPhaseString(phase: Phase): string {
return "Earth to Heaven";
case Phase.GOVERNMENT_BUILD:
return "Government build";
case Phase.MEXICO_ROLE_SELECTION:
case Phase.ROLE_SELECTION:
return "Role selection";
default:
assertNever(phase);
Expand Down
63 changes: 63 additions & 0 deletions src/maps/cyprus/action_summary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import * as React from "react";
import { Button } from "semantic-ui-react";
import { GenericMessage } from "../../client/game/action_summary";
import { useAction } from "../../client/services/action";
import { useActiveGameState } from "../../client/utils/injection_context";
import { Username } from "../../client/components/username";
import {
CyprusPickCountryAction,
CYPRUS_SELECTION_STATE,
} from "./role_selection";
import { ALL_COUNTRIES, countryName } from "./roles";

export function CyprusRoleSelectionSummary() {
const { canEmit, canEmitUserId, emit, isPending } = useAction(
CyprusPickCountryAction,
);
const selectionState = useActiveGameState(CYPRUS_SELECTION_STATE);

if (canEmitUserId == null) return <></>;

if (!canEmit) {
return (
<GenericMessage>
<Username userId={canEmitUserId} /> must choose a country.
</GenericMessage>
);
}

const taken = new Set(selectionState?.assignments.map((a) => a.country));
const available = ALL_COUNTRIES.filter((c) => !taken.has(c));

return (
<div>
<p>Choose your country:</p>
<p style={{ fontStyle: "italic" }}>
If you haven&apos;t coordinated countries with the other players, select
Randomize.
</p>
<Button.Group>
{available.map((country, i) => (
<React.Fragment key={country}>
{i > 0 && <Button.Or />}
<Button
disabled={isPending}
loading={isPending}
onClick={() => emit({ country })}
>
Play as {countryName(country)}
</Button>
</React.Fragment>
))}
<Button.Or />
<Button
disabled={isPending}
loading={isPending}
onClick={() => emit({ country: "randomize" })}
>
Randomize
</Button>
</Button.Group>
</div>
);
}
172 changes: 172 additions & 0 deletions src/maps/cyprus/role_selection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import z from "zod";
import { inject, injectState } from "../../engine/framework/execution_context";
import { Key } from "../../engine/framework/key";
import { ActionProcessor } from "../../engine/game/action";
import { Log } from "../../engine/game/log";
import { PhaseModule } from "../../engine/game/phase_module";
import { PhaseDelegator } from "../../engine/game/phase_delegator";
import { PhaseEngine } from "../../engine/game/phase";
import {
injectAllPlayersUnsafe,
injectCurrentPlayer,
TURN_ORDER,
} from "../../engine/game/state";
import { ROUND } from "../../engine/game/round";
import { Random } from "../../engine/game/random";
import { Phase } from "../../engine/state/phase";
import { PlayerColor, PlayerColorZod } from "../../engine/state/player";
import { assert } from "../../utils/validate";
import { ALL_COUNTRIES, countryName } from "./roles";

const CyprusSelectionStateZod = z.object({
assignments: z.array(
z.object({
playerId: z.number(),
country: PlayerColorZod,
}),
),
});

export const CYPRUS_SELECTION_STATE = new Key("cyprusRoleSelections", {
parse: CyprusSelectionStateZod.parse,
});

const PickCountryData = z.object({
country: z.union([PlayerColorZod, z.literal("randomize")]),
});
type PickCountryData = z.infer<typeof PickCountryData>;

export class CyprusPickCountryAction
implements ActionProcessor<PickCountryData>
{
static readonly action = "cyprusPickCountry";
readonly assertInput = PickCountryData.parse;

private readonly currentPlayer = injectCurrentPlayer();
private readonly selectionState = injectState(CYPRUS_SELECTION_STATE);
private readonly random = inject(Random);
private readonly log = inject(Log);

validate({ country }: PickCountryData): void {
if (country === "randomize") return;
const taken = this.selectionState().assignments.map((a) => a.country);
assert(!taken.includes(country as PlayerColor), {
invalidInput: "Country already chosen",
});
}

process({ country }: PickCountryData): boolean {
const current = this.currentPlayer();
const taken = new Set(
this.selectionState().assignments.map((a) => a.country),
);
const available = ALL_COUNTRIES.filter((c) => !taken.has(c));
const assigned =
country === "randomize"
? available[this.random.random(available.length)]
: country;
this.selectionState.update((state) => {
state.assignments.push({ playerId: current.playerId, country: assigned });
});
if (country === "randomize") {
this.log.currentPlayer(
`is randomly assigned to play as ${countryName(assigned)}`,
);
} else if (available.length === 1) {
this.log.currentPlayer(
`is assigned to the last remaining country, ${countryName(assigned)}`,
);
} else {
this.log.currentPlayer(`chooses to play as ${countryName(assigned)}`);
}
return true;
}
}

class CyprusRoleSelectionPhase extends PhaseModule {
static readonly phase = Phase.ROLE_SELECTION;

private readonly round = injectState(ROUND);
private readonly selectionState = injectState(CYPRUS_SELECTION_STATE);
private readonly allPlayers = injectAllPlayersUnsafe();
private readonly turnOrderState = injectState(TURN_ORDER);
private readonly random = inject(Random);

configureActions(): void {
this.installAction(CyprusPickCountryAction);
}

onStart(): void {
if (this.round() === 1) {
this.selectionState.initState({ assignments: [] });
}
}

getPlayerOrder(): PlayerColor[] {
return this.round() === 1 ? this.turnOrder() : [];
}

forcedAction() {
if (this.round() !== 1) return undefined;
const taken = new Set(
this.selectionState().assignments.map((a) => a.country),
);
const available = ALL_COUNTRIES.filter((c) => !taken.has(c));
if (available.length === 1) {
return {
action: CyprusPickCountryAction,
data: { country: available[0] },
};
}
return undefined;
}

onEnd(): void {
if (this.round() === 1) {
const assignments = this.selectionState().assignments;
const colorByPlayerId = new Map(
assignments.map((a) => [a.playerId, a.country]),
);

// Capture original turn order as playerIds before colors change
const orderedPlayerIds = this.turnOrder().map(
(color) => this.allPlayers().find((p) => p.color === color)!.playerId,
);

// Apply all country assignments to player colors at once
this.allPlayers.update((players) => {
for (const player of players) {
const newColor = colorByPlayerId.get(player.playerId);
if (newColor != null) {
player.color = newColor;
}
}
});

// Shuffle turn order after role selection is complete and player colors have been re-assigned
this.turnOrderState.set(
this.random.shuffle(
orderedPlayerIds.map(
(id) => this.allPlayers().find((p) => p.playerId === id)!.color,
),
),
);

this.selectionState.delete();
}
super.onEnd();
}
}

export class CyprusPhaseDelegator extends PhaseDelegator {
constructor() {
super();
this.install(CyprusRoleSelectionPhase);
}
}

export class CyprusPhaseEngine extends PhaseEngine {
phaseOrder(): Phase[] {
return [Phase.ROLE_SELECTION, ...super.phaseOrder()];
}
}
2 changes: 2 additions & 0 deletions src/maps/cyprus/roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export const UN = PlayerColor.GREEN;
export const GREECE = PlayerColor.BLUE;
export const TURKEY = PlayerColor.RED;

export const ALL_COUNTRIES = [UN, GREECE, TURKEY];

export function countryName(country: PlayerColor): string {
switch (country) {
case UN:
Expand Down
3 changes: 3 additions & 0 deletions src/maps/cyprus/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { map } from "./grid";
import { CyprusAllowedActions } from "./limitted_selection";
import { CyprusMoveAction } from "./move_goods";
import { CyprusPhaseDelegator, CyprusPhaseEngine } from "./role_selection";
import { CyprusCostCalculator, ShortBuild } from "./short_build";
import { CyprusStarter } from "./starter";
import { CyprusVariantConfig } from "./variant_config";
Expand Down Expand Up @@ -43,6 +44,8 @@ export class CyprusMapSettings implements MapSettings {
CyprusStarter,
CyprusMoveAction,
CyprusCostCalculator,
CyprusPhaseDelegator,
CyprusPhaseEngine,
];
}
}
12 changes: 12 additions & 0 deletions src/maps/cyprus/view_settings.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React from "react";
import { VariantConfig } from "../../api/variant_config";
import { CyprusVariantConfig } from "./variant_config";
import { MapViewSettings } from "../view_settings";
import { CyprusRules } from "./rules";
import { CyprusMapSettings } from "./settings";
import { CyprusVariantEditor } from "./variant_editor";
import { Phase } from "../../engine/state/phase";
import { CyprusRoleSelectionSummary } from "./action_summary";

export class CyprusViewSettings
extends CyprusMapSettings
Expand All @@ -19,4 +22,13 @@ export class CyprusViewSettings
getVariantString(variant: VariantConfig): string[] | undefined {
return [(variant as CyprusVariantConfig).variant2020 ? "2020" : "2012"];
}

getActionSummary(
phase: Phase | undefined,
): (() => React.ReactNode) | undefined {
if (phase === Phase.ROLE_SELECTION) {
return CyprusRoleSelectionSummary;
}
return undefined;
}
}
4 changes: 2 additions & 2 deletions src/maps/mexico/role_selection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class MexicoPickRoleAction implements ActionProcessor<PickRoleData> {
}

class MexicoRoleSelectionPhase extends PhaseModule {
static readonly phase = Phase.MEXICO_ROLE_SELECTION;
static readonly phase = Phase.ROLE_SELECTION;

private readonly round = injectState(ROUND);
private readonly random = inject(Random);
Expand Down Expand Up @@ -89,6 +89,6 @@ export class MexicoPhaseDelegator extends PhaseDelegator {

export class MexicoPhaseEngine extends PhaseEngine {
phaseOrder(): Phase[] {
return [Phase.MEXICO_ROLE_SELECTION, ...super.phaseOrder()];
return [Phase.ROLE_SELECTION, ...super.phaseOrder()];
}
}
2 changes: 1 addition & 1 deletion src/maps/mexico/view_settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class MexicoViewSettings
phase: Phase | undefined,
): (() => React.ReactNode) | undefined {
switch (phase) {
case Phase.MEXICO_ROLE_SELECTION:
case Phase.ROLE_SELECTION:
return MexicoRoleSelectionSummary;
case Phase.GOODS_GROWTH:
return MexicoProductionSummary;
Expand Down
Loading