import type { DurableObjectNamespace, DurableObjectState } from '@cloudflare/workers-types'; import type { Session, BettingInfo } from './types'; import { applyBetResults } from './game-results'; export interface Env { COUNTER: DurableObjectNamespace; } export class CounterDurableObject { private ctx: DurableObjectState; private env: Env; private sessions: Map; // 주사위 게임 상태 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.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 storedNoMoreBet = await this.ctx.storage.get('noMoreBet'); if (storedNoMoreBet !== undefined) { this.noMoreBet = storedNoMoreBet; } }); // 게임 루프 시작 this.startGameLoop(); } async fetch(request: Request): Promise { // WebSocket 업그레이드 요청 처리 const upgradeHeader = request.headers.get('Upgrade'); if (!upgradeHeader || upgradeHeader !== 'websocket') { return new Response('Expected Upgrade: websocket', { status: 426 }); } // @ts-ignore - WebSocketPair는 Cloudflare Workers 런타임에서만 사용 가능 const webSocketPair = new WebSocketPair(); const [client, server] = Object.values(webSocketPair) as [WebSocket, WebSocket]; // 세션 생성 const session: Session = { id: crypto.randomUUID(), 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 사용 // @ts-ignore - Cloudflare Workers types 불일치 this.ctx.acceptWebSocket(server); this.sessions.set(server, session); // 현재 상태 전송 this.broadcast(); return new Response(null, { status: 101, // @ts-ignore - webSocket 속성은 Cloudflare Workers에서 지원됨 webSocket: client }); } async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) { try { const data = typeof message === 'string' ? JSON.parse(message) : null; const session = this.sessions.get(ws); if (!session) return; if (data && data.type === 'setUser') { // 사용자 정보 설정 session.nickname = data.nickname; session.capital = data.capital; this.broadcast(); } 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 startGameLoop() { // 처음 시작 시 베팅 기간으로 시작 (45초) this.startBettingPeriod(); } // 1번 로직: noMoreBet = true, 15초간 유지 private startNoMoreBetPeriod() { this.noMoreBet = true; this.noMoreBetStartTime = Date.now(); this.noMoreBetEndTime = this.noMoreBetStartTime + 15000; // 15초 // 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; // 주사위 합계 const sum = this.dice1 + this.dice2 + this.dice3; // 추출된 함수로 배팅 결과 계산 및 세션 갱신 applyBetResults(this.sessions, sum); // 상태 저장 this.ctx.storage.put('noMoreBet', this.noMoreBet); // 브로드캐스트 (15초간 한 번만) this.broadcast(); // 15초 후 베팅 기간으로 전환 if (this.gameTimer) { clearTimeout(this.gameTimer); } this.gameTimer = setTimeout(() => { this.startBettingPeriod(); }, 15000); } // 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) { // 세션 제거 this.sessions.delete(ws); ws.close(code, 'Durable Object is closing WebSocket'); // 남은 클라이언트들에게 업데이트 전송 this.broadcast(); } private broadcast() { // WebSocket Hibernation API를 사용할 때는 getWebSockets()로 실제 연결 수를 확인 const connectedWebSockets = this.ctx.getWebSockets(); // 전체 사용자 배팅 내역 수집 const allBettings: BettingInfo[] = []; 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 }); } }); // 각 세션별로 메시지 전송 // @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); } }); } }