Skip to content

Commit 6e43403

Browse files
committed
bot garden
1 parent c2e8813 commit 6e43403

2 files changed

Lines changed: 324 additions & 3 deletions

File tree

src/services/BotGarden.ts

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,24 @@ import { ref } from 'vue'
44
import Flower from './enum/Flower'
55
import getAllEnumValues from '@brdgm/brdgm-commons/src/util/enum/getAllEnumValues'
66
import Season from './enum/Season'
7+
import getNextSeason from '@/util/getNextSeason'
78

89
/**
910
* Manages Lady Pei's garden.
1011
*/
1112
export default class BotGarden {
1213

14+
// Base garden holds 2 flowers per season
15+
static readonly BASE_CAPACITY = 2
16+
// Big extension adds 3 flower slots; max 1 per season, 3 total
17+
static readonly BIG_EXTENSION_CAPACITY = 3
18+
static readonly MAX_BIG_EXTENSIONS_PER_SEASON = 1
19+
static readonly MAX_TOTAL_BIG_EXTENSIONS = 3
20+
// Small extension adds 2 flower slots each; max 2 per season, 3 total; requires big extension
21+
static readonly SMALL_EXTENSION_CAPACITY = 2
22+
static readonly MAX_SMALL_EXTENSIONS_PER_SEASON = 2
23+
static readonly MAX_TOTAL_SMALL_EXTENSIONS = 3
24+
1325
private readonly _seasons
1426
private readonly _flowerOrder: Flower[]
1527

@@ -23,11 +35,86 @@ export default class BotGarden {
2335
}
2436

2537
/**
26-
* Plants the given flower.
27-
* @param flower Flower to plan
38+
* Plants the given flower in Lady Pei's garden.
39+
* Chooses the first empty space beginning with the next season and continuing clockwise.
40+
* If every space is full, adds an extension (big first, then small) to the next eligible season.
41+
* @param flower Flower to plant
42+
* @param currentSeason The current season
43+
* @returns true if the flower was planted, false if the garden is full
44+
*/
45+
public plant(flower: Flower, currentSeason: Season) : boolean {
46+
// Try to find an existing empty space, starting from next season clockwise
47+
let season = getNextSeason(currentSeason)
48+
for (let i = 0; i < 4; i++) {
49+
const gs = this.getGardenSeason(season)
50+
if (gs.flowers.length < BotGarden.getSeasonCapacity(gs)) {
51+
gs.flowers.push(flower)
52+
this.sortFlowers(gs)
53+
return true
54+
}
55+
season = getNextSeason(season)
56+
}
57+
58+
// No empty space — add an extension and place the flower
59+
season = getNextSeason(currentSeason)
60+
for (let i = 0; i < 4; i++) {
61+
const gs = this.getGardenSeason(season)
62+
if (this.canAddBigExtension(gs)) {
63+
gs.bigExtension++
64+
gs.flowers.push(flower)
65+
this.sortFlowers(gs)
66+
return true
67+
}
68+
if (this.canAddSmallExtension(gs)) {
69+
gs.smallExtensions++
70+
gs.flowers.push(flower)
71+
this.sortFlowers(gs)
72+
return true
73+
}
74+
season = getNextSeason(season)
75+
}
76+
77+
return false
78+
}
79+
80+
/**
81+
* Gets the total flower capacity for a garden season (base + extensions).
2882
*/
29-
public plant(flower: Flower) : void {
83+
static getSeasonCapacity(gardenSeason: GardenSeason) : number {
84+
return BotGarden.BASE_CAPACITY
85+
+ gardenSeason.bigExtension * BotGarden.BIG_EXTENSION_CAPACITY
86+
+ gardenSeason.smallExtensions * BotGarden.SMALL_EXTENSION_CAPACITY
87+
}
88+
89+
private getGardenSeason(season: Season) : GardenSeason {
90+
const gs = this._seasons.value.find(s => s.season === season)
91+
if (!gs) {
92+
throw new Error(`Season ${season} not found in garden.`)
93+
}
94+
return gs
95+
}
96+
97+
private get totalBigExtensions() : number {
98+
return this._seasons.value.reduce((sum, s) => sum + s.bigExtension, 0)
99+
}
100+
101+
private get totalSmallExtensions() : number {
102+
return this._seasons.value.reduce((sum, s) => sum + s.smallExtensions, 0)
103+
}
104+
105+
private canAddBigExtension(gardenSeason: GardenSeason) : boolean {
106+
return gardenSeason.bigExtension < BotGarden.MAX_BIG_EXTENSIONS_PER_SEASON
107+
&& this.totalBigExtensions < BotGarden.MAX_TOTAL_BIG_EXTENSIONS
108+
}
109+
110+
private canAddSmallExtension(gardenSeason: GardenSeason) : boolean {
111+
return gardenSeason.bigExtension > 0
112+
&& gardenSeason.smallExtensions < BotGarden.MAX_SMALL_EXTENSIONS_PER_SEASON
113+
&& this.totalSmallExtensions < BotGarden.MAX_TOTAL_SMALL_EXTENSIONS
114+
}
30115

116+
private sortFlowers(gardenSeason: GardenSeason) : void {
117+
gardenSeason.flowers.sort((a, b) => this._flowerOrder.indexOf(a) - this._flowerOrder.indexOf(b))
31118
}
32119

33120
/**
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import BotGarden from '@/services/BotGarden'
2+
import Flower from '@/services/enum/Flower'
3+
import Season from '@/services/enum/Season'
4+
import { GardenSeason } from '@/store/state'
5+
import { expect } from 'chai'
6+
7+
const FLOWER_ORDER = [Flower.RED, Flower.PURPLE, Flower.YELLOW, Flower.BLUE, Flower.ORANGE]
8+
9+
function makeGarden(overrides?: Partial<Record<Season, Partial<GardenSeason>>>) : GardenSeason[] {
10+
const defaults: GardenSeason[] = [
11+
{ season: Season.SPRING, flowers: [], bigExtension: 0, smallExtensions: 0 },
12+
{ season: Season.SUMMER, flowers: [], bigExtension: 0, smallExtensions: 0 },
13+
{ season: Season.AUTUMN, flowers: [], bigExtension: 0, smallExtensions: 0 },
14+
{ season: Season.WINTER, flowers: [], bigExtension: 0, smallExtensions: 0 },
15+
]
16+
if (overrides) {
17+
for (const gs of defaults) {
18+
const o = overrides[gs.season]
19+
if (o) {
20+
Object.assign(gs, o)
21+
}
22+
}
23+
}
24+
return defaults
25+
}
26+
27+
function findSeason(garden: BotGarden, season: Season) : GardenSeason {
28+
return garden.seasons.find(s => s.season === season)!
29+
}
30+
31+
describe('services/BotGarden', () => {
32+
33+
describe('getSeasonCapacity', () => {
34+
it('base only', () => {
35+
expect(BotGarden.getSeasonCapacity({ season: Season.SPRING, flowers: [], bigExtension: 0, smallExtensions: 0 }))
36+
.to.eq(2)
37+
})
38+
39+
it('with big extension', () => {
40+
expect(BotGarden.getSeasonCapacity({ season: Season.SPRING, flowers: [], bigExtension: 1, smallExtensions: 0 }))
41+
.to.eq(5)
42+
})
43+
44+
it('with big and one small extension', () => {
45+
expect(BotGarden.getSeasonCapacity({ season: Season.SPRING, flowers: [], bigExtension: 1, smallExtensions: 1 }))
46+
.to.eq(7)
47+
})
48+
49+
it('with big and two small extensions', () => {
50+
expect(BotGarden.getSeasonCapacity({ season: Season.SPRING, flowers: [], bigExtension: 1, smallExtensions: 2 }))
51+
.to.eq(9)
52+
})
53+
})
54+
55+
describe('plant', () => {
56+
it('plants in next season when empty', () => {
57+
const garden = BotGarden.fromPersistence(makeGarden(), FLOWER_ORDER)
58+
59+
garden.plant(Flower.RED, Season.SPRING)
60+
61+
expect(findSeason(garden, Season.SUMMER).flowers).to.eql([Flower.RED])
62+
})
63+
64+
it('skips full seasons and plants in next available', () => {
65+
const garden = BotGarden.fromPersistence(makeGarden({
66+
[Season.SUMMER]: { flowers: [Flower.RED, Flower.BLUE] },
67+
}), FLOWER_ORDER)
68+
69+
garden.plant(Flower.YELLOW, Season.SPRING)
70+
71+
expect(findSeason(garden, Season.SUMMER).flowers).to.have.length(2)
72+
expect(findSeason(garden, Season.AUTUMN).flowers).to.eql([Flower.YELLOW])
73+
})
74+
75+
it('wraps around to earlier seasons', () => {
76+
const garden = BotGarden.fromPersistence(makeGarden({
77+
[Season.SUMMER]: { flowers: [Flower.RED, Flower.BLUE] },
78+
[Season.AUTUMN]: { flowers: [Flower.RED, Flower.BLUE] },
79+
[Season.WINTER]: { flowers: [Flower.RED, Flower.BLUE] },
80+
}), FLOWER_ORDER)
81+
82+
garden.plant(Flower.YELLOW, Season.SPRING)
83+
84+
expect(findSeason(garden, Season.SPRING).flowers).to.eql([Flower.YELLOW])
85+
})
86+
87+
it('plants multiple flowers sequentially', () => {
88+
const garden = BotGarden.fromPersistence(makeGarden({
89+
[Season.SPRING]: { flowers: [Flower.RED] },
90+
[Season.SUMMER]: { flowers: [Flower.BLUE] },
91+
[Season.AUTUMN]: { flowers: [Flower.PURPLE] },
92+
[Season.WINTER]: { flowers: [Flower.ORANGE] },
93+
}), FLOWER_ORDER)
94+
95+
garden.plant(Flower.YELLOW, Season.SPRING)
96+
garden.plant(Flower.RED, Season.SPRING)
97+
98+
// First goes to SUMMER (next after SPRING, has 1 flower < 2 capacity)
99+
expect(findSeason(garden, Season.SUMMER).flowers).to.eql([Flower.YELLOW, Flower.BLUE])
100+
// Second goes to AUTUMN (SUMMER now full)
101+
expect(findSeason(garden, Season.AUTUMN).flowers).to.eql([Flower.RED, Flower.PURPLE])
102+
})
103+
104+
it('adds big extension when all base spaces full', () => {
105+
const garden = BotGarden.fromPersistence(makeGarden({
106+
[Season.SPRING]: { flowers: [Flower.RED, Flower.BLUE] },
107+
[Season.SUMMER]: { flowers: [Flower.RED, Flower.BLUE] },
108+
[Season.AUTUMN]: { flowers: [Flower.RED, Flower.BLUE] },
109+
[Season.WINTER]: { flowers: [Flower.RED, Flower.BLUE] },
110+
}), FLOWER_ORDER)
111+
112+
garden.plant(Flower.YELLOW, Season.SPRING)
113+
114+
const summer = findSeason(garden, Season.SUMMER)
115+
expect(summer.bigExtension).to.eq(1)
116+
expect(summer.flowers).to.eql([Flower.RED, Flower.YELLOW, Flower.BLUE])
117+
})
118+
119+
it('adds big extension to next eligible season when first already has one', () => {
120+
const garden = BotGarden.fromPersistence(makeGarden({
121+
[Season.SPRING]: { flowers: [Flower.RED, Flower.BLUE] },
122+
[Season.SUMMER]: { flowers: Array(9).fill(Flower.RED), bigExtension: 1, smallExtensions: 2 },
123+
[Season.AUTUMN]: { flowers: [Flower.RED, Flower.BLUE] },
124+
[Season.WINTER]: { flowers: [Flower.RED, Flower.BLUE] },
125+
}), FLOWER_ORDER)
126+
127+
garden.plant(Flower.YELLOW, Season.SPRING)
128+
129+
// SUMMER is fully extended and full, so big extension goes to AUTUMN
130+
const autumn = findSeason(garden, Season.AUTUMN)
131+
expect(autumn.bigExtension).to.eq(1)
132+
expect(autumn.flowers).to.eql([Flower.RED, Flower.YELLOW, Flower.BLUE])
133+
})
134+
135+
it('respects max 3 total big extensions', () => {
136+
const garden = BotGarden.fromPersistence(makeGarden({
137+
[Season.SPRING]: { flowers: [Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED], bigExtension: 1 },
138+
[Season.SUMMER]: { flowers: [Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED], bigExtension: 1 },
139+
[Season.AUTUMN]: { flowers: [Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED], bigExtension: 1 },
140+
[Season.WINTER]: { flowers: [Flower.RED] },
141+
}), FLOWER_ORDER)
142+
143+
// WINTER has base space available — no new big extension needed
144+
garden.plant(Flower.YELLOW, Season.AUTUMN)
145+
146+
expect(findSeason(garden, Season.WINTER).flowers).to.eql([Flower.RED, Flower.YELLOW])
147+
expect(findSeason(garden, Season.WINTER).bigExtension).to.eq(0)
148+
})
149+
150+
it('uses existing big extension space before adding new extension', () => {
151+
const garden = BotGarden.fromPersistence(makeGarden({
152+
[Season.SPRING]: { flowers: [Flower.RED, Flower.BLUE] },
153+
[Season.SUMMER]: { flowers: [Flower.RED, Flower.BLUE, Flower.PURPLE], bigExtension: 1 },
154+
[Season.AUTUMN]: { flowers: [Flower.RED, Flower.BLUE] },
155+
[Season.WINTER]: { flowers: [Flower.RED, Flower.BLUE] },
156+
}), FLOWER_ORDER)
157+
158+
garden.plant(Flower.YELLOW, Season.SPRING)
159+
160+
// SUMMER has big extension with 3/5 flowers — still has room
161+
const summer = findSeason(garden, Season.SUMMER)
162+
expect(summer.flowers).to.eql([Flower.RED, Flower.PURPLE, Flower.YELLOW, Flower.BLUE])
163+
expect(summer.bigExtension).to.eq(1)
164+
})
165+
166+
it('adds small extension when big extensions exhausted', () => {
167+
const garden = BotGarden.fromPersistence(makeGarden({
168+
[Season.SPRING]: { flowers: [Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED], bigExtension: 1 },
169+
[Season.SUMMER]: { flowers: [Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED], bigExtension: 1 },
170+
[Season.AUTUMN]: { flowers: [Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED], bigExtension: 1 },
171+
[Season.WINTER]: { flowers: [Flower.RED] },
172+
}), FLOWER_ORDER)
173+
174+
// Fill WINTER base
175+
garden.plant(Flower.YELLOW, Season.AUTUMN) // goes to WINTER (base space)
176+
// Now all base + big extension spaces full, 3 big extensions used
177+
// WINTER can't get big (3 total) or small (no big). Next is SPRING which has big → gets small.
178+
garden.plant(Flower.PURPLE, Season.AUTUMN)
179+
180+
const spring = findSeason(garden, Season.SPRING)
181+
expect(spring.smallExtensions).to.eq(1)
182+
expect(spring.flowers).to.have.length(6)
183+
})
184+
185+
it('small extension requires big extension on same season', () => {
186+
const garden = BotGarden.fromPersistence(makeGarden({
187+
[Season.SPRING]: { flowers: [Flower.RED, Flower.BLUE] },
188+
[Season.SUMMER]: { flowers: [Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED], bigExtension: 1 },
189+
[Season.AUTUMN]: { flowers: [Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED], bigExtension: 1 },
190+
[Season.WINTER]: { flowers: [Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED], bigExtension: 1 },
191+
}), FLOWER_ORDER)
192+
193+
// All big ext slots full. SPRING (no big ext) can't get small ext.
194+
// From WINTER: next is SPRING (full base, no big ext → skip big [max total], skip small [no big]),
195+
// then SUMMER (full 5/5 → needs ext, can't add big [max total], can add small [has big])
196+
garden.plant(Flower.ORANGE, Season.WINTER)
197+
198+
expect(findSeason(garden, Season.SPRING).smallExtensions).to.eq(0)
199+
expect(findSeason(garden, Season.SUMMER).smallExtensions).to.eq(1)
200+
expect(findSeason(garden, Season.SUMMER).flowers).to.have.length(6)
201+
})
202+
203+
it('max 2 small extensions per season', () => {
204+
const garden = BotGarden.fromPersistence(makeGarden({
205+
[Season.SPRING]: { flowers: [Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED], bigExtension: 1, smallExtensions: 2 },
206+
[Season.SUMMER]: { flowers: [Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED], bigExtension: 1 },
207+
[Season.AUTUMN]: { flowers: [Flower.RED, Flower.BLUE, Flower.RED, Flower.BLUE, Flower.RED], bigExtension: 1 },
208+
[Season.WINTER]: { flowers: [Flower.RED, Flower.BLUE] },
209+
}), FLOWER_ORDER)
210+
211+
// SPRING is 9/9 (base 2 + big 3 + 2 small * 2 = 9), can't add more small
212+
// From WINTER: next is SPRING (full, max small), SUMMER (full 5/5, add small)
213+
garden.plant(Flower.ORANGE, Season.WINTER)
214+
215+
expect(findSeason(garden, Season.SPRING).smallExtensions).to.eq(2)
216+
expect(findSeason(garden, Season.SUMMER).smallExtensions).to.eq(1)
217+
expect(findSeason(garden, Season.SUMMER).flowers).to.have.length(6)
218+
})
219+
220+
it('returns false when no space available at all', () => {
221+
// Max capacity: 4 seasons * 9 = 36 flowers
222+
// But only 3 big ext and 3 small ext available
223+
// Max = 3 seasons * (2+3+4) + 1 season * 2 = 3*9 + 2 = 29
224+
const garden = BotGarden.fromPersistence(makeGarden({
225+
[Season.SPRING]: { flowers: Array(9).fill(Flower.RED), bigExtension: 1, smallExtensions: 2 },
226+
[Season.SUMMER]: { flowers: Array(7).fill(Flower.RED), bigExtension: 1, smallExtensions: 1 },
227+
[Season.AUTUMN]: { flowers: Array(5).fill(Flower.RED), bigExtension: 1 },
228+
[Season.WINTER]: { flowers: [Flower.RED, Flower.RED] },
229+
}), FLOWER_ORDER)
230+
231+
expect(garden.plant(Flower.YELLOW, Season.SPRING)).to.eq(false)
232+
})
233+
})
234+
})

0 commit comments

Comments
 (0)