From 31aedbfcc9a22bc4a2816ff54b09c509e0f0be4c Mon Sep 17 00:00:00 2001 From: EJClaw Date: Sun, 4 Oct 2026 05:46:30 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B3=B5=EC=A0=95=EC=84=B1=20=ED=99=95?= =?UTF-8?q?=EC=9D=B8:=20=EA=B2=B0=EA=B3=BC=20=ED=99=94=EB=A9=B4=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B8=8C=EB=9D=BC=EC=9A=B0=EC=A0=80=EA=B0=80=20SHA?= =?UTF-8?q?-256(=EC=94=A8=EC=95=97)=EC=9D=84=20=EC=A7=81=EC=A0=91=20?= =?UTF-8?q?=EA=B3=84=EC=82=B0=ED=95=B4=20=EC=8B=9C=EC=9E=91=20=EB=95=8C=20?= =?UTF-8?q?=EA=B3=B5=EA=B0=9C=ED=95=9C=20=EA=B0=92=EA=B3=BC=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- apps/web/src/pages/RoomPage.tsx | 35 +++++++++++++++++++++++++++------ e2e/omok.e2e.ts | 4 ++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/apps/web/src/pages/RoomPage.tsx b/apps/web/src/pages/RoomPage.tsx index 15e580d..cca2ebf 100644 --- a/apps/web/src/pages/RoomPage.tsx +++ b/apps/web/src/pages/RoomPage.tsx @@ -612,17 +612,40 @@ function ResultPanel({ room, me, players, onLeave }: { room: RoomView; me: Publi 방 나가기 {room.lastSeed && ( -
- 공정성 확인 -

시작할 때 공개한 값: {room.seedHash ?? '-'}

-

이번 판의 난수 씨앗: {room.lastSeed}

-

씨앗의 SHA-256이 위 값과 같으면 서버가 게임 도중 난수를 바꾸지 않았다는 뜻이에요.

-
+ )} ); } +/** + * docs/04 §5: the seed's SHA-256 was published when the game started; the seed is revealed at the end. + * The browser recomputes the hash itself so players don't have to trust the server's word. + */ +function FairnessCheck({ seedHash, seed }: { seedHash: string | null; seed: string }) { + const [verdict, setVerdict] = useState(null); + const check = async () => { + const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(seed)); + const hex = [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join(''); + setVerdict(hex === seedHash); + }; + return ( +
+ 공정성 확인 +

시작할 때 공개한 값(SHA-256): {seedHash ?? '-'}

+

끝나고 공개한 난수 씨앗: {seed}

+ + {verdict !== null && ( +

+ {verdict ? '✓ 일치해요. 게임 도중 카드 순서나 난수가 바뀌지 않았어요.' : '✗ 일치하지 않아요. 관리자에게 알려 주세요.'} +

+ )} +
+ ); +} + // ---------------------------------------------------------------- chat function ChatButton({ onOpen }: { onOpen: () => void }) { diff --git a/e2e/omok.e2e.ts b/e2e/omok.e2e.ts index ead116f..55bad1c 100644 --- a/e2e/omok.e2e.ts +++ b/e2e/omok.e2e.ts @@ -129,6 +129,10 @@ try { await a.getByText('🎉 이겼어요!').waitFor(); await b.getByText('아쉽게 졌어요').waitFor(); await a.waitForTimeout(500); // let the sheet animation finish + // Fairness check: the browser recomputes SHA-256(seed) and compares with the published hash. + await a.getByText('공정성 확인').click(); + await a.getByRole('button', { name: '내 브라우저에서 직접 확인하기' }).click(); + await a.getByText('✓ 일치해요').waitFor(); await a.screenshot({ path: join(SHOTS, '3-result-pc.png') }); await b.screenshot({ path: join(SHOTS, '4-result-phone.png') }); });