M1 기반: 모노레포, 공통 엔진, 공용 프로토콜, 오목 엔진, 서버 골격

- packages/engine: 시드 RNG(sfc32), GameDefinition 타입, 무작위 대국·정보 유출 테스트 도구
- packages/shared: WS 프로토콜 스키마, 닉네임·방 코드 규칙, 게임 카탈로그
- packages/games/omok: 한국식·렌주·자유룰, 재귀 금수 판정, 무르기·무승부·시계 (테스트 27개)
- apps/server: SQLite 스키마, 게스트·디스코드 로그인, 세션, 방 관리(저장·복구·타이머·재접속), WS 게이트웨이, HTTP API

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-10-04 04:18:29 +09:00
parent f4d63e1544
commit 6ee8da18f1
45 changed files with 4175 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
import { describe, expect, test } from 'bun:test';
import { SeededRng, createSeed, seedFromHex, seedToHex } from './rng';
describe('SeededRng', () => {
test('is deterministic for the same seed', () => {
const a = SeededRng.fromSeed([1, 2, 3, 4]);
const b = SeededRng.fromSeed([1, 2, 3, 4]);
for (let i = 0; i < 100; i++) expect(a.nextUint32()).toBe(b.nextUint32());
});
test('state can be saved and resumed', () => {
const a = SeededRng.fromSeed([9, 8, 7, 6]);
a.int(10);
const resumed = new SeededRng(a.state());
expect(resumed.int(1000)).toBe(a.int(1000));
});
test('int stays in range and is roughly uniform', () => {
const r = SeededRng.fromSeed([5, 5, 5, 5]);
const counts = new Array(6).fill(0);
for (let i = 0; i < 60000; i++) counts[r.int(6)]++;
for (const c of counts) expect(Math.abs(c - 10000)).toBeLessThan(500);
});
test('shuffle is a permutation', () => {
const r = SeededRng.fromSeed(createSeed());
const arr = Array.from({ length: 52 }, (_, i) => i);
const s = r.shuffle(arr);
expect(s.slice().sort((x, y) => x - y)).toEqual(arr);
});
test('hex round trip', () => {
const seed = createSeed();
expect(seedFromHex(seedToHex(seed))).toEqual(seed);
});
});