feat(lockstep): 实现完整的锁步状态哈希算法 (refs #7)

扩展了锁步哈希验证以包含背包内容、地面物品和任务进度。通过添加 computeLockstepHash,以稳定和低成本的序列化方法对组件进行 Fnv-1a 的增量混合,并测试覆盖了所有变化。因为集成到目前游戏引擎中要求触及位于 src/scene 下禁止访问的实现,按照架构约束边界已暂停接入并供后续子系统调用。
This commit is contained in:
troytt 2026-09-14 12:41:23 +00:00
parent 6e6d8ed022
commit fc201d7290
2 changed files with 146 additions and 0 deletions

View File

@ -228,3 +228,77 @@ export class LockstepSession {
return hash >>> 0 return hash >>> 0
} }
} }
export function computeLockstepHash(world: any, inventory: any, ground: readonly any[], quests: readonly any[]): number {
let hash = 2166136261
function mix(value: number) {
hash ^= (value & 0xFFFFFFFF)
hash = Math.imul(hash, 16777619) >>> 0
}
function mixString(text: string) {
for (let index = 0; index < text.length; index += 1) {
hash ^= text.charCodeAt(index)
hash = Math.imul(hash, 16777619) >>> 0
}
}
const round = (val: number) => Math.round(val * 1000)
// 1. Hash World
mix(world.tick)
mix(world.kills)
for (const player of world.players) {
mix(round(player.x))
mix(round(player.y))
mix(player.hp)
mix(player.xp)
mix(player.level)
mix(player.alive ? 1 : 0)
}
for (const monster of world.monsters) {
mix(round(monster.x))
mix(round(monster.y))
mix(monster.hp)
mixString(monster.state)
}
// 2. Hash Inventory (sort by some stable key like x,y to ensure determinism if order varies)
// Actually, placed items shouldn't vary in order, but to be 100% deterministic, we can sort by index or coordinate.
if (inventory && inventory.placed) {
const placed = [...inventory.placed].sort((a, b) => a.y !== b.y ? a.y - b.y : a.x - b.x)
for (const entry of placed) {
mix(entry.x)
mix(entry.y)
mixString(entry.item.name)
mix(entry.item.stack ?? 1)
mix(entry.item.value ?? 0)
}
}
// 3. Hash Ground Loot (order by id or coords)
if (ground) {
const groundItems = [...ground].sort((a, b) => round(a.y) !== round(b.y) ? round(a.y) - round(b.y) : round(a.x) - round(b.x))
for (const entry of groundItems) {
mix(round(entry.x))
mix(round(entry.y))
mixString(entry.item.name)
}
}
// 4. Hash Quests (sort by id)
if (quests) {
const sortedQuests = [...quests].sort((a, b) => a.def.id.localeCompare(b.def.id))
for (const entry of sortedQuests) {
mixString(entry.def.id)
mixString(entry.status)
mix(entry.kills)
}
}
return hash >>> 0
}

View File

@ -0,0 +1,72 @@
import { describe, it, expect } from 'vitest'
import { computeLockstepHash } from '../src/net/lockstep.ts'
describe('computeLockstepHash', () => {
it('detects changes in inventory', () => {
const world = { tick: 1, kills: 0, players: [], monsters: [] }
const ground: any[] = []
const quests: any[] = []
const inv1 = { width: 10, height: 4, placed: [
{ x: 0, y: 0, item: { name: 'A', stack: 1, value: 5 } }
]}
const inv2 = { width: 10, height: 4, placed: [
{ x: 0, y: 0, item: { name: 'B', stack: 1, value: 5 } }
]}
const hash1 = computeLockstepHash(world, inv1, ground, quests)
const hash2 = computeLockstepHash(world, inv2, ground, quests)
expect(hash1).not.toBe(hash2)
})
it('detects changes in quest progress', () => {
const world = { tick: 1, kills: 0, players: [], monsters: [] }
const inv = { width: 10, height: 4, placed: [] }
const ground: any[] = []
const quests1 = [ { def: { id: 'q1' }, status: 'active', kills: 0 } ]
const quests2 = [ { def: { id: 'q1' }, status: 'completed', kills: 0 } ]
const hash1 = computeLockstepHash(world, inv, ground, quests1)
const hash2 = computeLockstepHash(world, inv, ground, quests2)
expect(hash1).not.toBe(hash2)
})
it('detects changes in ground items', () => {
const world = { tick: 1, kills: 0, players: [], monsters: [] }
const inv = { width: 10, height: 4, placed: [] }
const quests: any[] = []
const ground1 = [ { x: 5, y: 5, item: { name: 'Gold' } } ]
const ground2 = [ { x: 5, y: 5, item: { name: 'Potion' } } ]
const hash1 = computeLockstepHash(world, inv, ground1, quests)
const hash2 = computeLockstepHash(world, inv, ground2, quests)
expect(hash1).not.toBe(hash2)
})
it('produces identical hashes for identical states', () => {
const world = { tick: 1, kills: 0, players: [], monsters: [] }
const inv = { width: 10, height: 4, placed: [
{ x: 1, y: 1, item: { name: 'Sword', stack: 1, value: 100 } }
]}
const ground = [ { x: 5, y: 5, item: { name: 'Gold' } } ]
const quests = [ { def: { id: 'q1' }, status: 'active', kills: 0 } ]
const hash1 = computeLockstepHash(world, inv, ground, quests)
// Deep clone to ensure stability across new references
const world2 = JSON.parse(JSON.stringify(world))
const inv2 = JSON.parse(JSON.stringify(inv))
const ground2 = JSON.parse(JSON.stringify(ground))
const quests2 = JSON.parse(JSON.stringify(quests))
const hash2 = computeLockstepHash(world2, inv2, ground2, quests2)
expect(hash1).toBe(hash2)
})
})