import { describe, expect, it } from 'vitest' import { reduce } from './reducer.js' import { openSlotIndex, validateStationActivity } from './validate.js' import { timerFor } from './config.js' import { DIDS, doActivity, openVote, setup } from './testutil.js' const [SAB, A, B, C] = DIDS describe('validateStationActivity', () => { it('accepts a legal activity when the player stands in the station area, and picks a slot', () => { const s = setup({ startRound: true }) // everyone spawns at the origin, inside res-wood's area const r = validateStationActivity(s, A, 'res-wood', 'collect', 'wood') expect(r).toEqual({ ok: true, slot: 0 }) }) it('rejects when the player is not inside the station area', () => { const s = setup({ startRound: true }) // origin is NOT inside res-stone's area (x≈10) const r = validateStationActivity(s, A, 'res-stone', 'collect') expect(r).toEqual({ ok: false, reason: 'not-in-area' }) }) it('rejects an activity the player is not allowed to choose', () => { const s = setup({ startRound: true }) expect(validateStationActivity(s, A, 'res-wood', 'sabotage')).toEqual({ ok: false, reason: 'illegal-option', }) }) it('rejects when every slot is occupied or cooling down', () => { let s = setup({ startRound: true }) // occupy all three slots of res-wood with A, B, C for (const [i, did] of [A, B, C].entries()) { s = reduce(s, { type: 'actionStarted', serverTime: 1, did, stationId: 'res-wood', slot: i, kind: 'collect', endsAt: 1 + timerFor('collect', s.config), }) } expect(openSlotIndex(s, 'res-wood')).toBe(-1) expect(validateStationActivity(s, SAB, 'res-wood', 'collect', 'wood')).toEqual({ ok: false, reason: 'no-open-slot', }) }) it('rejects outside of a round and while already busy', () => { const lobby = setup() // not started expect(validateStationActivity(lobby, A, 'res-wood', 'collect')).toEqual({ ok: false, reason: 'not-in-round', }) let s = setup({ startRound: true }) s = reduce(s, { type: 'actionStarted', serverTime: 1, did: A, stationId: 'res-wood', slot: 0, kind: 'collect', endsAt: 1 + timerFor('collect', s.config), }) expect(validateStationActivity(s, A, 'res-wood', 'collect')).toEqual({ ok: false, reason: 'already-busy', }) }) }) describe('validateStationActivity: carry / discard / deposit gating', () => { it('validates "discard and X": legal with the discard flag, illegal without, illegal empty-handed', () => { let s = setup({ startRound: true }) s = doActivity(s, A, 'res-wood', 0, 'collect', 100) // A carries real wood, still inside res-wood // discard-and-collect: legal (the drop comes first), and it picks a fresh slot (slot 0 is cooling) expect(validateStationActivity(s, A, 'res-wood', 'collect', 'wood', true)).toEqual({ ok: true, slot: 1 }) // the same collect WITHOUT the discard flag is illegal while carrying expect(validateStationActivity(s, A, 'res-wood', 'collect', 'wood', false)).toEqual({ ok: false, reason: 'illegal-option', }) // discard requested but nothing in hand → illegal const empty = setup({ startRound: true }) expect(validateStationActivity(empty, A, 'res-wood', 'collect', 'wood', true)).toEqual({ ok: false, reason: 'illegal-option', }) }) it('validates fakeDeposit at a deposit point and rejects an unaccepted resource', () => { let s = setup({ startRound: true }) s = doActivity(s, SAB, 'res-wood', 0, 'pretendCollect', 100) // fake wood s = reduce(s, { type: 'playerMoved', serverTime: 200, did: SAB, pos: { x: 0, y: 50 }, facing: 'south' }) expect(validateStationActivity(s, SAB, 'deposit', 'fakeDeposit', 'wood')).toEqual({ ok: true, slot: 0 }) expect(validateStationActivity(s, SAB, 'deposit', 'fakeDeposit', 'gold')).toEqual({ ok: false, reason: 'bad-resource', }) }) it('gates bare-handed break at a pure deposit point, but allows the fake-carry cost-1 break', () => { // empty-handed saboteur at the deposit point: break is illegal (deposit-point gating) let bare = setup({ startRound: true }) bare = reduce(bare, { type: 'playerMoved', serverTime: 50, did: SAB, pos: { x: 0, y: 50 }, facing: 'south' }) expect(validateStationActivity(bare, SAB, 'deposit', 'breakStation')).toEqual({ ok: false, reason: 'illegal-option', }) // fake-carry cover unlocks the cost-1 break let cover = setup({ startRound: true }) cover = doActivity(cover, SAB, 'res-wood', 0, 'pretendCollect', 100) cover = reduce(cover, { type: 'playerMoved', serverTime: 200, did: SAB, pos: { x: 0, y: 50 }, facing: 'south' }) expect(validateStationActivity(cover, SAB, 'deposit', 'breakStation')).toEqual({ ok: true, slot: 0 }) }) it('validates callVote at a voting station and rejects it while a vote already runs', () => { let s = setup({ startRound: true }) s = reduce(s, { type: 'playerMoved', serverTime: 50, did: A, pos: { x: 100, y: 50 }, facing: 'south' }) expect(validateStationActivity(s, A, 'meeting', 'callVote')).toEqual({ ok: true, slot: 0 }) s = openVote(s, B, 60) // B opens a vote expect(validateStationActivity(s, A, 'meeting', 'callVote')).toEqual({ ok: false, reason: 'vote-in-progress', }) }) })