@gaia-project/engine
Advanced tools
Comparing version 4.8.22 to 4.8.23
@@ -59,2 +59,3 @@ "use strict"; | ||
Object.defineProperty(exports, "conversionToFreeAction", { enumerable: true, get: function () { return available_command_1.conversionToFreeAction; } }); | ||
Object.defineProperty(exports, "MAX_SHIPS_PER_HEX", { enumerable: true, get: function () { return available_command_1.MAX_SHIPS_PER_HEX; } }); | ||
var engine_2 = require("./src/engine"); | ||
@@ -74,2 +75,3 @@ Object.defineProperty(exports, "AuctionVariant", { enumerable: true, get: function () { return engine_2.AuctionVariant; } }); | ||
Object.defineProperty(exports, "FinalTile", { enumerable: true, get: function () { return enums_1.FinalTile; } }); | ||
Object.defineProperty(exports, "isShip", { enumerable: true, get: function () { return enums_1.isShip; } }); | ||
Object.defineProperty(exports, "Operator", { enumerable: true, get: function () { return enums_1.Operator; } }); | ||
@@ -76,0 +78,0 @@ Object.defineProperty(exports, "Phase", { enumerable: true, get: function () { return enums_1.Phase; } }); |
{ | ||
"name": "@gaia-project/engine", | ||
"version": "4.8.22", | ||
"version": "4.8.23", | ||
"description": "Javascript engine for project gaia", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -25,3 +25,3 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.choosableFactions = exports.possiblePISwaps = exports.possibleTechTiles = exports.canTakeAdvancedTechTile = exports.possibleFederationTiles = exports.possibleCoverTechTiles = exports.getTaklonsExtraLeechOffers = exports.possibleLeech = exports.possibleGaiaFreeActions = exports.possibleIncomes = exports.possibleFederations = exports.possibleRoundBoosters = exports.possibleSpaceLostPlanet = exports.possibleResearchAreas = exports.canResearchField = exports.possibleLabDowngrades = exports.transformToSpendCommand = exports.freeActionData = exports.possibleFreeActions = exports.possibleBoardActions = exports.possibleSpecialActions = exports.possibleMineBuildings = exports.possibleSpaceStations = exports.possibleBuildings = exports.generate = exports.conversionToFreeAction = exports.Offer = exports.UPGRADE_RESEARCH_COST = void 0; | ||
exports.choosableFactions = exports.possiblePISwaps = exports.possibleTechTiles = exports.canTakeAdvancedTechTile = exports.possibleFederationTiles = exports.possibleCoverTechTiles = exports.getTaklonsExtraLeechOffers = exports.possibleLeech = exports.possibleGaiaFreeActions = exports.possibleIncomes = exports.possibleFederations = exports.possibleRoundBoosters = exports.possibleSpaceLostPlanet = exports.possibleResearchAreas = exports.canResearchField = exports.possibleLabDowngrades = exports.transformToSpendCommand = exports.freeActionData = exports.possibleFreeActions = exports.possibleBoardActions = exports.possibleSpecialActions = exports.possibleMineBuildings = exports.possibleSpaceStations = exports.possibleShipMovements = exports.possibleBuildings = exports.shipsInHex = exports.generate = exports.conversionToFreeAction = exports.ShipAction = exports.Offer = exports.MAX_SHIPS_PER_HEX = exports.UPGRADE_RESEARCH_COST = void 0; | ||
const lodash_1 = require("lodash"); | ||
@@ -41,2 +41,4 @@ const actions_1 = require("./actions"); | ||
exports.UPGRADE_RESEARCH_COST = "4k"; | ||
exports.MAX_SHIPS_PER_HEX = 3; | ||
const SHIP_ACTION_RANGE = 1; | ||
class Offer { | ||
@@ -49,2 +51,6 @@ constructor(offer, cost) { | ||
exports.Offer = Offer; | ||
var ShipAction; | ||
(function (ShipAction) { | ||
ShipAction["BuildColony"] = "buildColony"; | ||
})(ShipAction = exports.ShipAction || (exports.ShipAction = {})); | ||
function conversionToFreeAction(act) { | ||
@@ -86,5 +92,8 @@ const entry = Object.entries(actions_1.freeActionConversions).find(([k, v]) => v.cost === act.cost && v.income === act.income); | ||
return [{ name: enums_1.Command.BrainStone, player, data }]; | ||
// case SubPhase.MoveShip: | ||
// return possibleShipMovements(engine, player); | ||
case enums_1.SubPhase.BeforeMove: { | ||
return [ | ||
...possibleBuildings(engine, player), | ||
...possibleShipMovements(engine, player), | ||
...possibleFederations(engine, player), | ||
@@ -153,3 +162,3 @@ ...possibleResearchAreas(engine, player, exports.UPGRADE_RESEARCH_COST), | ||
} | ||
const check = pl.canBuild(planet, building, lastRound, replay, { | ||
const check = pl.canBuild(map, hex, planet, building, lastRound, replay, { | ||
addedCost: [new reward_1.default(qicNeeded.amount, enums_1.Resource.Qic)], | ||
@@ -183,11 +192,35 @@ }); | ||
} | ||
function shipsInHex(location, data) { | ||
return data.players.flatMap((p) => p.data.ships).filter((s) => s.location === location); | ||
} | ||
exports.shipsInHex = shipsInHex; | ||
function possibleShips(pl, engine, map, hex) { | ||
const buildings = []; | ||
for (const ship of Object.values(enums_1.Building).filter((b) => enums_1.isShip(b))) { | ||
const check = pl.canBuild(null, null, null, ship, engine.isLastRound, engine.replay); | ||
if (check) { | ||
for (const h of map.withinDistance(hex, 1)) { | ||
if (!h.hasPlanet() && shipsInHex(h.toString(), engine).length < exports.MAX_SHIPS_PER_HEX) { | ||
buildings.push(newAvailableBuilding(ship, h, check, false)); | ||
} | ||
} | ||
} | ||
} | ||
return buildings; | ||
} | ||
function possibleBuildings(engine, player) { | ||
const map = engine.map; | ||
const pl = engine.player(player); | ||
const { data } = pl; | ||
const buildings = []; | ||
for (const hex of engine.map.toJSON()) { | ||
// upgrade existing player's building | ||
if (hex.buildingOf(player)) { | ||
const building = hex.buildingOf(player); | ||
const building = hex.buildingOf(player); | ||
if (building) { | ||
// excluding Transdim planet until transformed into Gaia planets | ||
if (hex.data.planet === enums_1.Planet.Transdim) { | ||
continue; | ||
} | ||
if (buildings_1.stdBuildingValue(building) > 0 && engine.expansions === enums_1.Expansion.Frontiers) { | ||
buildings.push(...possibleShips(pl, engine, map, hex)); | ||
} | ||
if (player !== hex.data.player) { | ||
@@ -197,6 +230,2 @@ // This is a secondary building, so we can't upgrade it | ||
} | ||
// excluding Transdim planet until transformed into Gaia planets | ||
if (hex.data.planet === enums_1.Planet.Transdim) { | ||
continue; | ||
} | ||
// Lost planet can't be upgraded | ||
@@ -213,3 +242,3 @@ if (hex.data.planet === enums_1.Planet.Lost) { | ||
for (const _pl of engine.players) { | ||
if (_pl !== engine.player(player)) { | ||
if (_pl !== pl) { | ||
for (const loc of _pl.data.occupied) { | ||
@@ -224,5 +253,5 @@ if (loc.hasStructure() && map.distance(loc, hex) < ISOLATED_DISTANCE) { | ||
})(); | ||
const upgraded = buildings_1.upgradedBuildings(building, engine.player(player).faction); | ||
const upgraded = buildings_1.upgradedBuildings(building, pl.faction); | ||
for (const upgrade of upgraded) { | ||
const check = engine.player(player).canBuild(hex.data.planet, upgrade, engine.isLastRound, engine.replay, { | ||
const check = pl.canBuild(map, hex, hex.data.planet, upgrade, engine.isLastRound, engine.replay, { | ||
isolated, | ||
@@ -250,3 +279,3 @@ existingBuilding: building, | ||
player, | ||
data: { buildings }, | ||
data: { buildings: lodash_1.uniq(buildings) }, | ||
}, | ||
@@ -258,6 +287,73 @@ ]; | ||
exports.possibleBuildings = possibleBuildings; | ||
function shipTargets(source, hex, range, targets, engine) { | ||
if (!targets.find((t) => t.coordinates === hex) && shipsInHex(hex, engine).length < exports.MAX_SHIPS_PER_HEX) { | ||
targets.push({ coordinates: hex }); | ||
} | ||
if (range === 0) { | ||
return targets; | ||
} | ||
const map = engine.map; | ||
for (const h of map.withinDistance(map.getS(hex), 1)) { | ||
const c = h.toString(); | ||
if (!h.hasPlanet() && c !== source) { | ||
shipTargets(source, c, range - 1, targets, engine); | ||
} | ||
} | ||
return targets; | ||
} | ||
function possibleColonyShipActions(engine, ship, shipLocation) { | ||
const map = engine.map; | ||
const pl = engine.player(ship.player); | ||
const locations = map.withinDistance(map.getS(shipLocation), SHIP_ACTION_RANGE).flatMap((h) => { | ||
if (h.hasPlanet() && !h.occupied() && h.data.planet !== enums_1.Planet.Transdim) { | ||
const check = pl.canBuild(map, h, h.data.planet, enums_1.Building.Colony, engine.isLastRound, engine.replay); | ||
if (check) { | ||
return [newAvailableBuilding(enums_1.Building.Colony, h, check, false)]; | ||
} | ||
} | ||
return []; | ||
}); | ||
if (locations.length > 0) { | ||
return [ | ||
{ | ||
type: ShipAction.BuildColony, | ||
locations, | ||
}, | ||
]; | ||
} | ||
return []; | ||
} | ||
function possibleShipActions(engine, ship, shipLocation) { | ||
switch (ship.type) { | ||
case enums_1.Building.ColonyShip: | ||
return possibleColonyShipActions(engine, ship, shipLocation); | ||
} | ||
return []; | ||
} | ||
function possibleShipMovements(engine, player) { | ||
const pl = engine.player(player); | ||
const ships = pl.data.ships.filter((s) => !s.moved); | ||
if (ships.length === 0) { | ||
return []; | ||
} | ||
const shipRange = engine.player(player).data.shipRange; | ||
return [ | ||
{ | ||
name: enums_1.Command.MoveShip, | ||
player, | ||
data: ships.map((s) => ({ | ||
ship: s.type, | ||
source: s.location, | ||
targets: shipTargets(s.location, s.location, shipRange, [], engine).map((t) => ({ | ||
location: t, | ||
actions: possibleShipActions(engine, s, t.coordinates), | ||
})), | ||
})), | ||
}, | ||
]; | ||
} | ||
exports.possibleShipMovements = possibleShipMovements; | ||
function possibleSpaceStations(engine, player) { | ||
const map = engine.map; | ||
const pl = engine.player(player); | ||
const { data } = pl; | ||
const buildings = []; | ||
@@ -269,4 +365,3 @@ for (const hex of map.toJSON()) { | ||
} | ||
const building = enums_1.Building.SpaceStation; | ||
addPossibleNewPlanet(map, hex, pl, pl.planet, building, buildings, engine.isLastRound, engine.replay); | ||
addPossibleNewPlanet(map, hex, pl, pl.planet, enums_1.Building.SpaceStation, buildings, engine.isLastRound, engine.replay); | ||
} | ||
@@ -273,0 +368,0 @@ if (buildings.length > 0) { |
@@ -31,2 +31,3 @@ "use strict"; | ||
case __1.Building.Academy2: | ||
case __1.Building.Colony: | ||
return 3; | ||
@@ -33,0 +34,0 @@ } |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.SubPhase = exports.Phase = exports.PowerArea = exports.FinalTile = exports.ScoringTile = exports.BoardAction = exports.Federation = exports.AdvTechTilePos = exports.AdvTechTile = exports.TechTilePos = exports.TechPos = exports.TechTile = exports.Booster = exports.RoundScoring = exports.Round = exports.Player = exports.Command = exports.Faction = exports.Building = exports.Condition = exports.Operator = exports.Resource = exports.Expansion = exports.ResearchField = exports.Planet = void 0; | ||
exports.SubPhase = exports.Phase = exports.PowerArea = exports.FinalTile = exports.ScoringTile = exports.BoardAction = exports.Federation = exports.AdvTechTilePos = exports.AdvTechTile = exports.TechTilePos = exports.TechPos = exports.TechTile = exports.Booster = exports.RoundScoring = exports.Round = exports.Player = exports.Command = exports.Faction = exports.isShip = exports.Building = exports.Condition = exports.Operator = exports.Resource = exports.Expansion = exports.ResearchField = exports.Planet = void 0; | ||
var Planet; | ||
@@ -29,3 +29,5 @@ (function (Planet) { | ||
(function (Expansion) { | ||
Expansion[Expansion["All"] = 1] = "All"; | ||
// 1 was the old spaceships expansion | ||
Expansion[Expansion["Frontiers"] = 2] = "Frontiers"; | ||
Expansion[Expansion["All"] = 2] = "All"; | ||
})(Expansion = exports.Expansion || (exports.Expansion = {})); | ||
@@ -147,3 +149,26 @@ (function (ResearchField) { | ||
Building["SpaceStation"] = "sp"; | ||
//frontiers | ||
Building["Colony"] = "colony"; | ||
Building["ColonyShip"] = "colonyShip"; | ||
Building["TradeShip"] = "tradeShip"; | ||
Building["ConstructionShip"] = "constructionShip"; | ||
Building["ResearchShip"] = "researchShip"; | ||
Building["Scout"] = "scout"; | ||
Building["Frigate"] = "frigate"; | ||
Building["BattleShip"] = "battleShip"; | ||
})(Building = exports.Building || (exports.Building = {})); | ||
function isShip(b) { | ||
switch (b) { | ||
case Building.ColonyShip: | ||
case Building.TradeShip: | ||
case Building.ConstructionShip: | ||
case Building.ResearchShip: | ||
case Building.Scout: | ||
case Building.Frigate: | ||
case Building.BattleShip: | ||
return true; | ||
} | ||
return false; | ||
} | ||
exports.isShip = isShip; | ||
var Faction; | ||
@@ -185,5 +210,5 @@ (function (Faction) { | ||
Command["Init"] = "init"; | ||
Command["MoveShip"] = "move"; | ||
Command["PISwap"] = "swap-PI"; | ||
Command["Pass"] = "pass"; | ||
Command["PickReward"] = "pick"; | ||
Command["PISwap"] = "swap-PI"; | ||
Command["PlaceLostPlanet"] = "lostPlanet"; | ||
@@ -494,2 +519,3 @@ Command["RotateSectors"] = "rotate"; | ||
Phase["RoundGaia"] = "roundGaia"; | ||
Phase["RoundShip"] = "roundShip"; | ||
Phase["RoundMove"] = "roundMove"; | ||
@@ -516,4 +542,3 @@ Phase["RoundLeech"] = "roundLeech"; | ||
SubPhase["DowngradeLab"] = "down-lab"; | ||
SubPhase["PickRewards"] = "pickRewards"; | ||
})(SubPhase = exports.SubPhase || (exports.SubPhase = {})); | ||
//# sourceMappingURL=enums.js.map |
@@ -35,17 +35,9 @@ "use strict"; | ||
const remaining = spec.substr(operatorString.length).trimLeft(); | ||
return [op, remaining, 0]; | ||
return [op, remaining]; | ||
} | ||
} | ||
// If there's one space in the string, that means that the second part HAS to be a condition | ||
if (spec.split(" ").length === 2) { | ||
const [operator, remaining] = spec.split(" "); | ||
const toPick = parseInt(operator, 10); | ||
return [operator.slice(("" + toPick).length), remaining, toPick]; | ||
} | ||
return [enums_1.Operator.Once, spec, 0]; | ||
return [enums_1.Operator.Once, spec]; | ||
} | ||
class Event { | ||
constructor(spec, source) { | ||
/** Number of rewards to pick. Default to ALL */ | ||
this.toPick = 0; | ||
this.activated = false; | ||
@@ -72,3 +64,3 @@ if (typeof spec === "object") { | ||
[this.condition, remaining] = findCondition(this.spec); | ||
[this.operator, remaining, this.toPick] = findOperator(remaining); | ||
[this.operator, remaining] = findOperator(remaining); | ||
this.rewards = reward_1.default.parse(remaining); | ||
@@ -75,0 +67,0 @@ } |
@@ -51,2 +51,39 @@ "use strict"; | ||
}, | ||
//frontiers | ||
[enums_1.Building.Colony]: { | ||
cost: "~", | ||
income: [ | ||
["+3c", "+3pw", "+3vp", "2vp"], | ||
["+3c", "+3pw", "+4vp", "2vp"], | ||
["+3c", "+3pw", "+5vp", "2vp"], | ||
], | ||
}, | ||
[enums_1.Building.ColonyShip]: { | ||
cost: "4c,3o", | ||
income: [[], [], []], | ||
}, | ||
[enums_1.Building.ConstructionShip]: { | ||
cost: "3c,2o", | ||
income: [[], [], []], | ||
}, | ||
[enums_1.Building.ResearchShip]: { | ||
cost: "3c,2o", | ||
income: [[], [], []], | ||
}, | ||
[enums_1.Building.TradeShip]: { | ||
cost: "5c,1o", | ||
income: [[], [], []], | ||
}, | ||
[enums_1.Building.Scout]: { | ||
cost: "1c,1o", | ||
income: [[], [], []], | ||
}, | ||
[enums_1.Building.Frigate]: { | ||
cost: "3c,2o", | ||
income: [[], [], []], | ||
}, | ||
[enums_1.Building.BattleShip]: { | ||
cost: "5c,5o", | ||
income: [[], [], []], | ||
}, | ||
}, | ||
@@ -78,3 +115,3 @@ income: ["3k,4o,15c,q", "+o,k"], | ||
} | ||
cost(targetPlanet, building, isolated = true) { | ||
cost(building, isolated = true) { | ||
if (building === enums_1.Building.TradingStation && isolated) { | ||
@@ -81,0 +118,0 @@ return this.buildings[building].isolatedCost; |
@@ -53,2 +53,8 @@ "use strict"; | ||
this.buildings = lodash_1.fromPairs(Object.values(enums_1.Building).map((bld) => [bld, 0])); | ||
this.destroyedShips = lodash_1.fromPairs(Object.values(enums_1.Building) | ||
.filter((b) => enums_1.isShip(b)) | ||
.map((bld) => [bld, 0])); | ||
this.deployedShips = lodash_1.fromPairs(Object.values(enums_1.Building) | ||
.filter((b) => enums_1.isShip(b)) | ||
.map((bld) => [bld, 0])); | ||
this.satellites = 0; | ||
@@ -64,2 +70,3 @@ this.research = { | ||
this.range = 1; | ||
this.shipRange = 2; | ||
/** Total number of gaiaformers gained (including those on the board & the gaia area) */ | ||
@@ -79,2 +86,3 @@ this.gaiaformers = 0; | ||
this.occupied = []; | ||
this.ships = []; | ||
this.tokenModifier = 1; | ||
@@ -109,4 +117,8 @@ this.lostPlanet = 0; | ||
buildings: this.buildings, | ||
destroyedShips: this.destroyedShips, | ||
deployedShips: this.deployedShips, | ||
federationCount: this.federationCount, | ||
lostPlanet: this.lostPlanet, | ||
ships: this.ships, | ||
shipRange: this.shipRange, | ||
}; | ||
@@ -113,0 +125,0 @@ return ret; |
@@ -181,10 +181,4 @@ "use strict"; | ||
} | ||
gainRewards(rewards, source, toPick = 0) { | ||
if (toPick) { | ||
this.data.toPick = { count: toPick, rewards: [...rewards], source }; | ||
this.emit("pick-rewards"); | ||
} | ||
else { | ||
this.data.gainRewards(rewards.map((rew) => this.factionReward(rew, source)), false, source); | ||
} | ||
gainRewards(rewards, source) { | ||
this.data.gainRewards(rewards.map((rew) => this.factionReward(rew, source)), false, source); | ||
} | ||
@@ -204,3 +198,3 @@ maxPayRange(cost) { | ||
} | ||
canBuild(targetPlanet, building, lastRound, replay, { isolated, addedCost, existingBuilding, } = {}) { | ||
canBuild(map, hex, targetPlanet, building, lastRound, replay, { isolated, addedCost, existingBuilding, } = {}) { | ||
if (this.data.buildings[building] >= this.maxBuildings(building)) { | ||
@@ -228,3 +222,3 @@ // Too many buildings of the same kind | ||
} | ||
else if (building === enums_1.Building.Mine) { | ||
else if (building === enums_1.Building.Mine || building === enums_1.Building.Colony) { | ||
// habitability costs | ||
@@ -264,3 +258,3 @@ if (targetPlanet === enums_1.Planet.Gaia) { | ||
} | ||
const cost = reward_1.default.merge(this.board.cost(targetPlanet, building, isolated), addedCost); | ||
const cost = reward_1.default.merge(this.board.cost(building, isolated), addedCost); | ||
const creditCost = (r) => r.filter((r) => r.type === enums_1.Resource.Credit)[0].count; | ||
@@ -278,10 +272,17 @@ if (building === enums_1.Building.TradingStation && | ||
} | ||
if (!this.data.canPay(cost)) { | ||
return null; | ||
if (hex && this.faction !== enums_1.Faction.Ivits) { | ||
for (const h of map.withinDistance(hex, 1)) { | ||
if (h.belongsToFederationOf(this.player)) { | ||
warnings.push("building-will-be-part-of-federation"); | ||
break; | ||
} | ||
} | ||
} | ||
return { | ||
cost, | ||
steps, | ||
warnings, | ||
}; | ||
return !this.data.canPay(cost) | ||
? null | ||
: { | ||
cost, | ||
steps, | ||
warnings, | ||
}; | ||
} | ||
@@ -334,3 +335,3 @@ maxBuildings(building) { | ||
const times = this.eventConditionCount(event.condition); | ||
this.gainRewards(event.rewards.map((reward) => new reward_1.default(reward.count * times, reward.type)), event.source, event.toPick); | ||
this.gainRewards(event.rewards.map((reward) => new reward_1.default(reward.count * times, reward.type)), event.source); | ||
} | ||
@@ -408,3 +409,3 @@ } | ||
// excluding Gaiaformers as occupied | ||
if (building !== enums_1.Building.GaiaFormer) { | ||
if (building !== enums_1.Building.GaiaFormer && !enums_1.isShip(building)) { | ||
if (!wasOccupied) { | ||
@@ -450,13 +451,18 @@ this.data.occupied.push(hex); | ||
const isAdditionalMine = !upgradedBuilding && hex.occupied(); | ||
if (isAdditionalMine) { | ||
hex.data.additionalMine = this.player; | ||
if (this.data.hasPlanetaryInstitute()) { | ||
this.gainRewards([new reward_1.default("2k")], enums_1.Faction.Lantids); | ||
} | ||
if (enums_1.isShip(building)) { | ||
this.placeShip(building, hex); | ||
} | ||
else { | ||
hex.data.building = building; | ||
hex.data.player = this.player; | ||
if (isAdditionalMine) { | ||
hex.data.additionalMine = this.player; | ||
if (this.data.hasPlanetaryInstitute()) { | ||
this.gainRewards([new reward_1.default("2k")], enums_1.Faction.Lantids); | ||
} | ||
} | ||
else { | ||
hex.data.building = building; | ||
hex.data.player = this.player; | ||
} | ||
this.addBuildingToNearbyFederation(building, hex, map); | ||
} | ||
this.addBuildingToNearbyFederation(building, hex, map); | ||
// get triggered income for new building | ||
@@ -486,2 +492,19 @@ this.receiveBuildingTriggerIncome(building, hex.data.planet, isAdditionalMine); | ||
} | ||
placeShip(ship, hex) { | ||
this.data.ships.push({ type: ship, location: hex.toString(), moved: false, player: this.player }); | ||
} | ||
findUnmovedShip(ship, location) { | ||
return this.data.ships.find((s) => s.location === location && s.type === ship && !s.moved); | ||
} | ||
removeShip(ship, destroyed) { | ||
const l = this.data.ships; | ||
l.splice(l.indexOf(ship), 1); | ||
if (destroyed) { | ||
this.data.destroyedShips[ship.type]++; | ||
this.data.buildings[ship.type]--; | ||
} | ||
else { | ||
this.data.deployedShips[ship.type]++; | ||
} | ||
} | ||
resetTemporaryVariables() { | ||
@@ -591,3 +614,3 @@ // reset temporary benefits | ||
if (event.condition === condition) { | ||
this.gainRewards(event.rewards, event.source, event.toPick); | ||
this.gainRewards(event.rewards, event.source); | ||
} | ||
@@ -594,0 +617,0 @@ } |
@@ -7,3 +7,3 @@ "use strict"; | ||
[enums_1.ResearchField.Terraforming]: [[], ["2o"], ["d"], ["d", "3pw"], ["2o"], []], | ||
[enums_1.ResearchField.Navigation]: [[], ["q"], ["r"], ["q", "3pw"], ["r"], ["r"]], | ||
[enums_1.ResearchField.Navigation]: [[], ["q"], ["r", "ship-range"], ["q", "3pw"], ["r", "ship-range"], ["r", "2ship-range"]], | ||
[enums_1.ResearchField.Intelligence]: [[], ["q"], ["q"], ["2q", "3pw"], ["2q"], ["4q"]], | ||
@@ -10,0 +10,0 @@ [enums_1.ResearchField.GaiaProject]: [[], [">gf"], ["3t"], [">gf", "3pw"], [">gf"], ["4vp", "g > vp"]], |
@@ -6,4 +6,5 @@ "use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.possibleSetupBoardActions = exports.applySetupOption = exports.initCustomSetup = exports.SetupType = void 0; | ||
exports.possibleSetupBoardActions = exports.applySetupOption = exports.initCustomSetup = exports.applyRandomBoardSetup = exports.SetupType = void 0; | ||
const assert_1 = __importDefault(require("assert")); | ||
const shuffle_seed_1 = __importDefault(require("shuffle-seed")); | ||
const enums_1 = require("./enums"); | ||
@@ -81,3 +82,3 @@ var SetupType; | ||
} | ||
const factories = (engine) => [ | ||
const getFactories = (engine, nbPlayers = engine.players.length) => [ | ||
{ | ||
@@ -87,3 +88,3 @@ type: SetupType.Booster, | ||
for (const b of enums_1.Booster.values(engine.expansions)) { | ||
engine.tiles.boosters[b] = false; | ||
delete engine.tiles.boosters[b]; | ||
} | ||
@@ -95,3 +96,4 @@ }, | ||
const used = boosters.filter((b) => engine.tiles.boosters[b]).length; | ||
if (used < engine.players.length + 3) { | ||
// Choose nbPlayers+3 boosters as part of the pool | ||
if (used < nbPlayers + 3) { | ||
const b = left[0]; | ||
@@ -109,3 +111,3 @@ return { | ||
}, | ||
techFactory(engine, SetupType.TechTile, enums_1.TechTilePos.values(engine.expansions), enums_1.TechTile.values(engine.expansions), engine.players.length), | ||
techFactory(engine, SetupType.TechTile, enums_1.TechTilePos.values(engine.expansions), enums_1.TechTile.values(engine.expansions), nbPlayers), | ||
techFactory(engine, SetupType.AdvTechTile, enums_1.AdvTechTilePos.values(engine.expansions), enums_1.AdvTechTile.values(engine.expansions), 1), | ||
@@ -157,4 +159,21 @@ { | ||
]; | ||
function applyRandomBoardSetup(engine, seed, nbPlayers) { | ||
//map has too many quirks to keep test cases compatible | ||
const factories = getFactories(engine, nbPlayers).filter((f) => f.type !== SetupType.MapTile); | ||
for (const factory of factories) { | ||
factory.init(); | ||
let options; | ||
let next; | ||
while ((next = factory.nextAvailable()) !== null) { | ||
if (!options) { | ||
//only shuffle once for compatability with old test cases | ||
options = shuffle_seed_1.default.shuffle(next.options, engine.map.rng()); | ||
} | ||
factory.applyOption(options.shift(), next.position); | ||
} | ||
} | ||
} | ||
exports.applyRandomBoardSetup = applyRandomBoardSetup; | ||
function initCustomSetup(engine) { | ||
for (const factory of factories(engine)) { | ||
for (const factory of getFactories(engine)) { | ||
factory.init(); | ||
@@ -165,3 +184,3 @@ } | ||
function nextAvailableSetupOption(engine) { | ||
for (const factory of factories(engine)) { | ||
for (const factory of getFactories(engine)) { | ||
const o = factory.nextAvailable(); | ||
@@ -179,3 +198,3 @@ if (o) { | ||
function applySetupOption(engine, type, position, option) { | ||
for (const factory of factories(engine)) { | ||
for (const factory of getFactories(engine)) { | ||
const o = factory.nextAvailable(); | ||
@@ -182,0 +201,0 @@ if (o) { |
@@ -20,2 +20,5 @@ "use strict"; | ||
} | ||
if (expansions.includes("frontiers")) { | ||
options.frontiers = true; | ||
} | ||
const engine = new engine_1.default([`init ${nbPlayers} ${seed}`], options); | ||
@@ -22,0 +25,0 @@ engine.generateAvailableCommandsIfNeeded(); |
@@ -19,5 +19,7 @@ import { finalRankings, gainFinalScoringVictoryPoints } from "./src/algorithms/scoring"; | ||
AvailableBuilding, | ||
AvailableFederation, | ||
AvailableFreeAction, | ||
AvailableFreeActionData, | ||
AvailableHex, | ||
AvailableMoveShipData, | ||
AvailableResearchData, | ||
@@ -31,2 +33,3 @@ AvailableResearchTrack, | ||
HighlightHex, | ||
MAX_SHIPS_PER_HEX, | ||
} from "./src/available-command"; | ||
@@ -53,2 +56,3 @@ export { | ||
FinalTile, | ||
isShip, | ||
Operator, | ||
@@ -71,3 +75,3 @@ Phase, | ||
export { factionPlanet } from "./src/factions"; | ||
export { federationCost, parseFederationLocation } from "./src/federation"; | ||
export { federationCost, FederationInfo, parseFederationLocation } from "./src/federation"; | ||
export { GaiaHex, GaiaHexData } from "./src/gaia-hex"; | ||
@@ -74,0 +78,0 @@ export { applyChargePowers } from "./src/income"; |
{ | ||
"name": "@gaia-project/engine", | ||
"version": "4.8.22", | ||
"version": "4.8.23", | ||
"description": "Javascript engine for project gaia", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
@@ -11,3 +11,3 @@ import { difference, range, uniq } from "lodash"; | ||
} from "./actions"; | ||
import { upgradedBuildings } from "./buildings"; | ||
import { stdBuildingValue, upgradedBuildings } from "./buildings"; | ||
import { qicForDistance } from "./cost"; | ||
@@ -25,2 +25,3 @@ import Engine, { AuctionVariant, BoardActions } from "./engine"; | ||
Federation, | ||
isShip, | ||
Operator, | ||
@@ -32,2 +33,3 @@ Phase, | ||
Resource, | ||
Ship, | ||
SubPhase, | ||
@@ -49,2 +51,4 @@ TechTile, | ||
export const UPGRADE_RESEARCH_COST = "4k"; | ||
export const MAX_SHIPS_PER_HEX = 3; | ||
const SHIP_ACTION_RANGE = 1; | ||
@@ -127,2 +131,12 @@ export type BrainstoneWarning = "brainstone-charges-wasted"; | ||
export enum ShipAction { | ||
BuildColony = "buildColony", | ||
} | ||
export type AvailableShipAction = { type: ShipAction; locations: AvailableBuilding[] }; | ||
export type AvailableShipTarget = { location: AvailableHex; actions: AvailableShipAction[] }; | ||
export type AvailableMoveShipData = { ship: Building; source: string; targets: AvailableShipTarget[] }; | ||
interface CommandData { | ||
@@ -149,2 +163,3 @@ [Command.Action]: AvailableBoardActionData; | ||
[Command.PlaceLostPlanet]: { spaces: AvailableHex[] }; | ||
[Command.MoveShip]: AvailableMoveShipData[]; | ||
[Command.RotateSectors]: never; | ||
@@ -161,3 +176,9 @@ [Command.Setup]: AvailableSetupOption; | ||
export type HighlightHex = { cost?: string; warnings?: BuildWarning[] }; | ||
export type HighlightHex = { | ||
cost?: string; | ||
warnings?: BuildWarning[]; | ||
building?: Building; | ||
hideBuilding?: Building; | ||
preventClick?: boolean; | ||
}; | ||
export type AvailableHex = HighlightHex & { coordinates: string }; | ||
@@ -214,5 +235,8 @@ | ||
return [{ name: Command.BrainStone, player, data }]; | ||
// case SubPhase.MoveShip: | ||
// return possibleShipMovements(engine, player); | ||
case SubPhase.BeforeMove: { | ||
return [ | ||
...possibleBuildings(engine, player), | ||
...possibleShipMovements(engine, player), | ||
...possibleFederations(engine, player), | ||
@@ -301,3 +325,3 @@ ...possibleResearchAreas(engine, player, UPGRADE_RESEARCH_COST), | ||
const check = pl.canBuild(planet, building, lastRound, replay, { | ||
const check = pl.canBuild(map, hex, planet, building, lastRound, replay, { | ||
addedCost: [new Reward(qicNeeded.amount, Resource.Qic)], | ||
@@ -336,6 +360,24 @@ }); | ||
export function shipsInHex(location: string, data): Ship[] { | ||
return data.players.flatMap((p) => p.data.ships).filter((s) => s.location === location); | ||
} | ||
function possibleShips(pl: PlayerObject, engine: Engine, map: SpaceMap, hex: GaiaHex) { | ||
const buildings: AvailableBuilding[] = []; | ||
for (const ship of Object.values(Building).filter((b) => isShip(b))) { | ||
const check = pl.canBuild(null, null, null, ship, engine.isLastRound, engine.replay); | ||
if (check) { | ||
for (const h of map.withinDistance(hex, 1)) { | ||
if (!h.hasPlanet() && shipsInHex(h.toString(), engine).length < MAX_SHIPS_PER_HEX) { | ||
buildings.push(newAvailableBuilding(ship, h, check, false)); | ||
} | ||
} | ||
} | ||
} | ||
return buildings; | ||
} | ||
export function possibleBuildings(engine: Engine, player: Player): AvailableCommand<Command.Build>[] { | ||
const map = engine.map; | ||
const pl = engine.player(player); | ||
const { data } = pl; | ||
const buildings: AvailableBuilding[] = []; | ||
@@ -345,5 +387,13 @@ | ||
// upgrade existing player's building | ||
if (hex.buildingOf(player)) { | ||
const building = hex.buildingOf(player); | ||
const building = hex.buildingOf(player); | ||
if (building) { | ||
// excluding Transdim planet until transformed into Gaia planets | ||
if (hex.data.planet === Planet.Transdim) { | ||
continue; | ||
} | ||
if (stdBuildingValue(building) > 0 && engine.expansions === Expansion.Frontiers) { | ||
buildings.push(...possibleShips(pl, engine, map, hex)); | ||
} | ||
if (player !== hex.data.player) { | ||
@@ -354,7 +404,2 @@ // This is a secondary building, so we can't upgrade it | ||
// excluding Transdim planet until transformed into Gaia planets | ||
if (hex.data.planet === Planet.Transdim) { | ||
continue; | ||
} | ||
// Lost planet can't be upgraded | ||
@@ -373,3 +418,3 @@ if (hex.data.planet === Planet.Lost) { | ||
for (const _pl of engine.players) { | ||
if (_pl !== engine.player(player)) { | ||
if (_pl !== pl) { | ||
for (const loc of _pl.data.occupied) { | ||
@@ -386,6 +431,6 @@ if (loc.hasStructure() && map.distance(loc, hex) < ISOLATED_DISTANCE) { | ||
const upgraded = upgradedBuildings(building, engine.player(player).faction); | ||
const upgraded = upgradedBuildings(building, pl.faction); | ||
for (const upgrade of upgraded) { | ||
const check = engine.player(player).canBuild(hex.data.planet, upgrade, engine.isLastRound, engine.replay, { | ||
const check = pl.canBuild(map, hex, hex.data.planet, upgrade, engine.isLastRound, engine.replay, { | ||
isolated, | ||
@@ -414,3 +459,3 @@ existingBuilding: building, | ||
player, | ||
data: { buildings }, | ||
data: { buildings: uniq(buildings) }, //ship locations may be duplicated | ||
}, | ||
@@ -423,6 +468,84 @@ ]; | ||
function shipTargets( | ||
source: string, | ||
hex: string, | ||
range: number, | ||
targets: AvailableHex[], | ||
engine: Engine | ||
): AvailableHex[] { | ||
if (!targets.find((t) => t.coordinates === hex) && shipsInHex(hex, engine).length < MAX_SHIPS_PER_HEX) { | ||
targets.push({ coordinates: hex }); | ||
} | ||
if (range === 0) { | ||
return targets; | ||
} | ||
const map = engine.map; | ||
for (const h of map.withinDistance(map.getS(hex), 1)) { | ||
const c = h.toString(); | ||
if (!h.hasPlanet() && c !== source) { | ||
shipTargets(source, c, range - 1, targets, engine); | ||
} | ||
} | ||
return targets; | ||
} | ||
function possibleColonyShipActions(engine: Engine, ship: Ship, shipLocation: string): AvailableShipAction[] { | ||
const map = engine.map; | ||
const pl = engine.player(ship.player); | ||
const locations: AvailableHex[] = map.withinDistance(map.getS(shipLocation), SHIP_ACTION_RANGE).flatMap((h) => { | ||
if (h.hasPlanet() && !h.occupied() && h.data.planet !== Planet.Transdim) { | ||
const check = pl.canBuild(map, h, h.data.planet, Building.Colony, engine.isLastRound, engine.replay); | ||
if (check) { | ||
return [newAvailableBuilding(Building.Colony, h, check, false)]; | ||
} | ||
} | ||
return []; | ||
}); | ||
if (locations.length > 0) { | ||
return [ | ||
{ | ||
type: ShipAction.BuildColony, | ||
locations, | ||
} as AvailableShipAction, | ||
]; | ||
} | ||
return []; | ||
} | ||
function possibleShipActions(engine: Engine, ship: Ship, shipLocation: string): AvailableShipAction[] { | ||
switch (ship.type) { | ||
case Building.ColonyShip: | ||
return possibleColonyShipActions(engine, ship, shipLocation); | ||
} | ||
return []; | ||
} | ||
export function possibleShipMovements(engine: Engine, player: Player): AvailableCommand<Command.MoveShip>[] { | ||
const pl = engine.player(player); | ||
const ships = pl.data.ships.filter((s) => !s.moved); | ||
if (ships.length === 0) { | ||
return []; | ||
} | ||
const shipRange = engine.player(player).data.shipRange; | ||
return [ | ||
{ | ||
name: Command.MoveShip, | ||
player, | ||
data: ships.map((s) => ({ | ||
ship: s.type, | ||
source: s.location, | ||
targets: shipTargets(s.location, s.location, shipRange, [], engine).map((t) => ({ | ||
location: t, | ||
actions: possibleShipActions(engine, s, t.coordinates), | ||
})), | ||
})), | ||
}, | ||
]; | ||
} | ||
export function possibleSpaceStations(engine: Engine, player: Player): AvailableCommand<Command.Build>[] { | ||
const map = engine.map; | ||
const pl = engine.player(player); | ||
const { data } = pl; | ||
const buildings = []; | ||
@@ -436,4 +559,3 @@ | ||
const building = Building.SpaceStation; | ||
addPossibleNewPlanet(map, hex, pl, pl.planet, building, buildings, engine.isLastRound, engine.replay); | ||
addPossibleNewPlanet(map, hex, pl, pl.planet, Building.SpaceStation, buildings, engine.isLastRound, engine.replay); | ||
} | ||
@@ -440,0 +562,0 @@ |
@@ -30,2 +30,3 @@ import { Building, Faction } from ".."; | ||
case Building.Academy2: | ||
case Building.Colony: | ||
return 3; | ||
@@ -32,0 +33,0 @@ } |
import assert from "assert"; | ||
import { isEqual, range, set, uniq } from "lodash"; | ||
import shuffleSeed from "shuffle-seed"; | ||
import { version } from "../package.json"; | ||
@@ -9,2 +8,3 @@ import { boardActions } from "./actions"; | ||
import AvailableCommand, { | ||
AvailableBuilding, | ||
AvailableFreeActionData, | ||
@@ -14,2 +14,3 @@ BrainstoneActionData, | ||
Offer, | ||
ShipAction, | ||
} from "./available-command"; | ||
@@ -28,2 +29,3 @@ import { stdBuildingValue } from "./buildings"; | ||
FinalTile, | ||
isShip, | ||
Operator, | ||
@@ -53,2 +55,3 @@ Phase, | ||
import { | ||
applyRandomBoardSetup, | ||
applySetupOption, | ||
@@ -66,3 +69,2 @@ initCustomSetup, | ||
// const ISOLATED_DISTANCE = 3; | ||
const LEECHING_DISTANCE = 2; | ||
@@ -101,2 +103,4 @@ | ||
flexibleFederations?: boolean; | ||
/** Frontiers expansion */ | ||
frontiers?: boolean; | ||
/** auction */ | ||
@@ -239,3 +243,3 @@ auction?: AuctionVariant; | ||
techs: {}, | ||
scorings: { round: null, final: null }, | ||
scorings: { round: [], final: [] }, | ||
federations: {}, | ||
@@ -256,4 +260,4 @@ }; | ||
get expansions() { | ||
return 0; | ||
get expansions(): Expansion { | ||
return 0 | (this.options.frontiers ? Expansion.Frontiers : 0); | ||
} | ||
@@ -497,3 +501,2 @@ | ||
}); | ||
player.on("pick-rewards", () => this.processNextMove(SubPhase.PickRewards)); | ||
@@ -1017,3 +1020,3 @@ /* For advanced log */ | ||
this.checkCommand(move.command); | ||
(this[move.command] as any)(this.playerToMove, ...move.args); | ||
(this[move.command === Command.MoveShip ? "_move" : move.command] as any)(this.playerToMove, ...move.args); | ||
@@ -1203,2 +1206,5 @@ return move; | ||
player.loadEvents(this.currentRoundScoringEvents); | ||
player.data.ships?.forEach((s) => { | ||
s.moved = false; | ||
}); | ||
} | ||
@@ -1550,20 +1556,4 @@ | ||
// Choose nbPlayers+3 boosters as part of the pool | ||
const boosters = shuffleSeed.shuffle(Booster.values(this.expansions), this.map.rng()).slice(0, nbPlayers + 3); | ||
for (const booster of boosters) { | ||
this.tiles.boosters[booster] = true; | ||
} | ||
applyRandomBoardSetup(this, seed, nbPlayers); | ||
// Shuffle tech tiles | ||
const techtiles = shuffleSeed.shuffle(TechTile.values(this.expansions), this.map.rng()); | ||
TechTilePos.values(this.expansions).forEach((pos, i) => { | ||
this.tiles.techs[pos] = { tile: techtiles[i], count: nbPlayers }; | ||
}); | ||
// Choose adv tech tiles as part of the pool | ||
const advtechtiles = shuffleSeed.shuffle(AdvTechTile.values(this.expansions), this.map.rng()); | ||
AdvTechTilePos.values(this.expansions).forEach((pos, i) => { | ||
this.tiles.techs[pos] = { tile: advtechtiles[i], count: 1 }; | ||
}); | ||
// powerActions | ||
@@ -1574,17 +1564,2 @@ BoardAction.values(this.expansions).forEach((pos: BoardAction) => { | ||
const feds = Federation.values(); | ||
this.terraformingFederation = shuffleSeed.shuffle(feds, this.map.rng())[0]; | ||
for (const federation of feds) { | ||
this.tiles.federations[federation] = 3; | ||
} | ||
this.tiles.federations[this.terraformingFederation] -= 1; | ||
// Choose roundScoring Tiles as part of the pool | ||
const roundscoringtiles = shuffleSeed.shuffle(ScoringTile.values(this.expansions), this.map.rng()).slice(0, 6); | ||
this.tiles.scorings.round = roundscoringtiles; | ||
// Choose finalScoring Tiles as part of the pool | ||
const finalscoringtiles = shuffleSeed.shuffle(FinalTile.values(this.expansions), this.map.rng()).slice(0, 2); | ||
this.tiles.scorings.final = finalscoringtiles; | ||
this.players = []; | ||
@@ -1682,12 +1657,3 @@ this.setup = []; | ||
if (elem.building === building && isEqual(this.map.parse(elem.coordinates), parsed)) { | ||
const { q, r } = this.map.parse(location); | ||
const hex = this.map.grid.get({ q, r }); | ||
pl.build(building, hex, Reward.parse(elem.cost), this.map, elem.steps); | ||
// will trigger a LeechPhase | ||
if (this.phase === Phase.RoundMove) { | ||
this.leechSources.unshift({ player, coordinates: location }); | ||
} | ||
this.placeBuilding(pl, elem); | ||
return; | ||
@@ -1703,2 +1669,12 @@ } | ||
private placeBuilding(pl: Player, building: AvailableBuilding) { | ||
const hex = this.map.getS(building.coordinates); | ||
pl.build(building.building, hex, Reward.parse(building.cost), this.map, building.steps); | ||
// will trigger a LeechPhase | ||
if ((this.phase === Phase.RoundMove || this.phase === Phase.RoundShip) && !isShip(building.building)) { | ||
this.leechSources.unshift({ player: pl.player, coordinates: building.coordinates }); | ||
} | ||
} | ||
[Command.UpgradeResearch](player: PlayerEnum, field: ResearchField) { | ||
@@ -1835,2 +1811,46 @@ const { tracks } = this.avCommand<Command.UpgradeResearch>().data; | ||
[`_${Command.MoveShip}`]( | ||
player: PlayerEnum, | ||
shipType: Building, | ||
source: string, | ||
dest: string, | ||
actionType?: ShipAction, | ||
actionLocation?: string | ||
) { | ||
const pl = this.player(player); | ||
const data = this.avCommand<Command.MoveShip>().data; | ||
const shipCommand = data.find((s) => s.ship === shipType && s.source === source); | ||
assert(shipCommand, `There is no ship ${shipType} at ${source}`); | ||
const target = shipCommand.targets.find((t) => t.location.coordinates === dest); | ||
assert(target, `The ship ${shipType} doesn't have the range to move from ${source} to ${dest}`); | ||
const ship = pl.findUnmovedShip(shipType, source); | ||
assert(ship, `No ${shipType} at ${source} (or has already moved)`); | ||
ship.moved = true; | ||
ship.location = dest; | ||
const actions = target.actions; | ||
if (actionType) { | ||
const action = actions?.find((a) => a.type === actionType); | ||
assert(action, `action ${actionType} not possible for ship ${shipType} at ship location ${dest}`); | ||
assert(actionLocation, "no action location provided"); | ||
const location = action.locations.find((l) => l.coordinates === actionLocation); | ||
assert(location, `action ${actionType} not possible for ship ${shipType} at action location ${actionLocation}`); | ||
switch (actionType) { | ||
case ShipAction.BuildColony: | ||
this.placeBuilding(pl, location); | ||
pl.removeShip(ship, false); | ||
break; | ||
} | ||
} | ||
} | ||
[Command.Spend](player: PlayerEnum, costS: string, _for: "for", incomeS: string) { | ||
@@ -1942,4 +1962,3 @@ const command = this.avCommand<Command.Spend>(); | ||
if (isEqual(this.map.parse(elem.coordinates), parsed)) { | ||
const { q, r } = this.map.parse(location); | ||
const hex = this.map.grid.get({ q, r }); | ||
const hex = this.map.getS(location); | ||
@@ -1946,0 +1965,0 @@ if (hex.buildingOf(player) === Building.Mine) { |
@@ -25,3 +25,5 @@ export enum Planet { | ||
export enum Expansion { | ||
All = 1, | ||
// 1 was the old spaceships expansion | ||
Frontiers = 2, | ||
All = 2, | ||
} | ||
@@ -149,4 +151,37 @@ | ||
SpaceStation = "sp", | ||
//frontiers | ||
Colony = "colony", | ||
ColonyShip = "colonyShip", | ||
TradeShip = "tradeShip", | ||
ConstructionShip = "constructionShip", | ||
ResearchShip = "researchShip", | ||
Scout = "scout", | ||
Frigate = "frigate", | ||
BattleShip = "battleShip", | ||
} | ||
export type Ship = { | ||
type: Building; | ||
player: Player; | ||
location: string; | ||
moved: boolean; | ||
}; | ||
export function isShip(b: Building) { | ||
switch (b) { | ||
case Building.ColonyShip: | ||
case Building.TradeShip: | ||
case Building.ConstructionShip: | ||
case Building.ResearchShip: | ||
case Building.Scout: | ||
case Building.Frigate: | ||
case Building.BattleShip: | ||
return true; | ||
} | ||
return false; | ||
} | ||
export enum Faction { | ||
@@ -187,5 +222,5 @@ Terrans = "terrans", | ||
Init = "init", | ||
MoveShip = "move", | ||
PISwap = "swap-PI", | ||
Pass = "pass", | ||
PickReward = "pick", | ||
PISwap = "swap-PI", | ||
PlaceLostPlanet = "lostPlanet", | ||
@@ -500,2 +535,3 @@ RotateSectors = "rotate", | ||
RoundGaia = "roundGaia", | ||
RoundShip = "roundShip", | ||
RoundMove = "roundMove", | ||
@@ -522,3 +558,2 @@ RoundLeech = "roundLeech", | ||
DowngradeLab = "down-lab", | ||
PickRewards = "pickRewards", | ||
} |
@@ -40,3 +40,3 @@ import { | ||
function findOperator(spec: string): [Operator, string, number] { | ||
function findOperator(spec: string): [Operator, string] { | ||
let operatorMatch = /^(.+?)(\b| )/.exec(spec); | ||
@@ -53,14 +53,7 @@ | ||
const remaining = spec.substr(operatorString.length).trimLeft(); | ||
return [op, remaining, 0]; | ||
return [op, remaining]; | ||
} | ||
} | ||
// If there's one space in the string, that means that the second part HAS to be a condition | ||
if (spec.split(" ").length === 2) { | ||
const [operator, remaining] = spec.split(" "); | ||
const toPick = parseInt(operator, 10); | ||
return [operator.slice(("" + toPick).length) as Operator, remaining, toPick]; | ||
} | ||
return [Operator.Once, spec, 0]; | ||
return [Operator.Once, spec]; | ||
} | ||
@@ -93,4 +86,2 @@ | ||
rewards: Reward[]; | ||
/** Number of rewards to pick. Default to ALL */ | ||
toPick = 0; | ||
activated = false; | ||
@@ -119,3 +110,3 @@ | ||
[this.condition, remaining] = findCondition(this.spec); | ||
[this.operator, remaining, this.toPick] = findOperator(remaining); | ||
[this.operator, remaining] = findOperator(remaining); | ||
this.rewards = Reward.parse(remaining); | ||
@@ -122,0 +113,0 @@ } |
import { get, set } from "lodash"; | ||
import { FactionVariant } from "../engine"; | ||
import { Building, Command, Faction, Operator, Phase, Planet, PowerArea } from "../enums"; | ||
import { Building, Command, Faction, Operator, Phase, PowerArea } from "../enums"; | ||
import Event from "../events"; | ||
@@ -64,2 +64,3 @@ import Player from "../player"; | ||
}, | ||
[Building.GaiaFormer]: { | ||
@@ -73,2 +74,42 @@ cost: "6t->tg", | ||
}, | ||
//frontiers | ||
[Building.Colony]: { | ||
cost: "~", | ||
income: [ | ||
["+3c", "+3pw", "+3vp", "2vp"], | ||
["+3c", "+3pw", "+4vp", "2vp"], | ||
["+3c", "+3pw", "+5vp", "2vp"], | ||
], | ||
}, | ||
[Building.ColonyShip]: { | ||
cost: "4c,3o", | ||
income: [[], [], []], | ||
}, | ||
[Building.ConstructionShip]: { | ||
cost: "3c,2o", | ||
income: [[], [], []], | ||
}, | ||
[Building.ResearchShip]: { | ||
cost: "3c,2o", | ||
income: [[], [], []], | ||
}, | ||
[Building.TradeShip]: { | ||
cost: "5c,1o", | ||
income: [[], [], []], | ||
}, | ||
[Building.Scout]: { | ||
cost: "1c,1o", | ||
income: [[], [], []], | ||
}, | ||
[Building.Frigate]: { | ||
cost: "3c,2o", | ||
income: [[], [], []], | ||
}, | ||
[Building.BattleShip]: { | ||
cost: "5c,5o", | ||
income: [[], [], []], | ||
}, | ||
}, | ||
@@ -123,3 +164,3 @@ income: ["3k,4o,15c,q", "+o,k"], | ||
cost(targetPlanet: Planet, building: Building, isolated = true): Reward[] { | ||
cost(building: Building, isolated = true): Reward[] { | ||
if (building === Building.TradingStation && isolated) { | ||
@@ -126,0 +167,0 @@ return this.buildings[building].isolatedCost; |
@@ -6,2 +6,4 @@ import assert from "assert"; | ||
type PlayerShip = { player: Player; ship: Building; moved: boolean }; | ||
export interface GaiaHexData { | ||
@@ -8,0 +10,0 @@ planet: Planet; |
@@ -374,3 +374,3 @@ import assert from "assert"; | ||
getS(coords: string) { | ||
getS(coords: string): GaiaHex { | ||
return this.grid.get(this.parse(coords)); | ||
@@ -377,0 +377,0 @@ } |
@@ -12,5 +12,7 @@ import { EventEmitter } from "eventemitter3"; | ||
Federation, | ||
isShip, | ||
PowerArea, | ||
ResearchField, | ||
Resource, | ||
Ship, | ||
TechTile, | ||
@@ -69,2 +71,18 @@ TechTilePos, | ||
destroyedShips: { | ||
[key in Building]: number; | ||
} = fromPairs( | ||
Object.values(Building) | ||
.filter((b) => isShip(b)) | ||
.map((bld) => [bld, 0]) | ||
) as any; | ||
deployedShips: { | ||
[key in Building]: number; | ||
} = fromPairs( | ||
Object.values(Building) | ||
.filter((b) => isShip(b)) | ||
.map((bld) => [bld, 0]) | ||
) as any; | ||
satellites = 0; | ||
@@ -82,2 +100,3 @@ research: { | ||
range = 1; | ||
shipRange = 2; | ||
/** Total number of gaiaformers gained (including those on the board & the gaia area) */ | ||
@@ -104,2 +123,3 @@ gaiaformers = 0; | ||
occupied: GaiaHex[] = []; | ||
ships: Ship[] = []; | ||
leechPossible: number; | ||
@@ -138,4 +158,8 @@ tokenModifier = 1; | ||
buildings: this.buildings, | ||
destroyedShips: this.destroyedShips, | ||
deployedShips: this.deployedShips, | ||
federationCount: this.federationCount, | ||
lostPlanet: this.lostPlanet, | ||
ships: this.ships, | ||
shipRange: this.shipRange, | ||
}; | ||
@@ -142,0 +166,0 @@ |
@@ -17,3 +17,3 @@ import { expect } from "chai"; | ||
const { cost } = player.canBuild(Planet.Terra, Building.Mine, false, false, { | ||
const { cost } = player.canBuild(null, null, Planet.Terra, Building.Mine, false, false, { | ||
addedCost: [new Reward(1, Resource.Qic)], | ||
@@ -20,0 +20,0 @@ }); |
@@ -18,2 +18,3 @@ import assert from "assert"; | ||
FinalTile, | ||
isShip, | ||
Operator, | ||
@@ -26,2 +27,3 @@ Phase, | ||
Resource, | ||
Ship, | ||
TechPos, | ||
@@ -91,3 +93,4 @@ TechTile, | ||
| "gaia-former-would-extend-range" | ||
| "gaia-former-last-round"; | ||
| "gaia-former-last-round" | ||
| "building-will-be-part-of-federation"; | ||
@@ -248,13 +251,8 @@ export type BuildCheck = { cost: Reward[]; steps: number; warnings: BuildWarning[] }; | ||
gainRewards(rewards: Reward[], source: EventSource, toPick = 0) { | ||
if (toPick) { | ||
this.data.toPick = { count: toPick, rewards: [...rewards], source }; | ||
this.emit("pick-rewards"); | ||
} else { | ||
this.data.gainRewards( | ||
rewards.map((rew) => this.factionReward(rew, source)), | ||
false, | ||
source | ||
); | ||
} | ||
gainRewards(rewards: Reward[], source: EventSource) { | ||
this.data.gainRewards( | ||
rewards.map((rew) => this.factionReward(rew, source)), | ||
false, | ||
source | ||
); | ||
} | ||
@@ -279,3 +277,5 @@ | ||
canBuild( | ||
targetPlanet: Planet, | ||
map: SpaceMap, | ||
hex: GaiaHex | null, | ||
targetPlanet: Planet | null, | ||
building: Building, | ||
@@ -319,3 +319,3 @@ lastRound: boolean, | ||
addedCost.push(new Reward(-this.data.gaiaFormingDiscount(), Resource.MoveTokenToGaiaArea)); | ||
} else if (building === Building.Mine) { | ||
} else if (building === Building.Mine || building === Building.Colony) { | ||
// habitability costs | ||
@@ -357,3 +357,3 @@ if (targetPlanet === Planet.Gaia) { | ||
const cost = Reward.merge(this.board.cost(targetPlanet, building, isolated), addedCost); | ||
const cost = Reward.merge(this.board.cost(building, isolated), addedCost); | ||
const creditCost = (r: Reward[]) => r.filter((r) => r.type === Resource.Credit)[0].count; | ||
@@ -374,11 +374,18 @@ if ( | ||
} | ||
if (hex && this.faction !== Faction.Ivits) { | ||
for (const h of map.withinDistance(hex, 1)) { | ||
if (h.belongsToFederationOf(this.player)) { | ||
warnings.push("building-will-be-part-of-federation"); | ||
break; | ||
} | ||
} | ||
} | ||
if (!this.data.canPay(cost)) { | ||
return null; | ||
} | ||
return { | ||
cost, | ||
steps, | ||
warnings, | ||
}; | ||
return !this.data.canPay(cost) | ||
? null | ||
: { | ||
cost, | ||
steps, | ||
warnings, | ||
}; | ||
} | ||
@@ -444,4 +451,3 @@ | ||
event.rewards.map((reward) => new Reward(reward.count * times, reward.type)), | ||
event.source, | ||
event.toPick | ||
event.source | ||
); | ||
@@ -538,3 +544,3 @@ } | ||
// excluding Gaiaformers as occupied | ||
if (building !== Building.GaiaFormer) { | ||
if (building !== Building.GaiaFormer && !isShip(building)) { | ||
if (!wasOccupied) { | ||
@@ -584,12 +590,16 @@ this.data.occupied.push(hex); | ||
if (isAdditionalMine) { | ||
hex.data.additionalMine = this.player; | ||
if (this.data.hasPlanetaryInstitute()) { | ||
this.gainRewards([new Reward("2k")], Faction.Lantids); | ||
if (isShip(building)) { | ||
this.placeShip(building, hex); | ||
} else { | ||
if (isAdditionalMine) { | ||
hex.data.additionalMine = this.player; | ||
if (this.data.hasPlanetaryInstitute()) { | ||
this.gainRewards([new Reward("2k")], Faction.Lantids); | ||
} | ||
} else { | ||
hex.data.building = building; | ||
hex.data.player = this.player; | ||
} | ||
} else { | ||
hex.data.building = building; | ||
hex.data.player = this.player; | ||
this.addBuildingToNearbyFederation(building, hex, map); | ||
} | ||
this.addBuildingToNearbyFederation(building, hex, map); | ||
@@ -625,2 +635,21 @@ // get triggered income for new building | ||
placeShip(ship: Building, hex: GaiaHex) { | ||
this.data.ships.push({ type: ship, location: hex.toString(), moved: false, player: this.player }); | ||
} | ||
findUnmovedShip(ship: Building, location: string): Ship | null { | ||
return this.data.ships.find((s) => s.location === location && s.type === ship && !s.moved); | ||
} | ||
removeShip(ship: Ship, destroyed: boolean) { | ||
const l = this.data.ships; | ||
l.splice(l.indexOf(ship), 1); | ||
if (destroyed) { | ||
this.data.destroyedShips[ship.type]++; | ||
this.data.buildings[ship.type]--; | ||
} else { | ||
this.data.deployedShips[ship.type]++; | ||
} | ||
} | ||
resetTemporaryVariables() { | ||
@@ -749,3 +778,3 @@ // reset temporary benefits | ||
if (event.condition === condition) { | ||
this.gainRewards(event.rewards, event.source, event.toPick); | ||
this.gainRewards(event.rewards, event.source); | ||
} | ||
@@ -752,0 +781,0 @@ } |
@@ -5,3 +5,3 @@ import { ResearchField } from "./enums"; | ||
[ResearchField.Terraforming]: [[], ["2o"], ["d"], ["d", "3pw"], ["2o"], []], | ||
[ResearchField.Navigation]: [[], ["q"], ["r"], ["q", "3pw"], ["r"], ["r"]], | ||
[ResearchField.Navigation]: [[], ["q"], ["r", "ship-range"], ["q", "3pw"], ["r", "ship-range"], ["r", "2ship-range"]], | ||
[ResearchField.Intelligence]: [[], ["q"], ["q"], ["2q", "3pw"], ["2q"], ["4q"]], | ||
@@ -8,0 +8,0 @@ [ResearchField.GaiaProject]: [[], [">gf"], ["3t"], [">gf", "3pw"], [">gf"], ["4vp", "g > vp"]], |
import assert from "assert"; | ||
import shuffleSeed from "shuffle-seed"; | ||
import AvailableCommand from "./available-command"; | ||
@@ -117,3 +118,3 @@ import Engine from "./engine"; | ||
const factories = (engine: Engine): SetupFactory[] => [ | ||
const getFactories = (engine: Engine, nbPlayers = engine.players.length): SetupFactory[] => [ | ||
{ | ||
@@ -123,3 +124,3 @@ type: SetupType.Booster, | ||
for (const b of Booster.values(engine.expansions)) { | ||
engine.tiles.boosters[b] = false; | ||
delete engine.tiles.boosters[b]; | ||
} | ||
@@ -131,3 +132,4 @@ }, | ||
const used = boosters.filter((b) => engine.tiles.boosters[b]).length; | ||
if (used < engine.players.length + 3) { | ||
// Choose nbPlayers+3 boosters as part of the pool | ||
if (used < nbPlayers + 3) { | ||
const b = left[0]; | ||
@@ -150,3 +152,3 @@ return { | ||
TechTile.values(engine.expansions), | ||
engine.players.length | ||
nbPlayers | ||
), | ||
@@ -218,4 +220,24 @@ techFactory( | ||
export function applyRandomBoardSetup(engine: Engine, seed: string, nbPlayers: number) { | ||
//map has too many quirks to keep test cases compatible | ||
const factories = getFactories(engine, nbPlayers).filter((f) => f.type !== SetupType.MapTile); | ||
for (const factory of factories) { | ||
factory.init(); | ||
let options: SetupOption[]; | ||
let next: SetupFactoryOption; | ||
while ((next = factory.nextAvailable()) !== null) { | ||
if (!options) { | ||
//only shuffle once for compatability with old test cases | ||
options = shuffleSeed.shuffle(next.options, engine.map.rng()); | ||
} | ||
factory.applyOption(options.shift(), next.position); | ||
} | ||
} | ||
} | ||
export function initCustomSetup(engine: Engine) { | ||
for (const factory of factories(engine)) { | ||
for (const factory of getFactories(engine)) { | ||
factory.init(); | ||
@@ -226,3 +248,3 @@ } | ||
function nextAvailableSetupOption(engine: Engine): AvailableSetupOption | null { | ||
for (const factory of factories(engine)) { | ||
for (const factory of getFactories(engine)) { | ||
const o = factory.nextAvailable(); | ||
@@ -241,3 +263,3 @@ if (o) { | ||
export function applySetupOption(engine: Engine, type: SetupType, position: SetupPosition, option: SetupOption) { | ||
for (const factory of factories(engine)) { | ||
for (const factory of getFactories(engine)) { | ||
const o = factory.nextAvailable(); | ||
@@ -244,0 +266,0 @@ if (o) { |
@@ -22,2 +22,6 @@ import assert from "assert"; | ||
if (expansions.includes("frontiers")) { | ||
options.frontiers = true; | ||
} | ||
const engine = new Engine([`init ${nbPlayers} ${seed}`], options); | ||
@@ -24,0 +28,0 @@ engine.generateAvailableCommandsIfNeeded(); |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1064024
22065