diff --git a/README.md b/README.md index 9856ec6..70e2c6e 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,20 @@ The app is online and free to play at doOffice(est)} + onClick={() => doOffice(est, renovation)} >
{estRollBoxes}
diff --git a/src/board/utils.tsx b/src/board/utils.tsx index 2a9173f..d31fdc9 100644 --- a/src/board/utils.tsx +++ b/src/board/utils.tsx @@ -7,9 +7,13 @@ import classNames from 'classnames'; * Convert `Est.EstColor` to CSS class name. * @param color * @param darker - If true, uses darker variant. + * @param renovation - True if establishment is closed under renovations. * @returns */ -export const estColorToClass = (color: Est.EstColor, darker: boolean): string => { +export const estColorToClass = (color: Est.EstColor, darker: boolean, renovation = false): string => { + if (renovation) { + return darker ? 'est_img_grey' : 'est_img_grey_light'; + } switch (color) { case Est.EstColor.Blue: return darker ? 'est_img_pri' : 'est_img_pri_light'; diff --git a/src/game/establishments/main.ts b/src/game/establishments/main.ts index fec781a..28109a2 100644 --- a/src/game/establishments/main.ts +++ b/src/game/establishments/main.ts @@ -59,7 +59,6 @@ export const isInUse = (est: Establishment, version: Version, expansions: Expans */ export const countRemaining = (G: MachikoroG, est: Establishment): number => { if (G.version !== est.version) { - console.warn(`Establishment id=${est._id} ver=${est.version} does not match the game version, ${G.version}.`); return 0; } return G.estData._remainingCount[est._id]; @@ -73,7 +72,6 @@ export const countRemaining = (G: MachikoroG, est: Establishment): number => { */ export const countAvailable = (G: MachikoroG, est: Establishment): number => { if (G.version !== est.version) { - console.warn(`Establishment id=${est._id} ver=${est.version} does not match the game version, ${G.version}.`); return 0; } return G.estData._availableCount[est._id]; @@ -88,7 +86,6 @@ export const countAvailable = (G: MachikoroG, est: Establishment): number => { */ export const countOwned = (G: MachikoroG, player: number, est: Establishment): number => { if (G.version !== est.version) { - console.warn(`Establishment id=${est._id} ver=${est.version} does not match the game version, ${G.version}.`); return 0; } return G.estData._ownedCount[player][est._id]; @@ -159,7 +156,7 @@ export const countTypeOwned = (G: MachikoroG, player: number, type: EstType): nu */ export const buy = (G: MachikoroG, player: number, est: Establishment): void => { if (G.version !== est.version) { - throw new Error(`Establishment id=${est._id} ver=${est.version} does not match the game version, ${G.version}.`); + throw new Error(`Establishment ${est.name} does not match the game version, ${G.version}.`); } G.estData._remainingCount[est._id] -= 1; G.estData._availableCount[est._id] -= 1; @@ -172,13 +169,53 @@ export const buy = (G: MachikoroG, player: number, est: Establishment): void => * @param args.from - Source player. * @param args.to - Destination player. * @param est - Establishment in question. + * @param renovation - True if the establishment being transferred is closed + * for renovations. */ -export const transfer = (G: MachikoroG, args: { from: number; to: number }, est: Establishment): void => { +export const transfer = ( + G: MachikoroG, + args: { from: number; to: number }, + est: Establishment, + renovation: boolean, +): void => { if (G.version !== est.version) { - throw new Error(`Establishment id=${est._id} ver=${est.version} does not match the game version, ${G.version}.`); + throw new Error(`Establishment ${est.name} does not match the game version, ${G.version}.`); } G.estData._ownedCount[args.from][est._id] -= 1; G.estData._ownedCount[args.to][est._id] += 1; + if (renovation) { + G.estData._renovationCount[args.from][est._id] -= 1; + G.estData._renovationCount[args.to][est._id] += 1; + } +}; + +/** + * @param G + * @param player + * @param est + * @returns The number of establishments of this kind that are owned by the + * player and are under renovations. + */ +export const countRenovation = (G: MachikoroG, player: number, est: Establishment): number => { + if (G.version !== est.version) { + return 0; + } + return G.estData._renovationCount[player][est._id]; +}; + +/** + * Update `G` to reflect the number of establishments of this kind that are + * owned by the player and are under renovations. + * @param G + * @param player + * @param est + * @param count + */ +export const setRenovationCount = (G: MachikoroG, player: number, est: Establishment, count: number): void => { + if (G.version !== est.version) { + throw new Error(`Establishment ${est.name} does not match the game version, ${G.version}.`); + } + G.estData._renovationCount[player][est._id] = count; }; /** diff --git a/src/game/establishments/metadata.ts b/src/game/establishments/metadata.ts index de004ff..4d5c911 100644 --- a/src/game/establishments/metadata.ts +++ b/src/game/establishments/metadata.ts @@ -300,7 +300,6 @@ export const Mine: Establishment = { _initial: 6, }; -// TODO: implement this export const Winery: Establishment = { _id: 21, version: Version.MK1, diff --git a/src/game/landmarks/main.ts b/src/game/landmarks/main.ts index d49feda..06038c7 100644 --- a/src/game/landmarks/main.ts +++ b/src/game/landmarks/main.ts @@ -37,7 +37,6 @@ export const isInUse = (land: Landmark, version: Version, expansions: Expansion[ */ export const isAvailable = (G: MachikoroG, land: Landmark): boolean => { if (G.version !== land.version) { - console.warn(`Landmark id=${land._id} ver=${land.version} does not match the game version, ${G.version}.`); return false; } return G.landData._available[land._id]; @@ -51,7 +50,6 @@ export const isAvailable = (G: MachikoroG, land: Landmark): boolean => { */ export const owns = (G: MachikoroG, player: number, land: Landmark): boolean => { if (G.version !== land.version) { - // this is used often for hard-coded landmarks, so no need to warn return false; } return G.landData._owned[player][land._id]; @@ -64,7 +62,6 @@ export const owns = (G: MachikoroG, player: number, land: Landmark): boolean => */ export const isOwned = (G: MachikoroG, land: Landmark): boolean => { if (G.version !== land.version) { - // this is used often for hard-coded landmarks, so no need to warn return false; } return G.landData._owned.some((ownedArr) => ownedArr[land._id]); @@ -179,7 +176,7 @@ export const costArray = (G: MachikoroG, land: Landmark, player: number | null): export const buy = (G: MachikoroG, player: number, land: Landmark): void => { const version = G.version; if (version !== land.version) { - throw new Error(`Landmark id=${land._id} ver=${land.version} does not match the game version, ${G.version}.`); + throw new Error(`Landmark ${land.name} does not match the game version, ${G.version}.`); } G.landData._owned[player][land._id] = true; // in Machi Koro 2, each landmark can only be bought by one player diff --git a/src/game/machikoro.ts b/src/game/machikoro.ts index 50aab16..a0057e5 100644 --- a/src/game/machikoro.ts +++ b/src/game/machikoro.ts @@ -454,8 +454,9 @@ const doTV: Move = (context, opponent: number) => { * own to give up. * @param context * @param est + * @param renovation - True if the establishment is closed for renovations. */ -const doOfficeGive: Move = (context, est: Establishment) => { +const doOfficeGive: Move = (context, est: Establishment, renovation: boolean) => { const { G, ctx } = context; if (!canDoOfficeGive(G, ctx, est)) { return INVALID_MOVE; @@ -463,16 +464,18 @@ const doOfficeGive: Move = (context, est: Establishment) => { if (G.turnState === TurnState.OfficeGive) { G.officeGiveEst = est; + G.officeGiveRenovation = renovation; // change game state directly instead of calling `switchState` G.turnState = TurnState.OfficeTake; } else if (G.turnState === TurnState.MovingCompanyGive) { G.officeGiveEst = est; + G.officeGiveRenovation = renovation; // change game state directly instead of calling `switchState` G.turnState = TurnState.MovingCompanyOpp; } else if (G.turnState === TurnState.MovingCompany2) { const player = parseInt(ctx.currentPlayer); const prevPlayer = getPreviousPlayers(ctx)[0]; - Est.transfer(G, { from: player, to: prevPlayer }, est); + Est.transfer(G, { from: player, to: prevPlayer }, est, renovation); Log.logMovingCompany(G, est.name, prevPlayer); switchState(context); } else { @@ -488,22 +491,28 @@ const doOfficeGive: Move = (context, est: Establishment) => { * @param context * @param opponent * @param est + * @param renovation - True if the establishment is closed for renovations. */ -const doOfficeTake: Move = (context, opponent: number, est: Establishment) => { +const doOfficeTake: Move = (context, opponent: number, est: Establishment, renovation: boolean) => { const { G, ctx } = context; if (!canDoOfficeTake(G, ctx, opponent, est)) { return INVALID_MOVE; } const player = parseInt(ctx.currentPlayer); - if (G.officeGiveEst === null) { - throw new Error('Unexpected error: `G.officeGiveEst` should be set before `doOfficeTake`.'); + if (G.officeGiveEst === null || G.officeGiveRenovation === null) { + throw new Error( + 'Unexpected error: `G.officeGiveEst` and `G.officeGiveRenovation` should be set before `doOfficeTake`.', + ); } - Est.transfer(G, { from: player, to: opponent }, G.officeGiveEst); - Est.transfer(G, { from: opponent, to: player }, est); + Est.transfer(G, { from: player, to: opponent }, G.officeGiveEst, G.officeGiveRenovation); + Est.transfer(G, { from: opponent, to: player }, est, renovation); Log.logOffice(G, { player_est_name: G.officeGiveEst.name, opponent_est_name: est.name }, opponent); - G.officeGiveEst = null; // cleanup + // cleanup + G.officeGiveEst = null; + G.officeGiveRenovation = null; + switchState(context); return; @@ -520,8 +529,11 @@ const skipOffice: Move = (context) => { return INVALID_MOVE; } - G.officeGiveEst = null; // cleanup + // cleanup + G.officeGiveEst = null; + G.officeGiveRenovation = null; G.doOffice = 0; + switchState(context); return; @@ -540,13 +552,18 @@ const doMovingCompanyOpp: Move = (context, opponent: number) => { } const player = parseInt(ctx.currentPlayer); - if (G.officeGiveEst === null) { - throw new Error('Unexpected error: `G.officeGiveEst` should be set before `doOfficeMovingCompanyOpp`.'); + if (G.officeGiveEst === null || G.officeGiveRenovation === null) { + throw new Error( + 'Unexpected error: `G.officeGiveEst` and `G.officeGiveRenovation` should be set before `doMovingCompanyOpp`.', + ); } - Est.transfer(G, { from: player, to: opponent }, G.officeGiveEst); + Est.transfer(G, { from: player, to: opponent }, G.officeGiveEst, G.officeGiveRenovation); Log.logMovingCompany(G, G.officeGiveEst.name, opponent); - G.officeGiveEst = null; // cleanup + // cleanup + G.officeGiveEst = null; + G.officeGiveRenovation = null; + switchState(context); return; @@ -666,14 +683,12 @@ const switchState = (context: FnContext): void => { if (G.turnState < TurnState.Buy) { // activate city hall before buying if (getCoins(G, player) === 0) { - if (G.version === Version.MK1 && Land.owns(G, player, Land.CityHall)) { - assertNonNull(Land.CityHall.coins); - addCoins(G, player, Land.CityHall.coins); - Log.logEarn(G, player, Land.CityHall.coins, Land.CityHall.name); - } else if (G.version === Version.MK2 && Land.owns(G, player, Land.CityHall2)) { - assertNonNull(Land.CityHall2.coins); - addCoins(G, player, Land.CityHall2.coins); - Log.logEarn(G, player, Land.CityHall2.coins, Land.CityHall2.name); + for (const cityHall of [Land.CityHall, Land.CityHall2]) { + if (Land.owns(G, player, cityHall)) { + assertNonNull(cityHall.coins); + addCoins(G, player, cityHall.coins); + Log.logEarn(G, player, cityHall.coins, cityHall.name); + } } } @@ -826,57 +841,69 @@ const activateBlueGreenEsts = (context: FnContext): void => { continue; // handled above } - const count = Est.countOwned(G, currentPlayer, est); - if (count === 0) { - continue; - } + // get number of establishments owned + let count = Est.countOwned(G, currentPlayer, est); + // subtract number of establishments closed for renovations + count -= Est.countRenovation(G, currentPlayer, est); - if (Est.isEqual(est, Est.MovingCompany)) { - G.doMovingCompany = count; - } + if (count > 0) { + if (Est.isEqual(est, Est.MovingCompany)) { + G.doMovingCompany = count; + } - let earnings = est.earn; - // +1 coin to Shop type if player owns Shopping Mall - if (est.type === EstType.Shop && Land.owns(G, currentPlayer, Land.ShoppingMall)) { - assertNonNull(Land.ShoppingMall.coins); - earnings += Land.ShoppingMall.coins; - } - // +1 coin to Shop type if any player owns Shopping Mall (Machi Koro 2) - if (est.type === EstType.Shop && Land.isOwned(G, Land.ShoppingMall2)) { - assertNonNull(Land.ShoppingMall2.coins); - earnings += Land.ShoppingMall2.coins; - } + let earnings = est.earn; + // +1 coin to Shop type if player owns Shopping Mall + if (est.type === EstType.Shop && Land.owns(G, currentPlayer, Land.ShoppingMall)) { + assertNonNull(Land.ShoppingMall.coins); + earnings += Land.ShoppingMall.coins; + } + // +1 coin to Shop type if any player owns Shopping Mall (Machi Koro 2) + if (est.type === EstType.Shop && Land.isOwned(G, Land.ShoppingMall2)) { + assertNonNull(Land.ShoppingMall2.coins); + earnings += Land.ShoppingMall2.coins; + } - // by default a green establishment earns `multiplier * earnings = 1 * earnings` - // but there are special cases where `multiplier` is not 1. - let multiplier: number; - if (Est.isEqual(est, Est.CheeseFactory)) { - multiplier = Est.countTypeOwned(G, currentPlayer, EstType.Animal); - } else if (Est.isEqual(est, Est.FurnitureFactory) || Est.isEqual(est, Est.FurnitureFactory2)) { - multiplier = Est.countTypeOwned(G, currentPlayer, EstType.Gear); - } else if (Est.isEqual(est, Est.FarmersMarket)) { - multiplier = Est.countTypeOwned(G, currentPlayer, EstType.Wheat); - } else if (Est.isEqual(est, Est.FlowerShop)) { - multiplier = Est.countOwned(G, currentPlayer, Est.FlowerGarden); - } else if (Est.isEqual(est, Est.FlowerShop2)) { - multiplier = Est.countOwned(G, currentPlayer, Est.FlowerGarden2); - } else if (Est.isEqual(est, Est.FoodWarehouse) || Est.isEqual(est, Est.FoodWarehouse2)) { - multiplier = Est.countTypeOwned(G, currentPlayer, EstType.Cup); - } else if (Est.isEqual(est, Est.Winery2)) { - multiplier = Est.countTypeOwned(G, currentPlayer, EstType.Fruit); - } else if (Est.isEqual(est, Est.GeneralStore)) { - multiplier = Land.countBuilt(G, currentPlayer) < 2 ? 1 : 0; - } else if (Est.isEqual(est, Est.SodaBottlingPlant)) { - multiplier = 0; - for (const player of getNextPlayers(ctx)) { - multiplier += Est.countTypeOwned(G, player, EstType.Cup); + // by default a green establishment earns `multiplier * earnings = 1 * earnings` + // but there are special cases where `multiplier` is not 1. + let multiplier: number; + if (Est.isEqual(est, Est.CheeseFactory)) { + multiplier = Est.countTypeOwned(G, currentPlayer, EstType.Animal); + } else if (Est.isEqual(est, Est.FurnitureFactory) || Est.isEqual(est, Est.FurnitureFactory2)) { + multiplier = Est.countTypeOwned(G, currentPlayer, EstType.Gear); + } else if (Est.isEqual(est, Est.FarmersMarket)) { + multiplier = Est.countTypeOwned(G, currentPlayer, EstType.Wheat); + } else if (Est.isEqual(est, Est.FlowerShop)) { + multiplier = Est.countOwned(G, currentPlayer, Est.FlowerGarden); + } else if (Est.isEqual(est, Est.FlowerShop2)) { + multiplier = Est.countOwned(G, currentPlayer, Est.FlowerGarden2); + } else if (Est.isEqual(est, Est.FoodWarehouse) || Est.isEqual(est, Est.FoodWarehouse2)) { + multiplier = Est.countTypeOwned(G, currentPlayer, EstType.Cup); + } else if (Est.isEqual(est, Est.Winery)) { + multiplier = Est.countOwned(G, currentPlayer, Est.Vineyard); + } else if (Est.isEqual(est, Est.Winery2)) { + multiplier = Est.countTypeOwned(G, currentPlayer, EstType.Fruit); + } else if (Est.isEqual(est, Est.GeneralStore)) { + multiplier = Land.countBuilt(G, currentPlayer) < 2 ? 1 : 0; + } else if (Est.isEqual(est, Est.SodaBottlingPlant)) { + multiplier = 0; + for (const player of getNextPlayers(ctx)) { + multiplier += Est.countTypeOwned(G, player, EstType.Cup); + } + } else { + multiplier = 1; } - } else { - multiplier = 1; + + const amount = earnings * multiplier * count; + earn(G, ctx, currentPlayer, amount, est.name); } - const amount = earnings * multiplier * count; - earn(G, ctx, currentPlayer, amount, est.name); + // if there are establishments closed for renovations, open them + if (Est.isEqual(est, Est.Winery)) { + // NOTE: it is slightly strange, but `Winery` will close for renovations even if there is no `Vineyard` + Est.setRenovationCount(G, currentPlayer, Est.Winery, count); + } else { + Est.setRenovationCount(G, currentPlayer, est, 0); + } } }; @@ -1230,6 +1257,7 @@ const newTurnG = { doMovingCompany: 0, doMovingCompany2: false, officeGiveEst: null, + officeGiveRenovation: null, justBoughtEst: null, justBoughtLand: null, receivedCoins: false, diff --git a/src/game/types.ts b/src/game/types.ts index 5551ec1..9ddc42b 100644 --- a/src/game/types.ts +++ b/src/game/types.ts @@ -28,6 +28,8 @@ import type { LogEvent } from './log'; * Company landmark (Machi Koro 2). * @prop officeGiveEst - the establishment picked for the Office or Moving * Company action to give. + * @prop officeGiveRenovation - True if the establishment pick for the Office + * or Moving Company action is closed for renovations. * @prop justBoughtEst - the establishment just bought. * @prop justBoughtLand - the landmark just bought. * @prop receivedCoins - true if the current player has received coins this turn. @@ -56,6 +58,7 @@ export interface MachikoroG { doMovingCompany: number; doMovingCompany2: boolean; officeGiveEst: Establishment | null; + officeGiveRenovation: boolean | null; justBoughtEst: Establishment | null; justBoughtLand: Landmark | null; receivedCoins: boolean; diff --git a/src/styles/main.css b/src/styles/main.css index 7bea566..b957f07 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -19,6 +19,9 @@ --est-maj: #782b82; /* Major Establishments (Base) */ --est-maj-light: #ab7fb0; /* Major Establishments (Alt) */ + --grey: #606060; /* Grey */ + --grey-light: #a0a0a0; /* Light Grey */ + --land: #ae5421; /* Landmark (Base) */ --land-light: #d9a16e; /* Landmark (Alt) */ } @@ -461,6 +464,14 @@ span.tooltip_sym { background-color: var(--est-maj-light); } +.est_img_grey { + background-color: var(--grey); +} + +.est_img_grey_light { + background-color: var(--grey-light); +} + .est_roll { margin: 0 auto; padding-left: 3px;