From 14a37436160c0abe331f4a30a1d6ecb046d105b3 Mon Sep 17 00:00:00 2001 From: pd0a6847 Date: Mon, 17 Nov 2025 13:55:52 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=8C=EC=9E=84=EA=B8=B0=EB=8A=A5=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20-=EC=9E=90=EB=B3=B8=EA=B8=88,=EB=8B=89=EB=84=A4?= =?UTF-8?q?=EC=9E=84,=EC=82=AC=EC=9A=A9=EC=9E=90=EB=B3=84=EB=B2=A0?= =?UTF-8?q?=ED=8C=85,=EB=8C=80=EC=86=8C=ED=99=80=EC=A7=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/counter-do.ts | 317 ++++++++++++++++------ src/routes/+page.svelte | 579 +++++++++++++++++++++++++++++----------- 2 files changed, 664 insertions(+), 232 deletions(-) diff --git a/src/lib/counter-do.ts b/src/lib/counter-do.ts index b2bf0ab..062f7c4 100644 --- a/src/lib/counter-do.ts +++ b/src/lib/counter-do.ts @@ -7,40 +7,59 @@ export interface Env { interface Session { id: string; webSocket: WebSocket; - quit?: boolean; + nickname?: string; + capital?: number; + // 배팅 정보 + oddBet: number; + evenBet: number; + bigBet: number; + smallBet: number; + // 결과 + oddResult?: 'win' | 'lose' | null; + evenResult?: 'win' | 'lose' | null; + bigResult?: 'win' | 'lose' | null; + smallResult?: 'win' | 'lose' | null; + lastWinAmount?: number; } export class CounterDurableObject { private ctx: DurableObjectState; private env: Env; private sessions: Map; - private count: number; - private lastUpdate: number; - private diceNumber: number; - private isPlaying: boolean; - private diceInterval: ReturnType | null; + + // 주사위 게임 상태 + private noMoreBet: boolean; + private dice1: number | null; + private dice2: number | null; + private dice3: number | null; + private noMoreBetStartTime: number | null; + private noMoreBetEndTime: number | null; + private gameTimer: ReturnType | null; constructor(ctx: DurableObjectState, env: Env) { this.ctx = ctx; this.env = env; this.sessions = new Map(); - this.count = 0; - this.lastUpdate = Date.now(); - this.diceNumber = 1; - this.isPlaying = false; - this.diceInterval = null; - // Durable Objects에서 영구 저장소로부터 카운트를 복원 + // 주사위 게임 초기화 + this.noMoreBet = false; + this.dice1 = null; + this.dice2 = null; + this.dice3 = null; + this.noMoreBetStartTime = null; + this.noMoreBetEndTime = null; + this.gameTimer = null; + + // Durable Objects에서 영구 저장소로부터 상태 복원 this.ctx.blockConcurrencyWhile(async () => { - const stored = await this.ctx.storage.get('count'); - if (stored !== undefined) { - this.count = stored; - } - const storedDice = await this.ctx.storage.get('diceNumber'); - if (storedDice !== undefined) { - this.diceNumber = storedDice; + const storedNoMoreBet = await this.ctx.storage.get('noMoreBet'); + if (storedNoMoreBet !== undefined) { + this.noMoreBet = storedNoMoreBet; } }); + + // 게임 루프 시작 + this.startGameLoop(); } async fetch(request: Request): Promise { @@ -57,7 +76,18 @@ export class CounterDurableObject { // 세션 생성 const session: Session = { id: crypto.randomUUID(), - webSocket: server + webSocket: server, + nickname: undefined, + capital: undefined, + oddBet: 0, + evenBet: 0, + bigBet: 0, + smallBet: 0, + oddResult: null, + evenResult: null, + bigResult: null, + smallResult: null, + lastWinAmount: 0 }; // WebSocket Hibernation API 사용 @@ -78,66 +108,170 @@ export class CounterDurableObject { async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) { try { const data = typeof message === 'string' ? JSON.parse(message) : null; + const session = this.sessions.get(ws); - if (data && data.type === 'increment') { - // 카운트 증가 - this.count++; - this.lastUpdate = Date.now(); + if (!session) return; - // 영구 저장소에 저장 - await this.ctx.storage.put('count', this.count); - - // 모든 클라이언트에 브로드캐스트 + if (data && data.type === 'setUser') { + // 사용자 정보 설정 + session.nickname = data.nickname; + session.capital = data.capital; this.broadcast(); - } else if (data && data.type === 'reset') { - // 카운트 리셋 - this.count = 0; - this.lastUpdate = Date.now(); - await this.ctx.storage.put('count', this.count); - this.broadcast(); - } else if (data && data.type === 'play') { - // 주사위 플레이 시작 - this.startDiceRolling(); - } else if (data && data.type === 'stop') { - // 주사위 플레이 정지 - this.stopDiceRolling(); + } else if (data && data.type === 'bet' && !this.noMoreBet) { + // 배팅 처리 + const amount = data.amount || 1000; + + if (session.capital && session.capital >= amount) { + switch (data.betType) { + case 'odd': + session.oddBet += amount; + break; + case 'even': + session.evenBet += amount; + break; + case 'big': + session.bigBet += amount; + break; + case 'small': + session.smallBet += amount; + break; + } + // 클라이언트 측에서 자본금 차감은 이미 되어있으므로 서버에서도 동기화 + session.capital -= amount; + this.broadcast(); + } } } catch (error) { console.error('Error handling message:', error); } } - private startDiceRolling() { - if (this.isPlaying) return; // 이미 실행 중이면 무시 - - this.isPlaying = true; - this.broadcast(); // 플레이 상태 전송 - - // 1초마다 주사위 굴리기 - this.diceInterval = setInterval(async () => { - // 1-6 사이 랜덤 숫자 - this.diceNumber = Math.floor(Math.random() * 6) + 1; - this.lastUpdate = Date.now(); - - // Durable Object Storage에 저장 - await this.ctx.storage.put('diceNumber', this.diceNumber); - - // 모든 클라이언트에 브로드캐스트 - this.broadcast(); - }, 1000); + // 게임 루프 시작 + private startGameLoop() { + // 처음 시작 시 베팅 기간으로 시작 (45초) + this.startBettingPeriod(); } - private stopDiceRolling() { - if (!this.isPlaying) return; + // 1번 로직: noMoreBet = true, 15초간 유지 + private startNoMoreBetPeriod() { + this.noMoreBet = true; + this.noMoreBetStartTime = Date.now(); + this.noMoreBetEndTime = this.noMoreBetStartTime + 15000; // 15초 - this.isPlaying = false; + // 3개의 주사위 랜덤 생성 (1-6) + this.dice1 = Math.floor(Math.random() * 6) + 1; + this.dice2 = Math.floor(Math.random() * 6) + 1; + this.dice3 = Math.floor(Math.random() * 6) + 1; - if (this.diceInterval) { - clearInterval(this.diceInterval); - this.diceInterval = null; + // 주사위 합계 + const sum = this.dice1 + this.dice2 + this.dice3; + const isOdd = sum % 2 === 1; + const isBig = sum >= 10; + + // 각 세션별로 배팅 결과 계산 + this.sessions.forEach((session) => { + if (!session.capital) return; + + let winAmount = 0; + + // 홀수 배팅 결과 + if (session.oddBet > 0) { + if (isOdd) { + session.oddResult = 'win'; + winAmount += session.oddBet * 2; + } else { + session.oddResult = 'lose'; + } + } + + // 짝수 배팅 결과 + if (session.evenBet > 0) { + if (!isOdd) { + session.evenResult = 'win'; + winAmount += session.evenBet * 2; + } else { + session.evenResult = 'lose'; + } + } + + // 대 배팅 결과 + if (session.bigBet > 0) { + if (isBig) { + session.bigResult = 'win'; + winAmount += session.bigBet * 2; + } else { + session.bigResult = 'lose'; + } + } + + // 소 배팅 결과 + if (session.smallBet > 0) { + if (!isBig) { + session.smallResult = 'win'; + winAmount += session.smallBet * 2; + } else { + session.smallResult = 'lose'; + } + } + + // 자본금 업데이트 + const totalBet = session.oddBet + session.evenBet + session.bigBet + session.smallBet; + session.lastWinAmount = winAmount - totalBet; + session.capital += winAmount; + }); + + // 상태 저장 + this.ctx.storage.put('noMoreBet', this.noMoreBet); + + // 브로드캐스트 (15초간 한 번만) + this.broadcast(); + + // 15초 후 베팅 기간으로 전환 + if (this.gameTimer) { + clearTimeout(this.gameTimer); } + this.gameTimer = setTimeout(() => { + this.startBettingPeriod(); + }, 15000); + } - this.broadcast(); // 정지 상태 전송 + // 2번 로직: noMoreBet = false, 45초간 유지 + private startBettingPeriod() { + this.noMoreBet = false; + this.noMoreBetStartTime = Date.now(); + this.noMoreBetEndTime = this.noMoreBetStartTime + 45000; // 45초 + + // 주사위 값 클리어 + this.dice1 = null; + this.dice2 = null; + this.dice3 = null; + + // 모든 세션의 배팅 및 결과 클리어 + this.sessions.forEach((session) => { + session.oddBet = 0; + session.evenBet = 0; + session.bigBet = 0; + session.smallBet = 0; + session.oddResult = null; + session.evenResult = null; + session.bigResult = null; + session.smallResult = null; + session.lastWinAmount = 0; + }); + + // 상태 저장 + this.ctx.storage.put('noMoreBet', this.noMoreBet); + + // 브로드캐스트 + this.broadcast(); + + // 45초 후 noMoreBet 기간으로 전환 + if (this.gameTimer) { + clearTimeout(this.gameTimer); + } + this.gameTimer = setTimeout(() => { + this.startNoMoreBetPeriod(); + }, 45000); } async webSocketClose(ws: WebSocket, code: number, _reason: string, _wasClean: boolean) { @@ -145,11 +279,6 @@ export class CounterDurableObject { this.sessions.delete(ws); ws.close(code, 'Durable Object is closing WebSocket'); - // 남은 클라이언트가 없으면 주사위 정지 - const connectedWebSockets = this.ctx.getWebSockets(); - if (connectedWebSockets.length === 0) { - this.stopDiceRolling(); - } // 남은 클라이언트들에게 업데이트 전송 this.broadcast(); @@ -159,18 +288,52 @@ export class CounterDurableObject { // WebSocket Hibernation API를 사용할 때는 getWebSockets()로 실제 연결 수를 확인 const connectedWebSockets = this.ctx.getWebSockets(); - const message = JSON.stringify({ - count: this.count, - online: connectedWebSockets.length, - lastUpdate: this.lastUpdate, - diceNumber: this.diceNumber, - isPlaying: this.isPlaying + // 전체 사용자 배팅 내역 수집 + const allBettings: Array<{ + nickname: string; + oddBet: number; + evenBet: number; + bigBet: number; + smallBet: number; + }> = []; + + this.sessions.forEach((session) => { + if (session.nickname && (session.oddBet > 0 || session.evenBet > 0 || session.bigBet > 0 || session.smallBet > 0)) { + allBettings.push({ + nickname: session.nickname, + oddBet: session.oddBet, + evenBet: session.evenBet, + bigBet: session.bigBet, + smallBet: session.smallBet + }); + } }); - // 모든 연결된 WebSocket에 메시지 전송 + // 각 세션별로 메시지 전송 // @ts-ignore - Cloudflare Workers types 불일치 connectedWebSockets.forEach((ws: WebSocket) => { try { + const session = this.sessions.get(ws); + + const message = JSON.stringify({ + noMoreBet: this.noMoreBet, + dice1: this.dice1, + dice2: this.dice2, + dice3: this.dice3, + noMoreBetStartTime: this.noMoreBetStartTime, + noMoreBetEndTime: this.noMoreBetEndTime, + online: connectedWebSockets.length, + // 세션별 배팅 정보 + capital: session?.capital, + oddResult: session?.oddResult, + evenResult: session?.evenResult, + bigResult: session?.bigResult, + smallResult: session?.smallResult, + lastWinAmount: session?.lastWinAmount, + // 전체 사용자 배팅 내역 + allBettings: allBettings + }); + ws.send(message); } catch (error) { console.error('Error broadcasting to client:', error); diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 6f771c2..27b5dcb 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,15 +1,52 @@ -
-
+
+

- Cloudflare Durable Objects + WebSocket + Durable Objects TEST

- -
-
-
-
- + +
+ +
+
+
+
+ {isConnected ? '연결됨' : isConnecting ? '연결 중...' : '연결 안됨'} +
+ {#if !isConnected} + + + + {:else} + + {/if}
- {#if !isConnected} - - {:else} - - {/if} -
-
- - -
-
-
- {count} -
-
전체 카운트
- -
- - -
-
- - -
-
-
+ +
+
{online}
실시간 접속자
-
-
- {lastUpdate ? lastUpdate.toLocaleTimeString('ko-KR') : '-'} -
-
마지막 업데이트
-
-
- -
-

- 🎲 Durable Object 성능 테스트 -

- - -
-
-
-
- {#each getDiceDots(diceNumber) as row} - {#each row as dot} -
- {#if dot} -
- {/if} -
- {/each} - {/each} + +
+ {#if isConnected && nickname} +
{nickname}
+
{capital.toLocaleString()}원
+
현재 자본금
+ {#if lastWinAmount !== 0} +
+ {lastWinAmount > 0 ? '+' : ''}{lastWinAmount.toLocaleString()}원
-
- -
- {diceNumber} -
-
-
- - -
- {#if !isPlaying} - + {/if} {:else} - +
-
+
자본금
{/if}
+
- -
-
- {isPlaying ? '🔄 주사위가 1초마다 자동으로 굴러가는 중...' : '⏸️ 정지됨'} + +
+
+
+ + {noMoreBet ? '🚫 베팅 마감' : '✅ 베팅 가능'} + + + {remainingTime}초 + +
+
+
- - -
-

💡 성능 테스트:

-

• 플레이 버튼 클릭 시 1초마다 주사위(1-6)가 자동으로 변경됩니다

-

• 모든 값은 Durable Object에 실시간 저장됩니다

-

• 모든 연결된 클라이언트에 즉시 동기화됩니다

+
+ {noMoreBet ? '주사위를 굴리는 중... 다음 라운드를 기다려주세요' : '홀/짝, 대/소를 선택하세요!'}
+ + + {#if dice1 !== null && dice2 !== null && dice3 !== null} +
+

+ 🎲 결과 +

+ + +
+ {#each [dice1, dice2, dice3] as dice} +
+
+
+ {#each getDiceDots(dice) as row} + {#each row as dot} +
+ {#if dot} +
+ {/if} +
+ {/each} + {/each} +
+
+
+ {dice} +
+
+ {/each} +
+ + +
+
+ 합계: {dice1 + dice2 + dice3} +
+
+ + {(dice1 + dice2 + dice3) % 2 === 1 ? '✓ 홀수' : '홀수'} + + + {(dice1 + dice2 + dice3) % 2 === 0 ? '✓ 짝수' : '짝수'} + +
+
+ + {(dice1 + dice2 + dice3) >= 10 ? '✓ 대' : '대'} + + + {(dice1 + dice2 + dice3) <= 9 ? '✓ 소' : '소'} + +
+
+ + + {#if oddBet > 0 || evenBet > 0 || bigBet > 0 || smallBet > 0} +
+ {#if oddBet > 0} +
+
홀수 배팅
+
{oddBet.toLocaleString()}원
+ {#if oddResult === 'win'} +
✓ 승리 +{oddBet.toLocaleString()}원
+ {:else if oddResult === 'lose'} +
✗ 패배
+ {/if} +
+ {/if} + {#if evenBet > 0} +
+
짝수 배팅
+
{evenBet.toLocaleString()}원
+ {#if evenResult === 'win'} +
✓ 승리 +{evenBet.toLocaleString()}원
+ {:else if evenResult === 'lose'} +
✗ 패배
+ {/if} +
+ {/if} + {#if bigBet > 0} +
+
대 배팅
+
{bigBet.toLocaleString()}원
+ {#if bigResult === 'win'} +
✓ 승리 +{bigBet.toLocaleString()}원
+ {:else if bigResult === 'lose'} +
✗ 패배
+ {/if} +
+ {/if} + {#if smallBet > 0} +
+
소 배팅
+
{smallBet.toLocaleString()}원
+ {#if smallResult === 'win'} +
✓ 승리 +{smallBet.toLocaleString()}원
+ {:else if smallResult === 'lose'} +
✗ 패배
+ {/if} +
+ {/if} +
+ {/if} +
+ {/if} + + +
+

+ 배팅 선택 (1000원씩 배팅) +

+ + +
+ + + + +
+ + {#if capital < 1000 && isConnected} +
+ 자본금이 부족합니다! +
+ {/if} +
+ + + {#if isConnected && allBettings.length > 0} +
+

+ 📊 전체 배팅 현황 +

+
+ {#each allBettings as betting} +
+
{betting.nickname}
+
+ {#if betting.oddBet > 0} +
+
+
{betting.oddBet.toLocaleString()}
+
+ {/if} + {#if betting.evenBet > 0} +
+
+
{betting.evenBet.toLocaleString()}
+
+ {/if} + {#if betting.bigBet > 0} +
+
+
{betting.bigBet.toLocaleString()}
+
+ {/if} + {#if betting.smallBet > 0} +
+
+
{betting.smallBet.toLocaleString()}
+
+ {/if} +
+
+ {/each} +
+
+ {/if} +
-

기능 설명

+

게임 규칙

  • ✅ Cloudflare Durable Objects로 상태 관리
  • ✅ WebSocket으로 실시간 양방향 통신
  • -
  • ✅ 카운트 버튼 클릭으로 증가
  • -
  • ✅ 실시간 접속자 수 표시
  • -
  • ✅ 모든 클라이언트에 실시간 동기화
  • -
  • ✅ Durable Objects 영구 저장소에 상태 저장
  • -
  • 🎲 초단위 주사위 자동 롤링으로 성능 테스트
  • +
  • 🎲 베팅 기간 (45초): 홀수 또는 짝수 선택 가능
  • +
  • 🚫 베팅 마감 (15초): 주사위 결과 확인 및 대기
  • +
  • 📊 Progress bar로 남은 시간 시각화
  • +
  • 🔄 게임은 자동으로 반복됩니다
  • +
  • 👥 모든 접속자에게 실시간 동기화