diff --git a/src/services/BotActions.ts b/src/services/BotActions.ts new file mode 100644 index 0000000..372dcd3 --- /dev/null +++ b/src/services/BotActions.ts @@ -0,0 +1,142 @@ +import Card, { DiceAction, ScoringAction } from './Card' +import Action from './enum/Action' +import DifficultyLevel from './enum/DifficultyLevel' +import ScoringActionType from './enum/ScoringActionType' +import ScoringCategory from './enum/ScoringCategory' +import NavigationState from '@/util/NavigationState' + +/** + * Manages the bot actions. + */ +export default class BotActions { + + readonly _items : ActionItem[] + readonly _reset : boolean + + public constructor(currentCard: Card, navigationState: NavigationState) { + const { blueDotCount, redDotCount, round, difficultyLevel, + eraScoringTiles, finalScoringTiles, actionRoll } = navigationState + + // check reset + this._reset = (blueDotCount >= 4 && redDotCount >= 4) + + // collect actions + if (!this.isReset && currentCard.diceActions && currentCard.diceActionsAdvanced) { + if (isAdvanced(round, difficultyLevel)) { + this._items = getDiceActions(currentCard.diceActionsAdvanced, actionRoll) + } + else { + this._items = getDiceActions(currentCard.diceActions, actionRoll) + } + } + else if (!this.isReset && currentCard.scoringAction && currentCard.scoringActionAdvanced) { + if (isAdvanced(round, difficultyLevel)) { + this._items = getScoringActions(currentCard.scoringActionAdvanced, round, eraScoringTiles, finalScoringTiles) + } + else { + this._items = getScoringActions(currentCard.scoringAction, round, eraScoringTiles, finalScoringTiles) + } + } + else { + this._items = [] + } + } + + public get items() : readonly ActionItem[] { + return this._items + } + + public get isReset() : boolean { + return this._reset + } + +} + +export interface ActionItem { + action: Action + scoringCategory?: ScoringCategory + count?: number +} + +function isAdvanced(round: number, difficultyLevel: DifficultyLevel) : boolean { + switch (difficultyLevel) { + case DifficultyLevel.BEGINNER: + return false + case DifficultyLevel.MODERATE: + return round >= 4 + case DifficultyLevel.MEDIUM: + return round >= 3 + case DifficultyLevel.ADVANCED: + return round >= 2 + case DifficultyLevel.EXPERT: + return true + default: + throw new Error(`Invalid difficulty level: ${difficultyLevel}`) + } +} + +function getDiceActions(diceActions: DiceAction[], actionRoll: number) : ActionItem[] { + return diceActions. + filter(item => item.values.includes(actionRoll)) + .map(item => { + const { action, scoringCategory, count } = item + const actionItem : ActionItem = { action } + if (scoringCategory) { + actionItem.scoringCategory = scoringCategory + } + if (count) { + actionItem.count = count + } + return actionItem + }) +} + +function getScoringActions(scoringAction: ScoringAction, round: number, + eraScoringTiles: ScoringCategory[], finalScoringTiles: ScoringCategory[]) : ActionItem[] { + const { scoringActionType, count, vpCount, alternativeLastEraVPCount } = scoringAction + switch (scoringActionType) { + case ScoringActionType.CURRENT_ERA_SCORING_CATEGORY: + return getEraScoringCategoryActions(getEraScoringCategory(eraScoringTiles, round), count, vpCount) + case ScoringActionType.NEXT_ERA_SCORING_CATEGORY: + if (round < 4) { + return getEraScoringCategoryActions(getEraScoringCategory(eraScoringTiles, round+1), count, vpCount) + } + else { + return [{ action:Action.GAIN_VP, count:alternativeLastEraVPCount }] + } + case ScoringActionType.LAST_ERA_SCORING_CATEGORY: + return getEraScoringCategoryActions(getEraScoringCategory(eraScoringTiles, 4), count, vpCount) + case ScoringActionType.FINAL_SCORING_CATEGORIES: + return getFinalScoringCategoryActions(finalScoringTiles, count, vpCount) + default: + throw new Error(`Invalid scoring action: ${scoringAction}`) + } +} + +function getEraScoringCategory(eraScoringTiles: ScoringCategory[], round:number) : ScoringCategory { + const scoringCategory = eraScoringTiles[round-1] + if (!scoringCategory) { + throw new Error(`Invalid era scoring category: ${round}`) + } + return scoringCategory +} + +function getEraScoringCategoryActions(scoringCategory: ScoringCategory, count: number, vpCount?: number) : ActionItem[] { + const items : ActionItem[] = [] + items.push({ action:Action.ADVANCE_SCORING_CATEGORY, scoringCategory, count }) + if (vpCount) { + items.push({ action:Action.GAIN_VP, count:vpCount }) + } + return items +} + +function getFinalScoringCategoryActions(scoringCategories: ScoringCategory[], count: number, vpCount?: number) : ActionItem[] { + const items : ActionItem[] = [] + scoringCategories.forEach(scoringCategory => { + items.push({ action:Action.ADVANCE_SCORING_CATEGORY, scoringCategory, count }) + }) + if (vpCount) { + items.push({ action:Action.GAIN_VP, count:vpCount }) + } + return items +} diff --git a/src/store/state.ts b/src/store/state.ts index 42f27a9..a6849ed 100644 --- a/src/store/state.ts +++ b/src/store/state.ts @@ -69,6 +69,8 @@ export interface BotPersistence { cardDeck: CardDeckPersistence evolutionCount: number prosperityCount: number + blueDotCount: number + redDotCount: number actionRoll: number territoryRoll: number beaconRoll: number diff --git a/src/util/NavigationState.ts b/src/util/NavigationState.ts index ddf040c..6350d2b 100644 --- a/src/util/NavigationState.ts +++ b/src/util/NavigationState.ts @@ -6,6 +6,8 @@ import Card from '@/services/Card' import Cards from '@/services/Cards' import rollDice from '@brdgm/brdgm-commons/src/util/random/rollDice' import Player from '@/services/enum/Player' +import DifficultyLevel from '@/services/enum/DifficultyLevel' +import ScoringCategory from '@/services/enum/ScoringCategory' export default class NavigationState { @@ -14,6 +16,10 @@ export default class NavigationState { readonly startPlayer : Player readonly player : Player + readonly difficultyLevel : DifficultyLevel + readonly eraScoringTiles : ScoringCategory[] + readonly finalScoringTiles : ScoringCategory[] + readonly cardDeck : CardDeck readonly evolutionCount : number readonly prosperityCount : number @@ -21,6 +27,8 @@ export default class NavigationState { readonly actionRoll : number readonly territoryRoll : number readonly beaconRoll : number + readonly blueDotCount : number + readonly redDotCount : number constructor(route: RouteLocation, state: State) { this.round = getIntRouteParam(route, 'round') @@ -34,12 +42,18 @@ export default class NavigationState { this.startPlayer = getStartPlayer(state, this.round) this.player = getPlayer(route, this.startPlayer) + this.difficultyLevel = state.setup.difficultyLevel + this.eraScoringTiles = state.setup.eraScoringTiles + this.finalScoringTiles = state.setup.finalScoringTiles + // try to load persistence with rolled die values for current turns const botPersistence = getBotPersistence(state, this.round, this.turn) if (botPersistence) { this.cardDeck = CardDeck.fromPersistence(botPersistence.cardDeck) this.evolutionCount = botPersistence.evolutionCount this.prosperityCount = botPersistence.prosperityCount + this.blueDotCount = botPersistence.blueDotCount + this.redDotCount = botPersistence.redDotCount this.actionRoll = botPersistence.actionRoll this.territoryRoll = botPersistence.territoryRoll this.beaconRoll = botPersistence.beaconRoll @@ -48,9 +62,15 @@ export default class NavigationState { else { const previousBotPersistence = getPreviousBotPersistence(state, this.round, this.turn) this.cardDeck = CardDeck.fromPersistence(previousBotPersistence.cardDeck) - // draw next card + // draw next card, count dots if (this.player == Player.BOT) { - this.cardDeck.draw() + const nextCard = this.cardDeck.draw() + this.blueDotCount = previousBotPersistence.blueDotCount + nextCard.blueDotCount + this.redDotCount = previousBotPersistence.redDotCount + nextCard.redDotCount + } + else { + this.blueDotCount = previousBotPersistence.blueDotCount + this.redDotCount = previousBotPersistence.redDotCount } // counters this.evolutionCount = previousBotPersistence.evolutionCount @@ -118,7 +138,13 @@ function getPreviousBotPersistence(state: State, round: number, turn: number) : // check previous round if (round > 1) { - return getPreviousBotPersistence(state, round - 1, MAX_TURN) + const lastRoundBotPersistence = getPreviousBotPersistence(state, round - 1, MAX_TURN) + if (lastRoundBotPersistence) { + // reset dot counters for new round + lastRoundBotPersistence.blueDotCount = 0 + lastRoundBotPersistence.redDotCount = 0 + return lastRoundBotPersistence + } } // get initial card deck @@ -130,6 +156,8 @@ function getPreviousBotPersistence(state: State, round: number, turn: number) : cardDeck: initialCardDeck, evolutionCount: 0, prosperityCount: 0, + blueDotCount: 0, + redDotCount: 0, actionRoll: 0, territoryRoll: 0, beaconRoll: 0 diff --git a/src/views/TurnBot.vue b/src/views/TurnBot.vue index ea79dd0..d8ca294 100644 --- a/src/views/TurnBot.vue +++ b/src/views/TurnBot.vue @@ -51,7 +51,8 @@ export default defineComponent({ }, methods: { saveTurn() : void { - const { player, cardDeck, evolutionCount, prosperityCount, actionRoll, territoryRoll, beaconRoll } = this.navigationState + const { player, cardDeck, evolutionCount, prosperityCount, blueDotCount, redDotCount, + actionRoll, territoryRoll, beaconRoll } = this.navigationState const turn : Turn = { round: this.round, turn: this.turn, @@ -60,6 +61,8 @@ export default defineComponent({ cardDeck: cardDeck.toPersistence(), evolutionCount, prosperityCount, + blueDotCount, + redDotCount, actionRoll, territoryRoll, beaconRoll diff --git a/tests/unit/helper/mockTurn.ts b/tests/unit/helper/mockTurn.ts index b634bb6..2c8859d 100644 --- a/tests/unit/helper/mockTurn.ts +++ b/tests/unit/helper/mockTurn.ts @@ -8,11 +8,15 @@ export default function (params?: MockTurnParams) : Turn { turn: params?.turn ?? 1, player: params?.player ?? Player.PLAYER } - if (params?.cardDeck || params?.evolutionCount || params?.prosperityCount || params?.actionRoll || params?.territoryRoll || params?.beaconRoll) { + if (params?.cardDeck || params?.evolutionCount || params?.prosperityCount + || params?.actionRoll || params?.territoryRoll || params?.beaconRoll + || params?.blueDotCount || params?.redDotCount) { turn.botPersistence = { cardDeck: params?.cardDeck ?? CardDeck.new().toPersistence(), evolutionCount: params?.evolutionCount ?? 0, prosperityCount: params?.prosperityCount ?? 0, + blueDotCount: params?.blueDotCount ?? 0, + redDotCount: params?.redDotCount ?? 0, actionRoll: params?.actionRoll ?? 0, territoryRoll: params?.territoryRoll ?? 0, beaconRoll: params?.beaconRoll ?? 0 @@ -28,6 +32,8 @@ export interface MockTurnParams { cardDeck?: CardDeckPersistence evolutionCount?: number prosperityCount?: number + blueDotCount?: number + redDotCount?: number actionRoll?: number territoryRoll?: number beaconRoll?: number diff --git a/tests/unit/services/BotActions.spec.ts b/tests/unit/services/BotActions.spec.ts new file mode 100644 index 0000000..d51ab86 --- /dev/null +++ b/tests/unit/services/BotActions.spec.ts @@ -0,0 +1,135 @@ +import NavigationState from '@/util/NavigationState' +import { expect } from 'chai' +import mockRouteLocation from '../helper/mockRouteLocation' +import mockState from '../helper/mockState' +import Player from '@/services/enum/Player' +import mockRound from '../helper/mockRound' +import mockTurn from '../helper/mockTurn' +import BotActions from '@/services/BotActions' +import Cards from '@/services/Cards' +import Action from '@/services/enum/Action' +import ScoringCategory from '@/services/enum/ScoringCategory' +import DifficultyLevel from '@/services/enum/DifficultyLevel' + +describe('services/BotActions', () => { + it('reset', () => { + const route = mockRouteLocation({name:'TurnBot', params:{round:'1',turn:'4'}}) + const navigationState = new NavigationState(route, getState(DifficultyLevel.BEGINNER)) + + const botActions = new BotActions(navigationState.currentCard, navigationState) + expect(botActions.isReset).to.eq(true) + expect(botActions.items.length).to.eq(0) + }) + + it('diceActions', () => { + const route = mockRouteLocation({name:'TurnBot', params:{round:'1',turn:'2'}}) + const navigationState = new NavigationState(route, getState(DifficultyLevel.BEGINNER)) + + const botActions = new BotActions(Cards.get(1), navigationState) + expect(botActions.isReset).to.eq(false) + expect(botActions.items).to.eql([ + { action: Action.ADVANCE_SCORING_CATEGORY, scoringCategory: ScoringCategory.TECHNOLOGY, count: 1 }, + { action: Action.GAIN_FATE_DIE } + ]) + }) + + it('diceActions-advanced', () => { + const route = mockRouteLocation({name:'TurnBot', params:{round:'1',turn:'2'}}) + const navigationState = new NavigationState(route, getState(DifficultyLevel.EXPERT)) + + const botActions = new BotActions(Cards.get(1), navigationState) + expect(botActions.isReset).to.eq(false) + expect(botActions.items).to.eql([ + { action: Action.ADVANCE_SCORING_CATEGORY, scoringCategory: ScoringCategory.TECHNOLOGY, count: 1 }, + { action: Action.GAIN_VP, count: 3 } + ]) + }) + + it('scoringActions-current-era-scoring-category', () => { + const route = mockRouteLocation({name:'TurnBot', params:{round:'1',turn:'2'}}) + const navigationState = new NavigationState(route, getState(DifficultyLevel.BEGINNER)) + + const botActions = new BotActions(Cards.get(12), navigationState) + expect(botActions.isReset).to.eq(false) + expect(botActions.items).to.eql([ + { action: Action.ADVANCE_SCORING_CATEGORY, scoringCategory: ScoringCategory.PROSPERITY, count: 2 } + ]) + }) + + it('scoringActions-current-era-scoring-category-advanced', () => { + const route = mockRouteLocation({name:'TurnBot', params:{round:'1',turn:'2'}}) + const navigationState = new NavigationState(route, getState(DifficultyLevel.EXPERT)) + + const botActions = new BotActions(Cards.get(12), navigationState) + expect(botActions.isReset).to.eq(false) + expect(botActions.items).to.eql([ + { action: Action.ADVANCE_SCORING_CATEGORY, scoringCategory: ScoringCategory.PROSPERITY, count: 2 }, + { action: Action.GAIN_VP, count: 3 } + ]) + }) + + it('scoringActions-next-era-scoring-category', () => { + const route = mockRouteLocation({name:'TurnBot', params:{round:'1',turn:'2'}}) + const navigationState = new NavigationState(route, getState(DifficultyLevel.BEGINNER)) + + const botActions = new BotActions(Cards.get(13), navigationState) + expect(botActions.isReset).to.eq(false) + expect(botActions.items).to.eql([ + { action: Action.ADVANCE_SCORING_CATEGORY, scoringCategory: ScoringCategory.POPULATION, count: 2 } + ]) + }) + + it('scoringActions-next-era-scoring-category-last-round', () => { + const route = mockRouteLocation({name:'TurnBot', params:{round:'4',turn:'2'}}) + const navigationState = new NavigationState(route, getState(DifficultyLevel.BEGINNER)) + + const botActions = new BotActions(Cards.get(13), navigationState) + expect(botActions.isReset).to.eq(false) + expect(botActions.items).to.eql([ + { action: Action.GAIN_VP, count: 4 } + ]) + }) + + it('scoringActions-last-era-scoring-category', () => { + const route = mockRouteLocation({name:'TurnBot', params:{round:'1',turn:'2'}}) + const navigationState = new NavigationState(route, getState(DifficultyLevel.BEGINNER)) + + const botActions = new BotActions(Cards.get(14), navigationState) + expect(botActions.isReset).to.eq(false) + expect(botActions.items).to.eql([ + { action: Action.ADVANCE_SCORING_CATEGORY, scoringCategory: ScoringCategory.KNOWLEDGE, count: 1 } + ]) + }) + + it('scoringActions-final-scoring-categories', () => { + const route = mockRouteLocation({name:'TurnBot', params:{round:'1',turn:'2'}}) + const navigationState = new NavigationState(route, getState(DifficultyLevel.EXPERT)) + + const botActions = new BotActions(Cards.get(15), navigationState) + expect(botActions.isReset).to.eq(false) + expect(botActions.items).to.eql([ + { action: Action.ADVANCE_SCORING_CATEGORY, scoringCategory: ScoringCategory.EVOLUTION, count: 1 }, + { action: Action.ADVANCE_SCORING_CATEGORY, scoringCategory: ScoringCategory.PRESTIGE, count: 1 }, + { action: Action.ADVANCE_SCORING_CATEGORY, scoringCategory: ScoringCategory.TECHNOLOGY, count: 1 }, + { action: Action.GAIN_VP, count: 3 } + ]) + }) +}) + +function getState(difficultyLevel: DifficultyLevel) { + return mockState({initialCardDeck:{pile:[1,2,3,4],discard:[]}, + difficultyLevel, + eraScoringTiles: [ScoringCategory.PROSPERITY,ScoringCategory.POPULATION,ScoringCategory.CULTURE,ScoringCategory.KNOWLEDGE], + finalScoringTiles: [ScoringCategory.EVOLUTION,ScoringCategory.PRESTIGE,ScoringCategory.TECHNOLOGY], + rounds:[ + mockRound({round:1, turns:[ + mockTurn({round:1,turn:1,player:Player.PLAYER}), + mockTurn({round:1,turn:2,player:Player.BOT,cardDeck:{pile:[2,3,4],discard:[1]}, + evolutionCount:2, prosperityCount:1, blueDotCount:3, redDotCount:2, + actionRoll:3, territoryRoll:4, beaconRoll:5}), + mockTurn({round:1,turn:3,player:Player.PLAYER}), + mockTurn({round:1,turn:4,player:Player.BOT,cardDeck:{pile:[3,4],discard:[2,1]}, + evolutionCount:2, prosperityCount:1, blueDotCount:5, redDotCount:4}) + ]}) + ]}) +} \ No newline at end of file diff --git a/tests/unit/util/NavigationState.spec.ts b/tests/unit/util/NavigationState.spec.ts index 2493653..23f739f 100644 --- a/tests/unit/util/NavigationState.spec.ts +++ b/tests/unit/util/NavigationState.spec.ts @@ -12,7 +12,7 @@ const stateBotData = mockState({initialCardDeck:{pile:[1,2,3,4],discard:[]}, rou mockTurn({round:1,turn:2,player:Player.BOT,cardDeck:{pile:[2,3,4],discard:[1]}}), mockTurn({round:1,turn:3,player:Player.PLAYER}), mockTurn({round:1,turn:4,player:Player.BOT,cardDeck:{pile:[3,4],discard:[2,1]}, - evolutionCount:2, prosperityCount:1, + evolutionCount:2, prosperityCount:1, blueDotCount:3, redDotCount:1, actionRoll:3, territoryRoll:4, beaconRoll:5}) ]}) ]}) @@ -68,6 +68,8 @@ describe('util/NavigationState', () => { expect(navigationState.cardDeck.toPersistence()).to.eql({pile:[3,4],discard:[2,1]}) expect(navigationState.evolutionCount).to.eq(2) expect(navigationState.prosperityCount).to.eq(1) + expect(navigationState.blueDotCount).to.eq(3) + expect(navigationState.redDotCount).to.eq(1) expect(navigationState.actionRoll).to.eq(3) expect(navigationState.territoryRoll).to.eq(4) expect(navigationState.beaconRoll).to.eq(5) @@ -81,6 +83,8 @@ describe('util/NavigationState', () => { expect(navigationState.cardDeck.toPersistence()).to.eql({pile:[4],discard:[3,2,1]}) expect(navigationState.evolutionCount).to.eq(2) expect(navigationState.prosperityCount).to.eq(1) + expect(navigationState.blueDotCount).to.eq(5) // card3.blueDotCount = 2 + expect(navigationState.redDotCount).to.eq(2) // card3.redDotCount = 1 expect(navigationState.actionRoll).to.greaterThanOrEqual(1) expect(navigationState.territoryRoll).to.greaterThanOrEqual(1) expect(navigationState.beaconRoll).to.greaterThanOrEqual(1) @@ -94,6 +98,8 @@ describe('util/NavigationState', () => { expect(navigationState.cardDeck.toPersistence()).to.eql({pile:[4],discard:[3,2,1]}) expect(navigationState.evolutionCount).to.eq(2) expect(navigationState.prosperityCount).to.eq(1) + expect(navigationState.blueDotCount).to.eq(2) // card3.blueDotCount = 2 + expect(navigationState.redDotCount).to.eq(1) // card3.redDotCount = 1 expect(navigationState.actionRoll).to.greaterThanOrEqual(1) expect(navigationState.territoryRoll).to.greaterThanOrEqual(1) expect(navigationState.beaconRoll).to.greaterThanOrEqual(1) @@ -107,6 +113,8 @@ describe('util/NavigationState', () => { expect(navigationState.cardDeck.toPersistence()).to.eql({pile:[1,2,3,4],discard:[]}) expect(navigationState.evolutionCount).to.eq(0) expect(navigationState.prosperityCount).to.eq(0) + expect(navigationState.blueDotCount).to.eq(0) + expect(navigationState.redDotCount).to.eq(0) expect(navigationState.actionRoll).to.eq(0) expect(navigationState.territoryRoll).to.eq(0) expect(navigationState.beaconRoll).to.eq(0)