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
1 change: 1 addition & 0 deletions src/client/game/action_summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export function ActionSummary() {
return <StLuciaTurnOrder />;
case Phase.GOVERNMENT_BUILD:
return <GovernmentBuild />;
case Phase.STALINIST_LOCOMOTIVE:
case Phase.ROLE_SELECTION:
case Phase.GOODS_GROWTH:
case Phase.INCOME:
Expand Down
7 changes: 7 additions & 0 deletions src/engine/state/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ export enum Action {

// Central New England
SMUGGLE = 27,

// Stalinist Russia
POLITBURO_DIRECTIVE = 28,
}

export const ActionZod = z.nativeEnum(Action);
Expand Down Expand Up @@ -122,6 +125,8 @@ export class ActionNamingProvider {
return "Goldsmith";
case Action.SMUGGLE:
return "Smuggle";
case Action.POLITBURO_DIRECTIVE:
return "Politburo Directive";
default:
assertNever(action);
}
Expand Down Expand Up @@ -183,6 +188,8 @@ export class ActionNamingProvider {
return "Deliver gold without spending mining expertise or increase income for gold deliveries.";
case Action.SMUGGLE:
return "You may make both of your deliveries within a single state.";
case Action.POLITBURO_DIRECTIVE:
return "For one of your deliveries, you may start from Moscow.";
default:
assertNever(action);
}
Expand Down
5 changes: 5 additions & 0 deletions src/engine/state/phase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export enum Phase {

// Mexico, Cyprus
ROLE_SELECTION = 17,

// Stalinist Russia
STALINIST_LOCOMOTIVE = 18,
}

export const PhaseZod = z.nativeEnum(Phase);
Expand Down Expand Up @@ -71,6 +74,8 @@ export function getPhaseString(phase: Phase): string {
return "Government build";
case Phase.ROLE_SELECTION:
return "Role selection";
case Phase.STALINIST_LOCOMOTIVE:
return "Locomotive";
default:
assertNever(phase);
}
Expand Down
2 changes: 2 additions & 0 deletions src/maps/registry.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions src/maps/stalinist_russia/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Set as ImmutableSet } from "immutable";
import { injectInitialPlayerCount } from "../../engine/game/state";
import { AllowedActions } from "../../engine/select_action/allowed_actions";
import { SelectAction } from "../../engine/select_action/select";
import { Action, ActionNamingProvider } from "../../engine/state/action";

export class StalinistRussiaAllowedActions extends AllowedActions {
private readonly playerCount = injectInitialPlayerCount();

getActions(): ImmutableSet<Action> {
let actions = super.getActions().add(Action.POLITBURO_DIRECTIVE);
// The Engineer action is only available at five players.
if (this.playerCount() !== 5) {
actions = actions.delete(Action.ENGINEER);
}
return actions;
}
}

export class StalinistRussiaSelectAction extends SelectAction {
protected applyLocomotive(): void {
// The Locomotive action does not immediately raise the locomotive on this
// map. Instead it grants going first during the dedicated locomotive phase.
}
}

export class StalinistRussiaActionNamingProvider extends ActionNamingProvider {
getActionDescription(action: Action): string {
if (action === Action.LOCOMOTIVE) {
return "Go first during the Locomotive phase.";
}
return super.getActionDescription(action);
}
}
38 changes: 38 additions & 0 deletions src/maps/stalinist_russia/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Coordinates } from "../../utils/coordinates";
import { injectState } from "../../engine/framework/execution_context";
import { injectCurrentPlayer, TURN_ORDER } from "../../engine/game/state";
import { BuildCostCalculator } from "../../engine/build/cost";
import { SpaceType } from "../../engine/state/location_type";
import { LandType } from "../../engine/state/space";
import { Direction, TileType } from "../../engine/state/tile";

export class StalinistRussiaBuildCostCalculator extends BuildCostCalculator {
private readonly currentPlayer = injectCurrentPlayer();
private readonly turnOrder = injectState(TURN_ORDER);

costOf(
coordinates: Coordinates,
newTileType: TileType,
orientation: Direction,
): number {
let cost = super.costOf(coordinates, newTileType, orientation);
// The player who is last in turn order (who received Stalin's disfavor this
// round) pays $1 extra per tile lay during the build phase.
const order = this.turnOrder();
if (
order.length > 0 &&
order[order.length - 1] === this.currentPlayer().color
) {
cost += 1;
}
return cost;
}

protected getCostOfLandTypeForTown(type: LandType): number {
// Towns with rivers cost an extra $1 on top of the usual cost.
if (type === SpaceType.RIVER) {
return super.getCostOfLandTypeForTown(type) + 1;
}
return super.getCostOfLandTypeForTown(type);
}
}
40 changes: 40 additions & 0 deletions src/maps/stalinist_russia/disfavor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { injectState } from "../../engine/framework/execution_context";
import { PlayerHelper } from "../../engine/game/player";
import { PlayerColor, PlayerData } from "../../engine/state/player";
import { TurnOrderHelper } from "../../engine/turn_order/helper";
import { DISFAVOR_TRACK, DISFAVOR_VALUES, MAX_DISFAVOR } from "./state";

export class StalinistRussiaTurnOrderHelper extends TurnOrderHelper {
private readonly disfavorTrack = injectState(DISFAVOR_TRACK);

pass(player: PlayerData): void {
// The first player to pass becomes the last player in turn order and earns
// Stalin's disfavor. nextTurnOrder is still empty at that point.
const becomesLastPlayer = this.turnOrderState().nextTurnOrder.length === 0;
super.pass(player);
if (becomesLastPlayer) {
this.disfavorTrack.update((track) => {
const current = track.get(player.color) ?? 0;
track.set(player.color, Math.min(current + 1, MAX_DISFAVOR));
});
this.log.player(player, "advances on Stalin's disfavor track");
}
}
}

export class StalinistRussiaPlayerHelper extends PlayerHelper {
private readonly disfavorTrack = injectState(DISFAVOR_TRACK);

protected calculateScore(player: PlayerData): number {
return super.calculateScore(player) + this.getScoreFromDisfavor(player);
}

getScoreFromDisfavor(player: PlayerData): number {
if (player.outOfGame) return 0;
return DISFAVOR_VALUES[this.disfavorPosition(player.color)];
}

disfavorPosition(color: PlayerColor): number {
return this.disfavorTrack().get(color) ?? 0;
}
}
77 changes: 77 additions & 0 deletions src/maps/stalinist_russia/disfavor_track.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useState } from "react";
import {
Accordion,
AccordionContent,
AccordionTitle,
Menu,
MenuItem,
Table,
TableBody,
TableCell,
TableRow,
} from "semantic-ui-react";
import { getPlayerColorCss } from "../../client/components/player_color";
import { useInjectedState } from "../../client/utils/injection_context";
import { PlayerColor } from "../../engine/state/player";
import * as styles from "./loco_track.module.css";
import { DISFAVOR_TRACK, DISFAVOR_VALUES } from "./state";

export function DisfavorTrack() {
const [expanded, setExpanded] = useState<boolean>(false);
const disfavorTrack = useInjectedState(DISFAVOR_TRACK);

const positions = DISFAVOR_VALUES.map((_, index) => index);

const playersAt = (position: number): PlayerColor[] => {
const result: PlayerColor[] = [];
for (const [color, index] of disfavorTrack) {
if (index === position) {
result.push(color);
}
}
return result;
};

return (
<Accordion fluid as={Menu} vertical>
<MenuItem>
<AccordionTitle
active={expanded}
index={0}
onClick={() => setExpanded(!expanded)}
content="Stalin's Disfavor Track"
/>
<AccordionContent active={expanded}>
<Table celled compact unstackable size="small">
<TableBody>
<TableRow>
<TableCell>Points</TableCell>
{positions.map((position) => (
<TableCell key={position} textAlign="center">
{DISFAVOR_VALUES[position]}
</TableCell>
))}
</TableRow>
<TableRow>
<TableCell>Players</TableCell>
{positions.map((position) => (
<TableCell key={position} textAlign="center">
{playersAt(position).map((color) => (
<PlayerBlock key={color} color={color} />
))}
</TableCell>
))}
</TableRow>
</TableBody>
</Table>
</AccordionContent>
</MenuItem>
</Accordion>
);
}

function PlayerBlock({ color }: { color: PlayerColor }) {
return (
<div className={`${styles.playerBlock} ${getPlayerColorCss(color)}`} />
);
}
18 changes: 18 additions & 0 deletions src/maps/stalinist_russia/disfavor_vps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import * as styles from "../../client/game/final_overview.module.css";
import { RowProps } from "../../client/game/final_overview_row";
import { useInjected } from "../../client/utils/injection_context";
import { StalinistRussiaPlayerHelper } from "./disfavor";

export function DisfavorVps({ players }: RowProps) {
const playerHelper = useInjected(StalinistRussiaPlayerHelper);
return (
<tr>
<th className={styles.label}>Stalin&apos;s Disfavor VPs</th>
{players.map(({ player }) => (
<td key={player.playerId}>
{playerHelper.getScoreFromDisfavor(player)}
</td>
))}
</tr>
);
}
9 changes: 9 additions & 0 deletions src/maps/stalinist_russia/expenses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ProfitHelper } from "../../engine/income_and_expenses/helper";
import { PlayerData } from "../../engine/state/player";

export class StalinistRussiaProfitHelper extends ProfitHelper {
getExpenses(player: PlayerData): number {
// The locomotive track does not contribute to expenses; only shares do.
return player.shares;
}
}
Loading
Loading