Skip to content

Commit c64a0c4

Browse files
committed
5th floor delivery billboard marker
1 parent 03b553f commit c64a0c4

6 files changed

Lines changed: 208 additions & 3 deletions

File tree

src/components/round/botAction/ActionDelivery.vue

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
<template #action>
44
<div class="action">
55
<AppIcon type="action" name="delivery" class="icon"/>:
6-
<div class="windowSelection">{{action.floor}}{{action.windowSelection?.toUpperCase()}}</div>
6+
<div class="windowSelection">{{floor}}{{floor < 5 ? action.windowSelection?.toUpperCase() : ''}}</div>
77
</div>
88
</template>
99
<template #instruction>
1010
<p>
1111
<span v-html="t('rules.action.delivery.selectWindow')"></span><br/>
12-
<span class="fw-bold" v-html="t('rules.action.delivery.selectedWindow', {floor:action.floor,selection:t(`windowSelection.${action.windowSelection}`)})"></span><br/>
12+
<span v-if="floor < 5" class="fw-bold" v-html="t('rules.action.delivery.selectedWindow', {floor,selection:t(`windowSelection.${action.windowSelection}`)})"></span>
13+
<span v-else class="fw-bold" v-html="t('rules.action.delivery.selectedWindowFloorOnly', {floor})"></span><br/>
1314
<FlowerIcon v-for="flower in action.flowers ?? []" :key="flower" :flower="flower"/>
1415
</p>
1516
<p v-html="t('rules.action.delivery.gainVPandXP', {vp:action.vp})"/>
@@ -53,6 +54,11 @@ export default defineComponent({
5354
required: false
5455
}
5556
},
57+
computed: {
58+
floor(): number {
59+
return this.action.floor ?? 1
60+
}
61+
},
5662
mounted() {
5763
this.$emit('ready')
5864
}

src/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
"title": "Delivery",
125125
"selectWindow": "Lady Pei delivers to a window with the most matching flowers (ignoring missing flowers, if required):",
126126
"selectedWindow": "Floor {floor}, Window: {selection}",
127+
"selectedWindowFloorOnly": "Floor {floor}",
127128
"gainVPandXP": "Lady Pei earns <b>{vp} VP</b> and 1 XP for each color used in the Window Box.",
128129
"placeMarker": "Place a marker from Lady Pei in the window above the box."
129130
},

src/services/BillboardMarkers.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { cloneDeep } from 'lodash'
22
import { BillboardMarker } from '@/store/state'
33
import { ref } from 'vue'
4+
import Player from './enum/Player'
5+
import WindowStates from './WindowStates'
46

57
/**
68
* Manages billboard markers per floor.
@@ -22,6 +24,29 @@ export default class BillboardMarkers {
2224
}
2325
}
2426

27+
/**
28+
* Adds a billboard marker to the floor (1-4) with the fewest total markers.
29+
* Total markers per floor = bot deliveries across all windows on that floor + billboard markers.
30+
* Ties are broken by choosing the highest floor.
31+
* @returns the chosen floor number
32+
*/
33+
public addMarkerToFloorWithLeastMarkers(windowStates: WindowStates) : number {
34+
let bestFloor = 4
35+
let bestCount = Infinity
36+
for (let floor = 1; floor <= 4; floor++) {
37+
const botDeliveries = windowStates.windowStates
38+
.filter(w => w.floor === floor)
39+
.reduce((sum, w) => sum + w.deliveries.filter(d => d === Player.BOT).length, 0)
40+
const total = botDeliveries + this.getMarkerCount(floor)
41+
if (total <= bestCount) {
42+
bestCount = total
43+
bestFloor = floor
44+
}
45+
}
46+
this.addMarker(bestFloor)
47+
return bestFloor
48+
}
49+
2550
public getMarkerCount(floor: number) : number {
2651
return this._markers.value.find(m => m.floor === floor)?.count ?? 0
2752
}

src/services/BotActions.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default class BotActions {
2626
private readonly _billboardMarkers : BillboardMarkers
2727
private readonly _season : Season
2828
private readonly _actions : BotAction[]
29+
private readonly _additionalActions : BotAction[] = []
2930

3031
public constructor(navigationState: NavigationState) {
3132
this._marketPrices = navigationState.marketPrices
@@ -39,7 +40,7 @@ export default class BotActions {
3940
}
4041

4142
public get actions(): readonly BotAction[] {
42-
return this._actions
43+
return [...this._actions, ...this._additionalActions]
4344
}
4445

4546
/**
@@ -185,6 +186,12 @@ export default class BotActions {
185186
botAction.vp = this.getVPFromMatchingFlowers(window.flowers, currentSeasonFlowers)
186187
botAction.xp = window.flowers.filter(flower => currentSeasonFlowers.includes(flower))
187188
this._windowStates.addDelivery(window.floor, window.windowSelection, Player.BOT)
189+
190+
// on 5th floor: also add billboard marker to floor with least total markers
191+
if (botAction.floor === 5) {
192+
const billboardFloor = this._billboardMarkers.addMarkerToFloorWithLeastMarkers(this._windowStates)
193+
this._additionalActions.push({ action: Action.BILLBOARD, floor: billboardFloor })
194+
}
188195
return true
189196
}
190197
return false

tests/unit/services/BillboardMarkers.spec.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import BillboardMarkers from '@/services/BillboardMarkers'
2+
import Player from '@/services/enum/Player'
3+
import WindowSelection from '@/services/enum/WindowSelection'
4+
import WindowStates from '@/services/WindowStates'
5+
import { WindowState } from '@/store/state'
26
import { expect } from 'chai'
37

48
describe('services/BillboardMarkers', () => {
@@ -69,4 +73,95 @@ describe('services/BillboardMarkers', () => {
6973
expect(markers.getMarkerCount(2)).to.eq(2)
7074
})
7175

76+
it('addMarkerToFloorWithLeastMarkers - all empty', () => {
77+
const markers = BillboardMarkers.new()
78+
const windowStates = WindowStates.fromPersistence([])
79+
80+
const floor = markers.addMarkerToFloorWithLeastMarkers(windowStates)
81+
82+
// all floors tied at 0, highest floor wins
83+
expect(floor).to.eq(4)
84+
expect(markers.getMarkerCount(4)).to.eq(1)
85+
})
86+
87+
it('addMarkerToFloorWithLeastMarkers - billboard markers only', () => {
88+
const markers = BillboardMarkers.fromPersistence([
89+
{ floor: 3, count: 2 },
90+
{ floor: 4, count: 1 }
91+
])
92+
const windowStates = WindowStates.fromPersistence([])
93+
94+
const floor = markers.addMarkerToFloorWithLeastMarkers(windowStates)
95+
96+
// floor 1=0, floor 2=0 → tied, highest floor wins → floor 2
97+
expect(floor).to.eq(2)
98+
expect(markers.getMarkerCount(2)).to.eq(1)
99+
})
100+
101+
it('addMarkerToFloorWithLeastMarkers - bot deliveries counted', () => {
102+
const markers = BillboardMarkers.new()
103+
const windows: WindowState[] = [
104+
{ floor: 2, windowSelection: WindowSelection.LEFT, flowers: [], deliveries: [Player.BOT, Player.BOT] },
105+
{ floor: 2, windowSelection: WindowSelection.RIGHT, flowers: [], deliveries: [Player.BOT] },
106+
{ floor: 3, windowSelection: WindowSelection.LEFT, flowers: [], deliveries: [Player.BOT] }
107+
]
108+
const windowStates = WindowStates.fromPersistence(windows)
109+
110+
const floor = markers.addMarkerToFloorWithLeastMarkers(windowStates)
111+
112+
// floor 1=0, floor 2=3, floor 3=1, floor 4=0 → tied 1&4 at 0, highest wins → floor 4
113+
expect(floor).to.eq(4)
114+
expect(markers.getMarkerCount(4)).to.eq(1)
115+
})
116+
117+
it('addMarkerToFloorWithLeastMarkers - only bot deliveries count, not player', () => {
118+
const markers = BillboardMarkers.new()
119+
const windows: WindowState[] = [
120+
{ floor: 4, windowSelection: WindowSelection.LEFT, flowers: [], deliveries: [Player.PLAYER, Player.PLAYER] },
121+
{ floor: 3, windowSelection: WindowSelection.LEFT, flowers: [], deliveries: [Player.BOT] }
122+
]
123+
const windowStates = WindowStates.fromPersistence(windows)
124+
125+
const floor = markers.addMarkerToFloorWithLeastMarkers(windowStates)
126+
127+
// floor 1=0, floor 2=0, floor 3=1, floor 4=0 (player deliveries not counted)
128+
// tied 1,2,4 at 0, highest wins → floor 4
129+
expect(floor).to.eq(4)
130+
})
131+
132+
it('addMarkerToFloorWithLeastMarkers - combined markers and deliveries', () => {
133+
const markers = BillboardMarkers.fromPersistence([
134+
{ floor: 1, count: 1 },
135+
{ floor: 4, count: 2 }
136+
])
137+
const windows: WindowState[] = [
138+
{ floor: 2, windowSelection: WindowSelection.LEFT, flowers: [], deliveries: [Player.BOT, Player.BOT] },
139+
{ floor: 3, windowSelection: WindowSelection.LEFT, flowers: [], deliveries: [Player.BOT] }
140+
]
141+
const windowStates = WindowStates.fromPersistence(windows)
142+
143+
const floor = markers.addMarkerToFloorWithLeastMarkers(windowStates)
144+
145+
// floor 1=1, floor 2=2, floor 3=1, floor 4=2 → tied 1&3 at 1, highest wins → floor 3
146+
expect(floor).to.eq(3)
147+
expect(markers.getMarkerCount(3)).to.eq(1)
148+
})
149+
150+
it('addMarkerToFloorWithLeastMarkers - ignores floor 5', () => {
151+
const markers = BillboardMarkers.new()
152+
const windows: WindowState[] = [
153+
{ floor: 1, windowSelection: WindowSelection.LEFT, flowers: [], deliveries: [Player.BOT] },
154+
{ floor: 2, windowSelection: WindowSelection.LEFT, flowers: [], deliveries: [Player.BOT] },
155+
{ floor: 3, windowSelection: WindowSelection.LEFT, flowers: [], deliveries: [Player.BOT] },
156+
{ floor: 4, windowSelection: WindowSelection.LEFT, flowers: [], deliveries: [Player.BOT] },
157+
{ floor: 5, windowSelection: WindowSelection.LEFT, flowers: [], deliveries: [] }
158+
]
159+
const windowStates = WindowStates.fromPersistence(windows)
160+
161+
const floor = markers.addMarkerToFloorWithLeastMarkers(windowStates)
162+
163+
// all floors 1-4 tied at 1, highest wins → floor 4 (not floor 5)
164+
expect(floor).to.eq(4)
165+
})
166+
72167
})

tests/unit/services/BotActions.spec.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -725,18 +725,89 @@ describe('services/BotActions', () => {
725725
]
726726
})
727727
const windowStates = WindowStates.new()
728+
const billboardMarkers = BillboardMarkers.new()
728729
// floor 5 already has all 5 flowers defined by default
729730
const navigationState = mockNavigationState({
730731
season: Season.AUTUMN,
731732
garden,
732733
windowStates,
734+
billboardMarkers,
733735
cardDeck
734736
})
735737

736738
const botActions = new BotActions(navigationState)
737739

738740
const action = botActions.actions[0]
739741
expect(action.vp).to.eq(16) // 5 matches → 16 VP
742+
// 5th floor delivery triggers additional billboard marker
743+
expect(botActions.actions.length).to.eq(2)
744+
expect(botActions.actions[1].action).to.eq(Action.BILLBOARD)
745+
})
746+
747+
it('5th floor delivery adds billboard marker to floor with least total markers', () => {
748+
const cardDeck = mockCardDeck({ pile: ['delivery-1'] })
749+
cardDeck.draw()
750+
const garden = mockBotGarden({
751+
seasons: [
752+
{ season: Season.AUTUMN, flowers: [Flower.ORANGE, Flower.BLUE, Flower.YELLOW, Flower.PURPLE, Flower.RED] }
753+
]
754+
})
755+
const windowStates = WindowStates.new()
756+
// add bot deliveries so floors 3 and 4 have markers
757+
windowStates.setWindowState(3, WindowSelection.LEFT, [Flower.RED, Flower.BLUE, Flower.PURPLE], [Player.BOT, Player.BOT])
758+
windowStates.setWindowState(4, WindowSelection.LEFT, [Flower.RED, Flower.BLUE, Flower.PURPLE, Flower.YELLOW], [Player.BOT])
759+
const billboardMarkers = BillboardMarkers.fromPersistence([
760+
{ floor: 1, count: 1 },
761+
{ floor: 2, count: 1 }
762+
])
763+
const navigationState = mockNavigationState({
764+
season: Season.AUTUMN,
765+
garden,
766+
windowStates,
767+
billboardMarkers,
768+
cardDeck
769+
})
770+
771+
const botActions = new BotActions(navigationState)
772+
773+
// floor totals: 1=1, 2=1, 3=2, 4=1 → tied at 1: floors 1,2,4 → highest wins → floor 4
774+
expect(botActions.actions.length).to.eq(2)
775+
expect(botActions.actions[0].action).to.eq(Action.DELIVERY)
776+
expect(botActions.actions[0].floor).to.eq(5)
777+
expect(botActions.actions[1].action).to.eq(Action.BILLBOARD)
778+
expect(botActions.actions[1].floor).to.eq(4)
779+
expect(billboardMarkers.getMarkerCount(4)).to.eq(1)
780+
})
781+
782+
it('non-5th floor delivery does not add billboard marker', () => {
783+
const cardDeck = mockCardDeck({ pile: ['delivery-1'] })
784+
cardDeck.draw()
785+
const garden = mockBotGarden({
786+
seasons: [
787+
{ season: Season.AUTUMN, flowers: [Flower.RED, Flower.BLUE] }
788+
]
789+
})
790+
const windowStates = WindowStates.new()
791+
windowStates.setWindowState(2, WindowSelection.LEFT, [Flower.RED, Flower.BLUE], [])
792+
const billboardMarkers = BillboardMarkers.new()
793+
const navigationState = mockNavigationState({
794+
season: Season.AUTUMN,
795+
garden,
796+
windowStates,
797+
billboardMarkers,
798+
cardDeck
799+
})
800+
801+
const botActions = new BotActions(navigationState)
802+
803+
expect(botActions.actions.length).to.eq(1)
804+
expect(botActions.actions[0].action).to.eq(Action.DELIVERY)
805+
expect(botActions.actions[0].floor).to.eq(2)
806+
// no additional billboard action
807+
expect(billboardMarkers.getMarkerCount(1)).to.eq(0)
808+
expect(billboardMarkers.getMarkerCount(2)).to.eq(0)
809+
expect(billboardMarkers.getMarkerCount(3)).to.eq(0)
810+
expect(billboardMarkers.getMarkerCount(4)).to.eq(0)
740811
})
741812

742813
it('sets VP to 0 when no flowers match', () => {

0 commit comments

Comments
 (0)