UNPKG

spore-archetype

Version:

Represents the various rules for space-stage Archetypes based on previous alignment choices in the 2008 game Spore.

241 lines (216 loc) 9.82 kB
import { CardColor, CellCard, CivilizationCard, CreatureCard, getCardColor, TribalCard } from "./cards"; export type Sequence = [CellCard, CreatureCard, TribalCard, CivilizationCard]; export function* generateAllValidSequences() { for (const cell of [CellCard.Skipped, CellCard.Carnivore, CellCard.Herbivore, CellCard.Omnivore]) { for (const creature of [CreatureCard.Skipped, CreatureCard.Predator, CreatureCard.Social, CreatureCard.Adaptable]) { if (creature === CreatureCard.Skipped && cell !== CellCard.Skipped) { continue; } for (const tribal of [TribalCard.Skipped, TribalCard.Aggressive, TribalCard.Friendly, TribalCard.Industrious]) { if (tribal === TribalCard.Skipped && creature !== CreatureCard.Skipped) { continue; } for (const civilization of [CivilizationCard.Skipped, CivilizationCard.Military, CivilizationCard.Religious, CivilizationCard.Economic]) { if (civilization === CivilizationCard.Skipped && tribal !== TribalCard.Skipped) { continue; } yield [cell, creature, tribal, civilization] as Sequence; } } } } } export enum Archetype { Wanderer, // meta-archetype : pure Warrior, Shaman, Trader, // meta-archetype : tendency Knight, Ecologist, Bard, // meta-archetype : half Diplomat, Scientist, Zealot, } /** * if you have three of the same color, or all of the same color, you are pure of that color * elif you have one of each exactly, you are pure of the last (civ) one * elif you have one card for each color but one more of one color (1-1-2), you are a tendency of that color * elif you have cards of exactly two colors with no 3 cards of the same color, you are half of those two colors * @param nullIfInvalid if the sequence is inconsistent and this is true, return null instead of throwing an error */ export function getArchetype(sequence: Sequence, nullIfInvalid?: false): Archetype; export function getArchetype(sequence: Sequence, nullIfInvalid: true): Archetype | null; export default function getArchetype(sequence: Sequence, nullIfInvalid: boolean = false): Archetype | null { if (!isConsistent(sequence)) { if (nullIfInvalid) { return null; } throw new Error("Invalid sequence"); } const counter = getCounter(sequence); return getWanderer(counter) || getPure(sequence, counter) || getTendency(counter) || getHalf(counter); } function isConsistent(sequence: Sequence) { // consistency check let isSkipped = true; for (const phase of sequence) { if (getCardColor(phase) !== CardColor.Black) { isSkipped = false; } else if (!isSkipped) { return false; } } return true; } function getCounter(sequence: Sequence): Map<CardColor, number> { // number of phases with each color (except black) const counter: Map<CardColor, number> = new Map(); for (const phase of sequence) { const color = getCardColor(phase); if (color !== CardColor.Black) { counter.set(color, (counter.get(color) || 0) + 1); } } counter.delete(CardColor.Black); return counter; } function getWanderer(counter: Map<CardColor, number>): Archetype.Wanderer | null { // return sequence.every(phase => getCardColor(phase) === CardColor.Black) ? Archetype.Wanderer : null; return counter.size === 0 ? Archetype.Wanderer : null; } function getPure(sequence: Sequence, counter: Map<CardColor, number>): Archetype.Warrior | Archetype.Shaman | Archetype.Trader | null { // all non-skipped of the same color if (counter.size === 1) { const color = counter.keys().next().value as CardColor; switch (color) { case CardColor.Red: return Archetype.Warrior; case CardColor.Green: return Archetype.Shaman; case CardColor.Blue: return Archetype.Trader; } } // (at least) three of the same color for (const [color, count] of counter) { if (count >= 3) { switch (color) { case CardColor.Red: return Archetype.Warrior; case CardColor.Green: return Archetype.Shaman; case CardColor.Blue: return Archetype.Trader; } } } // one of each exactly if (counter.get(CardColor.Red) === 1 && counter.get(CardColor.Green) === 1 && counter.get(CardColor.Blue) === 1) { // return the last color, which is the color of the last phase switch (sequence[3]) { case CivilizationCard.Military: return Archetype.Warrior; case CivilizationCard.Religious: return Archetype.Shaman; case CivilizationCard.Economic: return Archetype.Trader; } } return null; // not pure } function getTendency(counter: Map<CardColor, number>): Archetype.Knight | Archetype.Ecologist | Archetype.Bard | null { // one card for each color but one more of one color (1-1-2) // which means, 3 different colors, and one with a value of 2 if (counter.size === 3) { for (const [color, count] of counter) { if (count === 2) { switch (color) { case CardColor.Red: return Archetype.Knight; case CardColor.Green: return Archetype.Ecologist; case CardColor.Blue: return Archetype.Bard; default: // no other color can have 2 if the counter has all 3, skip other loops return null; } } } } return null; } function getHalf(counter: Map<CardColor, number>): Archetype.Diplomat | Archetype.Scientist | Archetype.Zealot | null { // cards of exactly two colors with no 3 cards of the same color if (counter.size === 2 && !Array.from(counter.values()).some(count => count >= 3)) { if (!counter.has(CardColor.Red)) { return Archetype.Diplomat; } else if (!counter.has(CardColor.Green)) { return Archetype.Scientist; } else if (!counter.has(CardColor.Blue)) { return Archetype.Zealot; } throw new Error("Invalid sequence"); } return null; } /** * Test for the consistency of the rules for each path. * test that for each sequence, either: * - the sequence has a black after a non-black, and is invalid * - the sequence is all-black, wanderer, and returns null for all three checkers * - the sequence returns a value for exactly one of the checkers */ export function testAll() { const pathsCounter: Map<Archetype, number> = new Map(); for (const sequence of generateAllValidSequences()) { const counter = getCounter(sequence); const pure = getPure(sequence, counter); const tendency = getTendency(counter); const half = getHalf(counter); if (pure !== null) { if (tendency !== null) { throw new Error(`Sequence ${sequence} got non-null for pure and tendency`); } else if (half !== null) { throw new Error(`Sequence ${sequence} got non-null for pure and half`); } else { pathsCounter.set(pure, (pathsCounter.get(pure) || 0) + 1); } } else if (tendency !== null) { if (half !== null) { throw new Error(`Sequence ${sequence} got non-null for tendency and half`); } else { pathsCounter.set(tendency, (pathsCounter.get(tendency) || 0) + 1); } } else if (half !== null) { pathsCounter.set(half, (pathsCounter.get(half) || 0) + 1); } else { // all three checkers returned null // test that the sequence is all-black if (!sequence.every(phase => getCardColor(phase) === CardColor.Black)) { throw new Error(`Sequence ${sequence} got null for all checkers`); } else { pathsCounter.set(Archetype.Wanderer, (pathsCounter.get(Archetype.Wanderer) || 0) + 1); } } } console.log(`Number of paths, expecting 121 : ${Array.from(pathsCounter.values()).reduce((a, b) => a + b, 0)}`); console.log(`Number of Wanderer paths, expecting 1 : ${pathsCounter.get(Archetype.Wanderer)}`); console.log("Number of pure paths, expecting 14 each :"); console.log(`Warrior : ${pathsCounter.get(Archetype.Warrior)}`); console.log(`Shaman : ${pathsCounter.get(Archetype.Shaman)}`); console.log(`Trader : ${pathsCounter.get(Archetype.Trader)}`); console.log("Number of tendency paths, expecting 12 each :"); console.log(`Knight : ${pathsCounter.get(Archetype.Knight)}`); console.log(`Ecologist : ${pathsCounter.get(Archetype.Ecologist)}`); console.log(`Bard : ${pathsCounter.get(Archetype.Bard)}`); console.log("Number of half paths, expecting 14 each :"); console.log(`Diplomat : ${pathsCounter.get(Archetype.Diplomat)}`); console.log(`Scientist : ${pathsCounter.get(Archetype.Scientist)}`); console.log(`Zealot : ${pathsCounter.get(Archetype.Zealot)}`); return pathsCounter; }