Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Tech Startup #125

Merged
merged 3 commits into from
Oct 14, 2023
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
15 changes: 13 additions & 2 deletions src/board/PlayerInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,32 @@ export default class PlayerInfo extends React.Component<PlayerInfoProps, object>
const canDoOfficeGive = isActive && player === currentPlayer && Game.canDoOfficeGive(G, ctx, est);
const canDoOfficeTake = isActive && player !== currentPlayer && Game.canDoOfficeTake(G, ctx, player, est);
const canDoRenovationCompany = isActive && Game.canDoRenovationCompany(G, est);
const canInvestTechStartup =
isActive && player === currentPlayer && Est.isEqual(est, Est.TechStartup) && Game.canInvestTechStartup(G, ctx);

const estClickable = canDoOfficeGive || canDoOfficeTake || canDoRenovationCompany;
const estClickable = canDoOfficeGive || canDoOfficeTake || canDoRenovationCompany || canInvestTechStartup;
let onClickEstEvent: (est: Est.Establishment, renovation: boolean) => void;
if (canDoOfficeGive) {
onClickEstEvent = (est, renovation) => moves.doOfficeGive(est, renovation);
} else if (canDoOfficeTake) {
onClickEstEvent = (est, renovation) => moves.doOfficeTake(player, est, renovation);
} else if (canDoRenovationCompany) {
onClickEstEvent = (est) => moves.doRenovationCompany(est);
} else if (canInvestTechStartup) {
onClickEstEvent = (est) => moves.investTechStartup(est);
} else {
onClickEstEvent = () => void 0;
}

let estDescriptionUnparsed = `${est.name}\n\n${est.description}`;
// for Tech Startup establishment, add the current investment to the description
if (Est.isEqual(est, Est.TechStartup)) {
const investment = Est.getInvestment(G, player);
estDescriptionUnparsed = `${estDescriptionUnparsed}\n\nCurrent investment: ${investment}`;
}

const estRollBoxes = formatRollBoxes(est.rolls, 'mini_roll_box');
const estDescription = parseMaterialSymbols(est.name + '\n\n' + est.description);
const estDescription = parseMaterialSymbols(estDescriptionUnparsed);

for (let j = 0; j < count; j++) {
const key = `${i}_${j}`;
Expand Down
33 changes: 27 additions & 6 deletions src/game/establishments/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,25 @@ import { Expansion, MachikoroG, SupplyVariant, Version } from '../types';
/**
* Maximum number of unique establishments in the supply for Variable Supply.
*/
export const _VARIABLE_SUPPLY_LIMIT = 10;
const VARIABLE_SUPPLY_LIMIT = 10;

/**
* Maximum number of unique establishments that activate with rolls <= 6 in the
* supply for Hybrid Supply.
*/
export const _HYBRID_SUPPY_LIMIT_LOWER = 5;
const HYBRID_SUPPY_LIMIT_LOWER = 5;

/**
* Maximum number of unique establishments that activate with rolls > 6 in the
* supply for Hybrid Supply.
*/
export const _HYBRID_SUPPY_LIMIT_UPPER = 5;
const HYBRID_SUPPY_LIMIT_UPPER = 5;

/**
* Maximum number of unique major (purple) establishments in the supply for
* Hybrid Supply.
*/
export const _HYBRID_SUPPY_LIMIT_MAJOR = 2;
const HYBRID_SUPPY_LIMIT_MAJOR = 2;

/**
* @param a
Expand Down Expand Up @@ -234,6 +234,26 @@ export const unownedRedBlueGreenEst = (G: MachikoroG): Establishment | null => {
return null;
};

/**
* @param G
* @param player
* @returns The number of coins invested in the player's Tech Startup
* establishment (Machi Koro 1).
*/
export const getInvestment = (G: MachikoroG, player: number): number => {
return G.estData._investment[player];
};

/**
* Increments the number of coins invested in the player's Tech Startup
* establishment (Machi Koro 1).
* @param G
* @param player
*/
export const incrementInvestment = (G: MachikoroG, player: number): void => {
G.estData._investment[player] += 1;
};

/**
* Replenish the supply.
* @param G
Expand All @@ -251,7 +271,7 @@ export const replenishSupply = (G: MachikoroG): void => {
}
} else if (supplyVariant === SupplyVariant.Variable) {
// put establishments into the supply until there are ten unique establishments
while (decks[0].length > 0 && getAllAvailable(G).length < _VARIABLE_SUPPLY_LIMIT) {
while (decks[0].length > 0 && getAllAvailable(G).length < VARIABLE_SUPPLY_LIMIT) {
const est = decks[0].pop();
assertNonNull(est);
G.estData._availableCount[est._id] += 1;
Expand All @@ -261,7 +281,7 @@ export const replenishSupply = (G: MachikoroG): void => {
// establishments with activation <= 6 and 5 establishments with activation
// > 6 (and for Machi Koro 1, 2 major establishments).

const limits = [_HYBRID_SUPPY_LIMIT_LOWER, _HYBRID_SUPPY_LIMIT_UPPER, _HYBRID_SUPPY_LIMIT_MAJOR];
const limits = [HYBRID_SUPPY_LIMIT_LOWER, HYBRID_SUPPY_LIMIT_UPPER, HYBRID_SUPPY_LIMIT_MAJOR];

let funcs: ((est: Establishment) => boolean)[];
if (version === Version.MK1) {
Expand Down Expand Up @@ -315,6 +335,7 @@ export const initialize = (
_availableCount: Array.from({ length: numEsts }, () => 0),
_ownedCount: Array.from({ length: numPlayers }, () => Array.from({ length: numEsts }, () => 0)),
_renovationCount: Array.from({ length: numPlayers }, () => Array.from({ length: numEsts }, () => 0)),
_investment: Array.from({ length: numPlayers }, () => 0),
};

// populate establishments in use
Expand Down
1 change: 0 additions & 1 deletion src/game/establishments/metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,6 @@ export const TaxOffice: Establishment = {
_initial: null,
};

// TODO: implement this
export const TechStartup: Establishment = {
_id: 36,
version: Version.MK1,
Expand Down
3 changes: 3 additions & 0 deletions src/game/establishments/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@ export interface Establishment {
* @prop _renovationCount - Array tracking how many of each establishment are
* under renovation for each player. Indexed by player number then by
* establishment ID.
* @prop _investment - Array tracking how many coins have been invested in the
* player's Tech Startup (Machi Koro 1) establishment. Indexed by player number.
*/
export interface EstablishmentData {
_remainingCount: number[];
_availableCount: number[];
_ownedCount: number[][];
_renovationCount: number[][];
_investment: number[];
}

/**
Expand Down
48 changes: 38 additions & 10 deletions src/game/log/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const LogEventType = {
DemolitionCompany: 'DemolitionCompany',
MovingCompany: 'MovingCompany',
Park: 'Park',
TechStartup: 'TechStartup',
TunaRoll: 'TunaRoll',
EndGame: 'EndGame',
OtherEvent: 'OtherEvent',
Expand All @@ -47,6 +48,7 @@ export type LogEvent =
| DemolitionCompany
| MovingCompany
| Park
| TechStartup
| TunaRoll
| EndGame
| OtherEvent;
Expand All @@ -67,11 +69,13 @@ export const parseLogEvent = (logEvent: LogEvent, names: string[]): string => {
} else if (logEvent.type === LogEventType.Office) {
return parseOffice(logEvent, names);
} else if (logEvent.type === LogEventType.DemolitionCompany) {
return parseDemolitionCompany(logEvent, names);
return parseDemolitionCompany(logEvent);
} else if (logEvent.type === LogEventType.MovingCompany) {
return parseMovingCompany(logEvent, names);
} else if (logEvent.type === LogEventType.Park) {
return parsePark(logEvent);
} else if (logEvent.type === LogEventType.TechStartup) {
return parseInvestTechStartup(logEvent);
} else if (logEvent.type === LogEventType.TunaRoll) {
return parseTunaRoll(logEvent);
} else if (logEvent.type === LogEventType.EndGame) {
Expand Down Expand Up @@ -294,18 +298,16 @@ const parseOffice = (logEvent: Office, names: string[]): string => {

interface DemolitionCompany extends BaseLogEvent {
type: typeof LogEventType.DemolitionCompany;
land_name: string;
player: number;
landName: string;
}

/**
* Log the effect of the Demolition Company establishment.
* @param G
* @param land_name
* @param opponent
* @param landName
*/
export const logDemolitionCompany = (G: MachikoroG, land_name: string, player: number): void => {
const logEvent: DemolitionCompany = { type: LogEventType.DemolitionCompany, land_name, player };
export const logDemolitionCompany = (G: MachikoroG, landName: string): void => {
const logEvent: DemolitionCompany = { type: LogEventType.DemolitionCompany, landName };
G._logBuffer.push(logEvent);
};

Expand All @@ -314,9 +316,9 @@ export const logDemolitionCompany = (G: MachikoroG, land_name: string, player: n
* @param names - All player names.
* @returns Displayed log text for the Demolition Company establishment.
*/
const parseDemolitionCompany = (logEvent: DemolitionCompany, names: string[]): string => {
const { land_name, player } = logEvent;
return `\t${names[player]} demolished their ${land_name} (Demolition Company)`;
const parseDemolitionCompany = (logEvent: DemolitionCompany): string => {
const { landName } = logEvent;
return `\tdemolished ${landName}`;
};

// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -375,6 +377,32 @@ const parsePark = (logEvent: Park): string => {

// ----------------------------------------------------------------------------

interface TechStartup extends BaseLogEvent {
type: typeof LogEventType.TechStartup;
newInvestment: number;
}

/**
* Log the player investing in Tech Startup establishment (Machi Koro 1)
* @param G
* @param newInvestment - The new investment amount.
*/
export const logInvestTechStartup = (G: MachikoroG, newInvestment: number): void => {
const logEvent: TechStartup = { type: LogEventType.TechStartup, newInvestment };
G._logBuffer.push(logEvent);
};

/**
* @param logEvent
* @returns Displayed log text for investing in Tech Startup establishment
* (Machi Koro 1).
*/
const parseInvestTechStartup = (logEvent: TechStartup): string => {
return `\tinvested in Tech Startup (total investment: ${logEvent.newInvestment})`;
};

// ----------------------------------------------------------------------------

interface TunaRoll extends BaseLogEvent {
type: typeof LogEventType.TunaRoll;
roll: number;
Expand Down
64 changes: 55 additions & 9 deletions src/game/machikoro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,24 @@ export const canSkipRenovationCompany = (G: MachikoroG): Establishment | null =>
return Est.unownedRedBlueGreenEst(G);
};

/**
* @param G
* @returns True if the current player can invest in the Tech Startup
* establishment (Machi Koro 1).
*/
export const canInvestTechStartup = (G: MachikoroG, ctx: Ctx): boolean => {
const player = parseInt(ctx.currentPlayer);
return (
(G.turnState === TurnState.Buy || G.turnState === TurnState.End) &&
// player must own the establishment
Est.countOwned(G, player, Est.TechStartup) > 0 &&
// player has enough coins
getCoins(G, player) >= 1 &&
// player must not have invested this turn
!G.didTechStartup
);
};

/**
* @param G
* @returns True if the current player can end their turn.
Expand Down Expand Up @@ -598,7 +616,7 @@ const doDemolitionCompany: Move<MachikoroG> = (context, land: Landmark) => {

const player = parseInt(ctx.currentPlayer);
Land.demolish(G, player, land);
Log.logDemolitionCompany(G, land.name, player);
Log.logDemolitionCompany(G, land.name);

switchState(context);

Expand Down Expand Up @@ -670,6 +688,29 @@ const doRenovationCompany: Move<MachikoroG> = (context, est: Establishment) => {
return;
};

/**
* Invest in the Tech Startup establishment (Machi Koro 1).
* @param context
* @returns
*/
const investTechStartup: Move<MachikoroG> = (context) => {
const { G, ctx } = context;
if (!canInvestTechStartup(G, ctx)) {
return INVALID_MOVE;
}

const player = parseInt(ctx.currentPlayer);
addCoins(G, player, -1);
Est.incrementInvestment(G, player);
G.didTechStartup = true;
Log.logInvestTechStartup(G, Est.getInvestment(G, player));

// change game state directly instead of calling `switchState`
G.turnState = TurnState.End;

return;
};

/**
* End the turn.
* @param context
Expand All @@ -686,13 +727,11 @@ const endTurn: Move<MachikoroG> = (context) => {

// a player earns coins via the airport if they did not buy anything
if (G.turnState === TurnState.Buy) {
if (Land.owns(G, player, Land.Airport)) {
assertNonNull(Land.Airport.coins);
earn(G, ctx, player, Land.Airport.coins, Land.Airport.name);
}
if (Land.isOwned(G, Land.Airport2)) {
assertNonNull(Land.Airport2.coins);
earn(G, ctx, player, Land.Airport2.coins, Land.Airport2.name);
for (const airport of [Land.Airport, Land.Airport2]) {
if (Land.owns(G, player, airport)) {
assertNonNull(airport.coins);
earn(G, ctx, player, airport.coins, airport.name);
}
}
}

Expand Down Expand Up @@ -824,7 +863,7 @@ const switchState = (context: FnContext<MachikoroG>): void => {

activateBoughtLand(context);
}
if (G.turnState < TurnState.End) {
if (G.turnState <= TurnState.End) {
G.turnState = TurnState.End;
return; // await player action
}
Expand Down Expand Up @@ -1116,6 +1155,11 @@ const activatePurpleEsts = (context: FnContext<MachikoroG>): void => {
activatePark(G, ctx);
} else if (Est.isEqual(est, Est.RenovationCompany)) {
G.doRenovationCompany = true;
} else if (Est.isEqual(est, Est.TechStartup)) {
for (const opponent of getPreviousPlayers(ctx)) {
const amount = Est.getInvestment(G, currentPlayer) * count;
take(G, ctx, { from: opponent, to: currentPlayer }, amount, est.name);
}
}
}
};
Expand Down Expand Up @@ -1424,6 +1468,7 @@ const newTurnG = {
doMovingCompany: 0,
doMovingCompany2: false,
doRenovationCompany: false,
didTechStartup: false,
officeGiveEst: null,
officeGiveRenovation: null,
justBoughtEst: null,
Expand Down Expand Up @@ -1526,6 +1571,7 @@ export const Machikoro: Game<MachikoroG, Record<string, unknown>, SetupData> = {
doDemolitionCompany,
doMovingCompanyOpp,
doRenovationCompany,
investTechStartup,
endTurn,
},

Expand Down
5 changes: 4 additions & 1 deletion src/game/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ import type { LogEvent } from './log';
* Company landmark (Machi Koro 2).
* @prop doRenovationCompany - true if the current player will activate the
* Renovation Company establishment.
* @prop didTechStartup - true if the current player has invested in the Tech
* Startup establishment (Machi Koro 1) this turn.
* @prop officeGiveEst - the establishment picked for the Office or Moving
* Company action to give.
* @prop officeGiveRenovation - True if the establishment pick for the Office
* @prop officeGiveRenovation - True if the establishment picked for the Office
* or Moving Company action is closed for renovations.
* @prop justBoughtEst - the establishment just bought.
* @prop justBoughtLand - the landmark just bought.
Expand Down Expand Up @@ -64,6 +66,7 @@ export interface MachikoroG {
doMovingCompany: number;
doMovingCompany2: boolean;
doRenovationCompany: boolean;
didTechStartup: boolean;
officeGiveEst: Establishment | null;
officeGiveRenovation: boolean | null;
justBoughtEst: Establishment | null;
Expand Down
Loading