const assert = require('node:assert/strict'); const test = require('node:test'); const { createTargetActivator, detectFeedback, createReactionPicker } = require('../../data/target_interaction.js'); test('a single tap selects a target without firing', () => { const selected = []; let shots = 0; const activator = createTargetActivator({ canFire: () => true, select: target => selected.push(target), fire: async () => { shots += 1; } }); assert.equal(activator.tap({ x: 1, y: 6 }).action, 'select'); assert.deepEqual(selected, [{ x: 1, y: 6 }]); assert.equal(shots, 0); }); test('a second tap on the same cell fires once while a different cell selects', async () => { let timeMs = 0; const selected = []; let shots = 0; const activator = createTargetActivator({ canFire: () => true, select: target => selected.push(target), fire: async () => { shots += 1; }, now: () => timeMs }); activator.tap({ x: 1, y: 6 }); timeMs = 200; const sameCell = activator.tap({ x: 1, y: 6 }); await sameCell.promise; timeMs = 300; assert.equal(activator.tap({ x: 2, y: 6 }).action, 'select'); assert.equal(shots, 1); assert.deepEqual(selected, [{ x: 1, y: 6 }, { x: 2, y: 6 }]); }); test('double activation and explicit fire are deduplicated while a shot is pending', async () => { let resolveShot; let shots = 0; const activator = createTargetActivator({ canFire: () => true, select: () => {}, fire: () => new Promise(resolve => { shots += 1; resolveShot = resolve; }) }); const first = activator.doubleActivate({ x: 1, y: 6 }); const duplicate = activator.doubleActivate({ x: 1, y: 6 }); assert.equal(await duplicate.promise, false); assert.equal(shots, 1); resolveShot(); await first.promise; }); test('out-of-turn or already-targeted cells cannot select or fire', () => { let selected = 0; let shots = 0; const activator = createTargetActivator({ canFire: () => false, select: () => { selected += 1; }, fire: async () => { shots += 1; } }); assert.equal(activator.tap({ x: 1, y: 6 }).action, 'ignored'); assert.equal(activator.keyboard({ x: 1, y: 6 }).action, 'ignored'); assert.equal(selected, 0); assert.equal(shots, 0); }); function gameState(version, boards, overrides = {}) { return { version, gameId: 7, phase: 'in_progress', turn: 'player1', winner: null, boards, ...overrides }; } test('feedback distinguishes a hit on the opponent from damage to the player', () => { const water = '0'.repeat(100); const opponentHit = `${water.slice(0, 12)}3${water.slice(13)}`; assert.equal(detectFeedback(gameState(1, [water, water]), gameState(2, [water, opponentHit]), 'player1').type, 'hit'); assert.equal(detectFeedback(gameState(1, [water, water]), gameState(2, [opponentHit, water]), 'player1').type, 'damage'); }); test('sinking and victory feedback take priority over ordinary cell changes', () => { const water = '0'.repeat(100); const sunk = `44${water.slice(2)}`; assert.equal(detectFeedback(gameState(1, [water, water]), gameState(2, [water, sunk]), 'player1').type, 'sunk'); const finished = gameState(3, [water, sunk], { phase: 'finished', winner: 0 }); assert.equal(detectFeedback(gameState(2, [water, water]), finished, 'player1').type, 'victory'); assert.equal(detectFeedback(gameState(2, [water, water]), finished, 'player2').type, 'defeat'); }); test('duplicate snapshots do not repeat rewards', () => { const water = '0'.repeat(100); assert.equal(detectFeedback(gameState(2, [water, water]), gameState(2, [water, water]), 'player1'), undefined); }); test('reaction picker avoids an immediate repeat for the same event', () => { const picker = createReactionPicker({ hit: ['one', 'two', 'three'] }, () => 0); assert.equal(picker('hit'), 'one'); assert.equal(picker('hit'), 'two'); assert.equal(picker('hit'), 'one'); }); test('reaction picker handles single and missing catalogs', () => { const picker = createReactionPicker({ miss: ['splash'] }, () => 0.9); assert.equal(picker('miss'), 'splash'); assert.equal(picker('miss'), 'splash'); assert.equal(picker('unknown'), undefined); });