From 96dd2c3357e1735eaa486c2b02f6e5ef2ad3162e Mon Sep 17 00:00:00 2001 From: William Bartlett Date: Sun, 12 Feb 2023 11:29:15 +0100 Subject: [PATCH] Keepers in Iron --- src/action-parser.ts | 6 +- src/interfaces/actions.ts | 11 ++- src/interfaces/rootgame.ts | 16 +++- src/parsers/index.ts | 1 + src/parsers/keepers.ts | 72 +++++++++++++++ src/utils/regex-former.ts | 10 ++- test/keepers.spec.ts | 173 +++++++++++++++++++++++++++++++++++++ 7 files changed, 280 insertions(+), 9 deletions(-) create mode 100644 src/parsers/keepers.ts create mode 100644 test/keepers.spec.ts diff --git a/src/action-parser.ts b/src/action-parser.ts index 3ffe642..a41667b 100644 --- a/src/action-parser.ts +++ b/src/action-parser.ts @@ -1,5 +1,5 @@ import { RootActionClearPath, RootActionCombat, RootActionCraft, RootActionDominance, RootActionGainVP, RootActionMove, RootActionReveal, RootActionUpdateFunds, RootCard, RootCardName, RootFaction, RootFactionBoard, RootItem, RootItemState, RootPiece, RootPieceType, RootLocation, RootSuit, RootThing, RootVagabondRelationshipStatus, RootForest, RootActionPlot, RootActionType, RootVagabondItemSpecial } from './interfaces'; -import { parseConspiracyAction, parseCultAction, parseDuchyAction, parseEyrieAction, parseHundredsAction, parseMarquiseAction, parseRiverfolkAction, parseVagabondAction, parseWoodlandAction } from './parsers'; +import { parseConspiracyAction, parseCultAction, parseDuchyAction, parseEyrieAction, parseHundredsAction, parseKeepersAction, parseMarquiseAction, parseRiverfolkAction, parseVagabondAction, parseWoodlandAction } from './parsers'; import { splitAction } from './utils/action-splitter'; import { extendCardName } from './utils/card-name-utils'; import { formRegex } from './utils/regex-former'; @@ -33,7 +33,7 @@ const EXTENDED_MOVE_REGEX = formRegex('[Number|||countMoved]^'); +const ADD_TO_RETINUE_REGEX = formRegex('[Number|||countAdded]K->$_') +const DISCARD_FROM_RETINUE_REGEX = formRegex('$_->') + +export function parseAddToRetinue(actions: string[]): RootActionMove { + + const movingComponents = []; + + for (let action of actions) { + const result = action.match(ADD_TO_RETINUE_REGEX); + const component = { + number: +(result.groups.countAdded || 1), + thing: parseCard(result.groups.cardAdded), + start: RootFaction.Keepers, + destination: { + faction: RootFaction.Keepers, + sublocation: +result.groups.columnAdded, + } + }; + + movingComponents.push(component); + } + + return { + things: movingComponents + }; + +} + +export function parseKeepersAction(action: string): RootAction { + + if (FLIP_RELIC_REGEX.test(action)) { + const result = action.match(FLIP_RELIC_REGEX); + + return { + type: RootActionType.FlipRelic, + relic: result.groups.relicFlipped, + clearing: +result.groups.relicClearing + }; + } + + if (DISCARD_FROM_RETINUE_REGEX.test(action)) { + const result = action.match(DISCARD_FROM_RETINUE_REGEX); + const card = parseCard(result.groups.cardDiscarded); + + return { + things: [{ + number: 1, + thing: card, + start: { + faction: RootFaction.Keepers, + sublocation: +result.groups.columnDiscarded + }, + destination: (card.cardName === RootCardName.FaithfulRetainer) ? null : 'Discard pile' + }] + } + } + + const simpleActions = splitAction(action); + + if (simpleActions.every(act => ADD_TO_RETINUE_REGEX.test(act))) { + return parseAddToRetinue(simpleActions); + } + + return null; + +} diff --git a/src/utils/regex-former.ts b/src/utils/regex-former.ts index dbcb3f8..2b78e77 100644 --- a/src/utils/regex-former.ts +++ b/src/utils/regex-former.ts @@ -1,4 +1,4 @@ -import { RootCardName, RootDuchyMinisterSpecial, RootEyrieLeaderSpecial, RootEyrieSpecial, RootFaction, RootHundredsMoodSpecial, RootItem, RootItemState, RootLizardOutcastSpecial, RootPieceType, RootQuestCard, RootRiverfolkPriceSpecial, RootSpecialCardName, RootSuit, RootVagabondCharacterSpecial, RootVagabondItemSpecial, RootVagabondRelationshipStatus } from '../interfaces'; +import { RootCardName, RootDuchyMinisterSpecial, RootEyrieLeaderSpecial, RootEyrieSpecial, RootFaction, RootHundredsMoodSpecial, RootKeepersSpecial, RootItem, RootItemState, RootLizardOutcastSpecial, RootPieceType, RootQuestCard, RootRiverfolkPriceSpecial, RootSpecialCardName, RootSuit, RootVagabondCharacterSpecial, RootVagabondItemSpecial, RootVagabondRelationshipStatus } from '../interfaces'; const DIVIDER_BEFORE_GROUP_NAME = '|||'; // arbitrarily chosen to be a divider that will never appear in Rootlog code @@ -17,8 +17,8 @@ const HAND = `(${ALL_FACTIONS})`; // A facti // In order from most-to-least specific, including the discard pile represented by * const ALL_LOCATIONS = `(${FOREST}|${CLEARING}|${FACTION_BOARD}|${HAND}|${ALL_ITEM_STATE}|\\*)`; -// [Faction][_], subtype must be one letter -const PIECE_REGEX_STRING = `(${ALL_FACTIONS})?(${ALL_PIECE_TYPES})(_[a-z])?`; +// [Faction][_], subtypes must be one letter or digit +const PIECE_REGEX_STRING = `(${ALL_FACTIONS})?(${ALL_PIECE_TYPES})(_[a-z0-9](_[a-z])?)?`; // [Suit]#[CardName] const CARD_REGEX_STRING = `(${ALL_SUITS})?#${ALL_CARD_NAMES}?`; // % or %_ to represent 'all items' @@ -45,6 +45,8 @@ const DUCHY_SPECIFIC_LOCATIONS = `(0)`; // The Burrow const DUCHY_MINISTERS = `(${Object.values(RootDuchyMinisterSpecial).join('|')})`; // Lord of the Hundreds const HUNDREDS_MOODS = `(${Object.values(RootHundredsMoodSpecial).join('|')})` +// Keepers in Iron +const KEEPERS_COLUMNS = `(${Object.values(RootKeepersSpecial).join('|')})`; const EXTENDED_LOCATIONS = `(${VAGABOND_SPECIFIC_LOCATIONS}|${DUCHY_SPECIFIC_LOCATIONS}|${ALL_LOCATIONS}|\\*)`; // includes faction-specific card names @@ -99,6 +101,8 @@ const parseForRegexString = function(str: string): string { return _parseForRegexString(DUCHY_MINISTERS, groupName); case ('mood'): return _parseForRegexString(HUNDREDS_MOODS, groupName); + case ('retinue'): + return _parseForRegexString(KEEPERS_COLUMNS, groupName); case ('extendedlocation'): return _parseForRegexString(EXTENDED_LOCATIONS, groupName); case ('extendedcomponent'): diff --git a/test/keepers.spec.ts b/test/keepers.spec.ts new file mode 100644 index 0000000..692d5d7 --- /dev/null +++ b/test/keepers.spec.ts @@ -0,0 +1,173 @@ +import test from 'ava-ts'; +import { parseAction } from '../src/action-parser'; + +import { RootActionMove, RootActionType, RootFaction, RootFactionBoard, RootSuit } from '../src/interfaces'; + +test('Correctly parses placing a Keepers waystation', t => { + + const result = parseAction('b_t_j->3', RootFaction.Keepers); + + t.deepEqual(result.things, [{ + number: 1, + thing: { + faction: RootFaction.Keepers, + piece: 'b_t_j', + pieceType: 'b' + }, + start: null, + destination: 3 + }]); +}); + +test('Correctly parses delving a relic', t => { + + const result = parseAction('t_j1_2_5_10->10', RootFaction.Keepers); + + t.deepEqual(result.things, [{ + number: 1, + thing: { + faction: RootFaction.Keepers, + piece: 't_j', + pieceType: 't' + }, + start: { + clearings: [1, 2, 5, 10], + }, + destination: 10 + }]); +}); + +test('Correctly parses flipping a relic', t => { + + const result = parseAction('t3^t_2_j', RootFaction.Keepers); + + t.deepEqual(result, { + type: RootActionType.FlipRelic, + clearing: 3, + relic: 't_2_j' + }); +}); + +test('Correctly parses recovering a relic', t => { + + const result = parseAction('t_2_j2->$', RootFaction.Keepers); + + t.deepEqual(result.things, [{ + number: 1, + thing: { + faction: RootFaction.Keepers, + piece: 't_2_j', + pieceType: 't' + }, + start: 2, + destination: { + faction: RootFaction.Keepers + } + }]); +}); + +test('Correctly parses adding a card to the retinue', t => { + + const result = parseAction('M#K->$_1', RootFaction.Keepers); + + t.deepEqual(result.things, [{ + number: 1, + thing: { + suit: RootSuit.Mouse, + cardName: null, + }, + start: RootFaction.Keepers, + destination: { + faction: RootFaction.Keepers, + sublocation: 1 + } + }]); +}); + +test('Correctly parses discarding a Faithful Retainer from the retinue', t => { + + const result = parseAction('#faith$_1->', RootFaction.Keepers); + + t.deepEqual(result.things, [{ + number: 1, + thing: { + suit: null, + cardName: 'faith', + }, + start: { + faction: RootFaction.Keepers, + sublocation: 1, + }, + destination: null + }]); +}); + +test('Correctly parses discarding a deck card from the retinue', t => { + + const result = parseAction('B#$_2->', RootFaction.Keepers); + + t.deepEqual(result.things, [{ + number: 1, + thing: { + suit: RootSuit.Bird, + cardName: null, + }, + start: { + faction: RootFaction.Keepers, + sublocation: 2, + }, + destination: 'Discard pile' + }]); +}); + +test('Correctly parses removing relic after battle', t => { + + const result = parseAction('Kt_2_j3->', RootFaction.Eyrie); + + t.deepEqual(result.things, [{ + number: 1, + thing: { + faction: RootFaction.Keepers, + piece: 't_2_j', + pieceType: 't', + }, + start: 3, + destination: null + }]); +}); + +test('Correctly parses placing relic in forest during setup', t => { + + const result = parseAction('t_j->1_2_5_10', RootFaction.Keepers); + + t.deepEqual(result.things, [{ + number: 1, + thing: { + faction: RootFaction.Keepers, + piece: 't_j', + pieceType: 't', + }, + start: null, + destination: { + clearings: [1, 2, 5, 10] + } + }]); +}); + +test('Correctly parses placing relic in forest after removal', t => { + + const result = parseAction('Kt_2_j->1_2_5_10', RootFaction.Eyrie); + + t.deepEqual(result.things, [{ + number: 1, + thing: { + faction: RootFaction.Keepers, + piece: 't_2_j', + pieceType: 't', + }, + start: null, + destination: { + clearings: [1, 2, 5, 10] + } + }]); +});