From 9fe8ffa965a72b1f7d92d3a36ef708b68d563241 Mon Sep 17 00:00:00 2001 From: michang Date: Tue, 27 Aug 2024 15:16:03 +0900 Subject: [PATCH 01/25] =?UTF-8?q?feat=20:=20=EA=B3=B5=EC=9D=98=20=EC=9E=94?= =?UTF-8?q?=EC=83=81=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EA=B3=B5=20?= =?UTF-8?q?=EB=AC=BC=EB=A6=AC=20=EA=B0=92=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/game/srcs/Ball.py | 4 ++-- frontend/src/components/Game-Core.js | 30 +++++++++++++++++++++++----- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/backend/game/srcs/Ball.py b/backend/game/srcs/Ball.py index 25ac373..cac8959 100644 --- a/backend/game/srcs/Ball.py +++ b/backend/game/srcs/Ball.py @@ -8,8 +8,8 @@ class Ball: - SPEED = 2 - RADIUS = 10 + SPEED = 1.75 + RADIUS = 14 def __init__(self, id, screen_height, screen_width, bar): self.id = id diff --git a/frontend/src/components/Game-Core.js b/frontend/src/components/Game-Core.js index 5e0ede3..603ae2f 100644 --- a/frontend/src/components/Game-Core.js +++ b/frontend/src/components/Game-Core.js @@ -13,7 +13,7 @@ export class GameCore extends Component { 'wss://' + "localhost:443" + '/ws/game/' - + this.props.uid + + this.props.uid + '/' ); return {}; @@ -31,11 +31,11 @@ export class GameCore extends Component { 'collision': new Audio('../../img/key.mp3'), }; let SCREEN_HEIGHT, SCREEN_WIDTH, BAR_HEIGHT, BAR_WIDTH, BAR_X_GAP, - BALL_RADIUS, LEFT_BAR_X, LEFT_BAR_Y, RIGHT_BAR_X, RIGHT_BAR_Y, - CELL_WIDTH, CELL_HEIGHT; + BALL_RADIUS, LEFT_BAR_X, LEFT_BAR_Y, RIGHT_BAR_X, RIGHT_BAR_Y, + CELL_WIDTH, CELL_HEIGHT; let counter = 0; let leftBar, rightBar, leftBall, rightBall, map; - + console.log(canvas) function playSound(soundName) { var sound = sounds[soundName]; @@ -92,17 +92,37 @@ export class GameCore extends Component { this.targetY = y; this.radius = radius; this.color = color; + this.trail = []; // 잔상을 저장할 배열 } draw() { + this.trail.forEach((pos, index) => { + const alpha = (index + 1) / (this.trail.length + 10); + const scale = (index / (this.trail.length + 1)); + const radius = this.radius * scale; + + ctx.globalAlpha = alpha; // 투명도 설정 + ctx.fillStyle = this.color; + ctx.fillRect(pos.x - radius, pos.y - radius, radius * 2, radius * 2); + }); + + ctx.globalAlpha = 1; Map.strokeRoundedRect(ctx, this.x - this.radius, this.y - this.radius, this.radius * 2, this.radius * 2, 6); ctx.fillStyle = this.color; ctx.fill(); } - update() { this.x = this.targetX; this.y = this.targetY; + // 10 프레임마다 잔상 저장 + if (counter % 4 === 0) { + this.trail.push({ x: this.x, y: this.y }); + + if (this.trail.length > 10) { + this.trail.shift(); // 오래된 잔상을 제거 + } + } + } } From 33d1393c8b3d106919f8d352bf4d22e68e392a20 Mon Sep 17 00:00:00 2001 From: michang Date: Tue, 27 Aug 2024 17:11:03 +0900 Subject: [PATCH 02/25] =?UTF-8?q?feat=20:=20=EC=A0=90=EC=88=98=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20=EB=B0=8F=20=EC=84=B1=ED=98=84=EC=9D=B4=ED=98=95?= =?UTF-8?q?=EA=B3=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/game/consumers.py | 16 +++++++----- backend/game/srcs/Ball.py | 17 ++++++++----- backend/game/srcs/Bar.py | 4 +-- frontend/src/components/Game-Core.js | 38 +++++++++++++++++++--------- 4 files changed, 48 insertions(+), 27 deletions(-) diff --git a/backend/game/consumers.py b/backend/game/consumers.py index 1b530a2..4043c0e 100644 --- a/backend/game/consumers.py +++ b/backend/game/consumers.py @@ -30,6 +30,7 @@ def __init__(self): ) self.left_ball = Ball(0, SCREEN_HEIGHT, SCREEN_WIDTH, self.left_bar) self.right_ball = Ball(1, SCREEN_HEIGHT, SCREEN_WIDTH, self.right_bar) + self.score = [0, 0] class GameConsumer(AsyncWebsocketConsumer): game_tasks = {} @@ -50,7 +51,7 @@ async def receive(self, text_data): text_data_json = json.loads(text_data) action = text_data_json["action"] - print("text_data", text_data_json) + # print("text_data", text_data_json) if action == "authenticate": token = text_data_json.get("token") if not token or not self.authenticate(token): @@ -178,19 +179,19 @@ async def game_loop(self): state.left_ball.move(state.left_bar) state.left_ball.check_bar_collision(state.left_bar) state.left_ball.check_bar_collision(state.right_bar) - state.left_ball.check_collision(state.map) + state.left_ball.check_collision(state.map, state.score) state.right_ball.move(state.right_bar) state.right_ball.check_bar_collision(state.left_bar) state.right_ball.check_bar_collision(state.right_bar) - state.right_ball.check_collision(state.map) + state.right_ball.check_collision(state.map, state.score) - if count % 3 == 0: + if count % 5 == 0: await self.send_game_state() - await asyncio.sleep(0.005) + await asyncio.sleep(0.00390625) await state.map.init() else: - await asyncio.sleep(0.005) + await asyncio.sleep(0.00390625) count += 1 except asyncio.CancelledError: # Handle the game loop cancellation @@ -211,6 +212,7 @@ async def send_game_state(self): "right_ball_x": state.right_ball.x, "right_ball_y": state.right_ball.y, "map_diff": state.map.diff, + "score": state.score, }, ) @@ -223,6 +225,7 @@ async def update_game_state_message(self, event): left_ball_y = event["left_ball_y"] right_ball_x = event["right_ball_x"] right_ball_y = event["right_ball_y"] + score = event["score"] # Send the updated game state to the WebSocket await self.send( @@ -238,6 +241,7 @@ async def update_game_state_message(self, event): "right_ball_x": int(right_ball_x), "right_ball_y": int(right_ball_y), "map_diff": GameConsumer.game_states[self.room_group_name].map.diff, + "score": score, } ) ) diff --git a/backend/game/srcs/Ball.py b/backend/game/srcs/Ball.py index cac8959..211e82c 100644 --- a/backend/game/srcs/Ball.py +++ b/backend/game/srcs/Ball.py @@ -4,11 +4,11 @@ RADIAN60 = np.pi / 3 RADIAN20 = np.pi / 9 COLLISION_IGNORE_FRAMES = 5 # 충돌 무시 프레임 수 -PENALTY_FRAMES = 50 # 페널티 프레임 수 +PENALTY_FRAMES = 500 # 페널티 프레임 수 class Ball: - SPEED = 1.75 + SPEED = [1.75, 2.75, 3.75] RADIUS = 14 def __init__(self, id, screen_height, screen_width, bar): @@ -22,7 +22,7 @@ def __init__(self, id, screen_height, screen_width, bar): def initialize(self, bar): self.x = bar.x + bar.width / 2 + (150 if self.id == 0 else -150) self.y = bar.y + bar.height / 2 + (1 if self.id == 0 else -1) - self.dx = -self.SPEED if self.id == 0 else self.SPEED + self.dx = -self.SPEED[0] if self.id == 0 else self.SPEED[0] self.dy = 0 self.collision_timer = 0 self.life = 0 @@ -48,10 +48,10 @@ def check_bar_collision(self, bar): return if bar.id == 1 and bar.x > self.x + self.RADIUS: return - speed = self.SPEED + speed = self.SPEED[0] if (bar.state == BarState.RELEASE): - speed *= bar.power / bar.max_power + 1 self.life = bar.get_ball_power() + speed = self.SPEED[self.life] self.x = bar.min_x + (bar.width if bar.id == 0 else 0) print("speed", speed) # 바와의 충돌로 dx를 반전시키고, dy를 새로 계산 @@ -71,7 +71,7 @@ def check_bar_collision(self, bar): self.dx = abs(self.dx) self.collision_timer = COLLISION_IGNORE_FRAMES - def check_collision(self, game_map): + def check_collision(self, game_map, score): if self.penalty_timer > 0: return # 미래 위치 예측 @@ -85,10 +85,14 @@ def check_collision(self, game_map): elif future_x - self.RADIUS <= -self.RADIUS * 10: self.x = -self.RADIUS * 5 self.penalty_timer = PENALTY_FRAMES + if self.id == 1: + score[1] += 20 return elif future_x + self.RADIUS >= self.screen_width + self.RADIUS * 10: self.x = self.screen_width + self.RADIUS * 5 self.penalty_timer = PENALTY_FRAMES + if self.id == 0: + score[0] += 20 return # 맵과의 충돌 검사 @@ -143,6 +147,7 @@ def check_collision(self, game_map): # 블록 상태 업데이트 new_value = 1 if current_value == 0 else 0 game_map.update(grid_x, grid_y, new_value) + score[self.id] += 1 # 충돌 타이머 설정 self.collision_timer = COLLISION_IGNORE_FRAMES diff --git a/backend/game/srcs/Bar.py b/backend/game/srcs/Bar.py index 926f9fa..b9efe8c 100644 --- a/backend/game/srcs/Bar.py +++ b/backend/game/srcs/Bar.py @@ -37,7 +37,6 @@ def pull(self): if self.power < self.max_power: self.power = abs(self.x - self.min_x) self.x += ((-2 / (self.max_power * 2)) * self.power + 3) * self.pull_speed - print("power", self.power) def set_release(self): if self.state != BarState.PULLING: @@ -46,10 +45,9 @@ def set_release(self): def release(self): self.x += -self.pull_speed * (self.power + 0.1) * 0.25 - print(-self.pull_speed * self.power * 0.25 + 0.1) if (self.id == 0 and self.x > self.min_x + 10) or ( self.id == 1 and self.x < self.min_x - 10 - ): + ) or self.power == 0: self.x = self.min_x self.power = 0 self.state = BarState.IDLE diff --git a/frontend/src/components/Game-Core.js b/frontend/src/components/Game-Core.js index 603ae2f..d9b1a2c 100644 --- a/frontend/src/components/Game-Core.js +++ b/frontend/src/components/Game-Core.js @@ -34,7 +34,7 @@ export class GameCore extends Component { BALL_RADIUS, LEFT_BAR_X, LEFT_BAR_Y, RIGHT_BAR_X, RIGHT_BAR_Y, CELL_WIDTH, CELL_HEIGHT; let counter = 0; - let leftBar, rightBar, leftBall, rightBall, map; + let leftBar, rightBar, leftBall, rightBall, map, score; console.log(canvas) function playSound(soundName) { @@ -97,7 +97,7 @@ export class GameCore extends Component { draw() { this.trail.forEach((pos, index) => { - const alpha = (index + 1) / (this.trail.length + 10); + const alpha = (index + 1) / (this.trail.length + 15); const scale = (index / (this.trail.length + 1)); const radius = this.radius * scale; @@ -115,7 +115,7 @@ export class GameCore extends Component { this.x = this.targetX; this.y = this.targetY; // 10 프레임마다 잔상 저장 - if (counter % 4 === 0) { + if (counter % 3 === 0) { this.trail.push({ x: this.x, y: this.y }); if (this.trail.length > 10) { @@ -264,6 +264,26 @@ export class GameCore extends Component { } } + class Score { + constructor() { + scoreCtx.font = "100px Arial"; + scoreCtx.textAlign = "center"; + this.score = [0, 0]; + } + + update(score) { + this.score = score; + } + + draw() { + scoreCtx.clearRect(0, 0, scoreCanvas.width, scoreCanvas.height); + scoreCtx.fillStyle = COLOR[1]; + scoreCtx.fillText(this.score[0], 125, 100); + scoreCtx.fillStyle = COLOR[0]; + scoreCtx.fillText(this.score[1], 375, 100); + } + } + this.gameSocket.onopen = () => { const token = getCookie("jwt"); this.gameSocket.send(JSON.stringify({ 'action': 'authenticate', 'token': token })); @@ -283,6 +303,7 @@ export class GameCore extends Component { leftBall.targetY = data.left_ball_y; rightBall.targetX = data.right_ball_x; rightBall.targetY = data.right_ball_y; + score.update(data.score); map.update(data.map_diff); } }; @@ -332,6 +353,7 @@ export class GameCore extends Component { leftBall.draw(); rightBall.draw(); Particles.drawAll(); + score.draw(); } function initializeGame(data) { @@ -349,15 +371,6 @@ export class GameCore extends Component { canvas.height = SCREEN_HEIGHT; scoreCanvas.width = 500; scoreCanvas.height = 150; - var text = "2 : 3" - // var blur = 10; - // var width = scoreCtx.measureText(text).width + blur * 2; - // scoreCtx.textBaseline = "top" - // scoreCtx.shadowColor = "#000" - // scoreCtx.shadowOffsetX = width; - // scoreCtx.shadowOffsetY = 0; - // scoreCtx.shadowBlur = blur; - scoreCtx.fillText(text, -10, 0); CELL_WIDTH = canvas.width / data.map[0].length; CELL_HEIGHT = canvas.height / data.map.length; @@ -366,6 +379,7 @@ export class GameCore extends Component { rightBar = new Bar(RIGHT_BAR_X, RIGHT_BAR_Y, BAR_WIDTH, BAR_HEIGHT, SCREEN_HEIGHT, 1); leftBall = new Ball(data.left_ball_x, data.left_ball_y, BALL_RADIUS, BALL_COLOR[0]); rightBall = new Ball(data.right_ball_x, data.right_ball_y, BALL_RADIUS, BALL_COLOR[1]); + score = new Score(); console.log(SCREEN_HEIGHT, SCREEN_WIDTH, BAR_HEIGHT, BAR_WIDTH, BALL_RADIUS); setInterval(interpolate, 3); From 40c72a5e07ce26b91374b456fa04ca8829e73984 Mon Sep 17 00:00:00 2001 From: michang Date: Tue, 27 Aug 2024 18:44:22 +0900 Subject: [PATCH 03/25] =?UTF-8?q?feat=20:=20=EA=B3=B5=EC=9D=B4=20=EC=96=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=83=9C=EC=96=B4=EB=82=98=EB=8A=94=20=EC=A7=80=20?= =?UTF-8?q?=EC=95=8C=EB=A0=A4=EC=A5=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/game/consumers.py | 10 ++++++-- backend/game/srcs/Ball.py | 4 ++-- backend/game/srcs/Bar.py | 6 +++-- frontend/src/components/Game-Core.js | 32 ++++++++++++++++++------- frontend/src/components/Game-Default.js | 3 ++- 5 files changed, 40 insertions(+), 15 deletions(-) diff --git a/backend/game/consumers.py b/backend/game/consumers.py index 4043c0e..68211ae 100644 --- a/backend/game/consumers.py +++ b/backend/game/consumers.py @@ -31,6 +31,8 @@ def __init__(self): self.left_ball = Ball(0, SCREEN_HEIGHT, SCREEN_WIDTH, self.left_bar) self.right_ball = Ball(1, SCREEN_HEIGHT, SCREEN_WIDTH, self.right_bar) self.score = [0, 0] + self.player = ["player1", "player2"] + self.penalty_time = [0, 0] class GameConsumer(AsyncWebsocketConsumer): game_tasks = {} @@ -164,6 +166,7 @@ async def send_initialize_game(self): "right_ball_y": state.right_ball.y, "ball_radius": Ball.RADIUS, "map": state.map.map, + "player": state.player, } ) ) @@ -176,12 +179,12 @@ async def game_loop(self): state.left_bar.update() state.right_bar.update() - state.left_ball.move(state.left_bar) + state.penalty_time[0] = state.left_ball.move(state.left_bar) state.left_ball.check_bar_collision(state.left_bar) state.left_ball.check_bar_collision(state.right_bar) state.left_ball.check_collision(state.map, state.score) - state.right_ball.move(state.right_bar) + state.penalty_time[1] = state.right_ball.move(state.right_bar) state.right_ball.check_bar_collision(state.left_bar) state.right_ball.check_bar_collision(state.right_bar) state.right_ball.check_collision(state.map, state.score) @@ -213,6 +216,7 @@ async def send_game_state(self): "right_ball_y": state.right_ball.y, "map_diff": state.map.diff, "score": state.score, + "penalty_time": state.penalty_time, }, ) @@ -226,6 +230,7 @@ async def update_game_state_message(self, event): right_ball_x = event["right_ball_x"] right_ball_y = event["right_ball_y"] score = event["score"] + penalty_time = event["penalty_time"] # Send the updated game state to the WebSocket await self.send( @@ -242,6 +247,7 @@ async def update_game_state_message(self, event): "right_ball_y": int(right_ball_y), "map_diff": GameConsumer.game_states[self.room_group_name].map.diff, "score": score, + "penalty_time": penalty_time, } ) ) diff --git a/backend/game/srcs/Ball.py b/backend/game/srcs/Ball.py index 211e82c..e1870eb 100644 --- a/backend/game/srcs/Ball.py +++ b/backend/game/srcs/Ball.py @@ -30,15 +30,15 @@ def initialize(self, bar): def move(self, bar): if self.penalty_timer > 0: self.penalty_timer -= 1 - # print(self.penalty_timer) if self.penalty_timer == 0: self.initialize(bar) - return + return self.penalty_timer // 10 self.x += self.dx self.y += self.dy if self.collision_timer > 0: self.collision_timer -= 1 + return self.penalty_timer // 10 def check_bar_collision(self, bar): if self.collision_timer > 0 or self.penalty_timer > 0: diff --git a/backend/game/srcs/Bar.py b/backend/game/srcs/Bar.py index b9efe8c..2118704 100644 --- a/backend/game/srcs/Bar.py +++ b/backend/game/srcs/Bar.py @@ -5,7 +5,7 @@ class Bar: X_GAP = 40 HEIGHT = 200 WIDTH = 20 - SPEED = 15 + SPEED = 20 PULL_SPEED = 1 def __init__( @@ -37,6 +37,7 @@ def pull(self): if self.power < self.max_power: self.power = abs(self.x - self.min_x) self.x += ((-2 / (self.max_power * 2)) * self.power + 3) * self.pull_speed + print("power", self.power) def set_release(self): if self.state != BarState.PULLING: @@ -45,9 +46,10 @@ def set_release(self): def release(self): self.x += -self.pull_speed * (self.power + 0.1) * 0.25 + print(-self.pull_speed * self.power * 0.25 + 0.1) if (self.id == 0 and self.x > self.min_x + 10) or ( self.id == 1 and self.x < self.min_x - 10 - ) or self.power == 0: + ): self.x = self.min_x self.power = 0 self.state = BarState.IDLE diff --git a/frontend/src/components/Game-Core.js b/frontend/src/components/Game-Core.js index d9b1a2c..ae1ca7e 100644 --- a/frontend/src/components/Game-Core.js +++ b/frontend/src/components/Game-Core.js @@ -32,9 +32,9 @@ export class GameCore extends Component { }; let SCREEN_HEIGHT, SCREEN_WIDTH, BAR_HEIGHT, BAR_WIDTH, BAR_X_GAP, BALL_RADIUS, LEFT_BAR_X, LEFT_BAR_Y, RIGHT_BAR_X, RIGHT_BAR_Y, - CELL_WIDTH, CELL_HEIGHT; + CELL_WIDTH, CELL_HEIGHT, PLAYER; let counter = 0; - let leftBar, rightBar, leftBall, rightBall, map, score; + let leftBar, rightBar, leftBall, rightBall, map, score, penaltyTime; console.log(canvas) function playSound(soundName) { @@ -68,6 +68,14 @@ export class GameCore extends Component { ctx.lineWidth = 2; Map.strokeRoundedRect(ctx, this.x, this.y, this.width, this.height, this.width / 2); ctx.stroke(); + if (penaltyTime[this.id] !== 0) + { + ctx.font = "bold 30px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = BALL_COLOR[this.id]; + ctx.fillText(penaltyTime[this.id], this.x + this.width / 2 + (this.id == 0 ? 1 : -1) * 175, this.y + this.height / 2); + } } update() { @@ -266,21 +274,27 @@ export class GameCore extends Component { class Score { constructor() { - scoreCtx.font = "100px Arial"; scoreCtx.textAlign = "center"; this.score = [0, 0]; } - + update(score) { this.score = score; } - + draw() { scoreCtx.clearRect(0, 0, scoreCanvas.width, scoreCanvas.height); + scoreCtx.font = "25px Arial"; + scoreCtx.fillStyle = COLOR[1]; + scoreCtx.fillText(PLAYER[0], 125, 50); + scoreCtx.fillStyle = COLOR[0]; + scoreCtx.fillText(PLAYER[1], 375, 50); + + scoreCtx.font = "100px Arial"; scoreCtx.fillStyle = COLOR[1]; - scoreCtx.fillText(this.score[0], 125, 100); + scoreCtx.fillText(this.score[0], 125, 140); scoreCtx.fillStyle = COLOR[0]; - scoreCtx.fillText(this.score[1], 375, 100); + scoreCtx.fillText(this.score[1], 375, 140); } } @@ -303,6 +317,7 @@ export class GameCore extends Component { leftBall.targetY = data.left_ball_y; rightBall.targetX = data.right_ball_x; rightBall.targetY = data.right_ball_y; + penaltyTime = data.penalty_time; score.update(data.score); map.update(data.map_diff); } @@ -367,10 +382,11 @@ export class GameCore extends Component { BAR_WIDTH = data.bar_width; BAR_X_GAP = data.bar_x_gap; BALL_RADIUS = data.ball_radius; + PLAYER = data.player; canvas.width = SCREEN_WIDTH; canvas.height = SCREEN_HEIGHT; scoreCanvas.width = 500; - scoreCanvas.height = 150; + scoreCanvas.height = 175; CELL_WIDTH = canvas.width / data.map[0].length; CELL_HEIGHT = canvas.height / data.map.length; diff --git a/frontend/src/components/Game-Default.js b/frontend/src/components/Game-Default.js index 2ae03e9..07aba0a 100644 --- a/frontend/src/components/Game-Default.js +++ b/frontend/src/components/Game-Default.js @@ -63,7 +63,8 @@ export class GameDefault extends Component {
-
+
+
`; } From 288dacd6a788c0680c2a3557205243719e537e4f Mon Sep 17 00:00:00 2001 From: michang Date: Tue, 27 Aug 2024 20:32:10 +0900 Subject: [PATCH 04/25] =?UTF-8?q?feat=20:=20=EC=A0=90=EC=88=98=EA=B0=80=20?= =?UTF-8?q?=EB=8D=94=20=EC=A2=8B=EA=B2=8C=20=ED=91=9C=EC=8B=9C=EB=90=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/game/consumers.py | 25 +++++++++------- frontend/src/components/Game-Core.js | 43 ++++++++++++++++++---------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/backend/game/consumers.py b/backend/game/consumers.py index 68211ae..1693a5d 100644 --- a/backend/game/consumers.py +++ b/backend/game/consumers.py @@ -10,23 +10,19 @@ SCREEN_HEIGHT = 750 SCREEN_WIDTH = 1250 +MAX_SCORE = 150 class GameState: def __init__(self): print("initializing game state!") self.map = GameMap() - self.left_bar = Bar( - 0, - Bar.X_GAP, - SCREEN_HEIGHT // 2 - Bar.HEIGHT // 2, - 0 - ) + self.left_bar = Bar(0, Bar.X_GAP, SCREEN_HEIGHT // 2 - Bar.HEIGHT // 2, 0) self.right_bar = Bar( 1, SCREEN_WIDTH - Bar.WIDTH - Bar.X_GAP, SCREEN_HEIGHT // 2 - Bar.HEIGHT // 2, - SCREEN_WIDTH - Bar.WIDTH + SCREEN_WIDTH - Bar.WIDTH, ) self.left_ball = Ball(0, SCREEN_HEIGHT, SCREEN_WIDTH, self.left_bar) self.right_ball = Ball(1, SCREEN_HEIGHT, SCREEN_WIDTH, self.right_bar) @@ -34,6 +30,7 @@ def __init__(self): self.player = ["player1", "player2"] self.penalty_time = [0, 0] + class GameConsumer(AsyncWebsocketConsumer): game_tasks = {} game_states = {} @@ -116,9 +113,13 @@ def authenticate(self, token): # Decode JWT token decoded = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) uid = decoded.get("id") - print(f"uid: {uid}", f"room_name: {self.room_name}", str(uid) == str(self.room_name)) + print( + f"uid: {uid}", + f"room_name: {self.room_name}", + str(uid) == str(self.room_name), + ) # Check if uid matches the room_name - if str(uid) == str(self.room_name): + if str(uid) == str(self.room_name): return True else: return False @@ -132,7 +133,9 @@ def authenticate(self, token): async def disconnect(self, close_code): # Leave room group if self.authenticated: - await self.channel_layer.group_discard(self.room_group_name, self.channel_name) + await self.channel_layer.group_discard( + self.room_group_name, self.channel_name + ) # Decrease the client count for this room if self.room_group_name in GameConsumer.client_counts: GameConsumer.client_counts[self.room_group_name] -= 1 @@ -144,7 +147,6 @@ async def disconnect(self, close_code): else: GameConsumer.client_counts[self.room_group_name] = 0 - async def send_initialize_game(self): state = GameConsumer.game_states[self.room_group_name] await self.send( @@ -167,6 +169,7 @@ async def send_initialize_game(self): "ball_radius": Ball.RADIUS, "map": state.map.map, "player": state.player, + "max_score": MAX_SCORE, } ) ) diff --git a/frontend/src/components/Game-Core.js b/frontend/src/components/Game-Core.js index ae1ca7e..54b17e2 100644 --- a/frontend/src/components/Game-Core.js +++ b/frontend/src/components/Game-Core.js @@ -32,9 +32,9 @@ export class GameCore extends Component { }; let SCREEN_HEIGHT, SCREEN_WIDTH, BAR_HEIGHT, BAR_WIDTH, BAR_X_GAP, BALL_RADIUS, LEFT_BAR_X, LEFT_BAR_Y, RIGHT_BAR_X, RIGHT_BAR_Y, - CELL_WIDTH, CELL_HEIGHT, PLAYER; + CELL_WIDTH, CELL_HEIGHT, PLAYER, MAX_SCORE; let counter = 0; - let leftBar, rightBar, leftBall, rightBall, map, score, penaltyTime; + let leftBar, rightBar, leftBall, rightBall, map, score, penaltyTime = [0, 0]; console.log(canvas) function playSound(soundName) { @@ -68,8 +68,7 @@ export class GameCore extends Component { ctx.lineWidth = 2; Map.strokeRoundedRect(ctx, this.x, this.y, this.width, this.height, this.width / 2); ctx.stroke(); - if (penaltyTime[this.id] !== 0) - { + if (penaltyTime[this.id] !== 0) { ctx.font = "bold 30px Arial"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; @@ -108,12 +107,12 @@ export class GameCore extends Component { const alpha = (index + 1) / (this.trail.length + 15); const scale = (index / (this.trail.length + 1)); const radius = this.radius * scale; - + ctx.globalAlpha = alpha; // 투명도 설정 ctx.fillStyle = this.color; ctx.fillRect(pos.x - radius, pos.y - radius, radius * 2, radius * 2); }); - + ctx.globalAlpha = 1; Map.strokeRoundedRect(ctx, this.x - this.radius, this.y - this.radius, this.radius * 2, this.radius * 2, 6); ctx.fillStyle = this.color; @@ -274,27 +273,40 @@ export class GameCore extends Component { class Score { constructor() { - scoreCtx.textAlign = "center"; this.score = [0, 0]; } - + update(score) { this.score = score; } - + draw() { scoreCtx.clearRect(0, 0, scoreCanvas.width, scoreCanvas.height); + scoreCtx.textAlign = "center"; scoreCtx.font = "25px Arial"; scoreCtx.fillStyle = COLOR[1]; - scoreCtx.fillText(PLAYER[0], 125, 50); + scoreCtx.fillText(PLAYER[0], 200, 50); scoreCtx.fillStyle = COLOR[0]; - scoreCtx.fillText(PLAYER[1], 375, 50); - + scoreCtx.fillText(PLAYER[1], scoreCanvas.width - 200, 50); + + scoreCtx.textAlign = "left"; scoreCtx.font = "100px Arial"; scoreCtx.fillStyle = COLOR[1]; - scoreCtx.fillText(this.score[0], 125, 140); + let firstScore = this.score[0].toString(); + scoreCtx.fillText(firstScore, 125, 140); + + let firstScoreWidth = scoreCtx.measureText(firstScore).width; + scoreCtx.font = "50px Arial"; + let scoreText = " / " + MAX_SCORE; + scoreCtx.fillText(scoreText, 125 + firstScoreWidth, 140); + + scoreCtx.textAlign = "right"; scoreCtx.fillStyle = COLOR[0]; - scoreCtx.fillText(this.score[1], 375, 140); + scoreCtx.fillText(scoreText, scoreCanvas.width - 125, 140); + let secondScore = this.score[1].toString(); + let secondScoreX = scoreCanvas.width - 125 - scoreCtx.measureText(scoreText).width; + scoreCtx.font = "100px Arial"; + scoreCtx.fillText(secondScore, secondScoreX, 140); } } @@ -383,9 +395,10 @@ export class GameCore extends Component { BAR_X_GAP = data.bar_x_gap; BALL_RADIUS = data.ball_radius; PLAYER = data.player; + MAX_SCORE = data.max_score; canvas.width = SCREEN_WIDTH; canvas.height = SCREEN_HEIGHT; - scoreCanvas.width = 500; + scoreCanvas.width = 1000; scoreCanvas.height = 175; CELL_WIDTH = canvas.width / data.map[0].length; CELL_HEIGHT = canvas.height / data.map.length; From f5f0228026baa3ce4a87cd6f7fc1df47b1e07bfa Mon Sep 17 00:00:00 2001 From: michang Date: Tue, 27 Aug 2024 20:41:26 +0900 Subject: [PATCH 05/25] =?UTF-8?q?feat=20:=20=EB=AA=A8=EB=93=A0=20=EC=86=8C?= =?UTF-8?q?=EC=BC=93=EC=9D=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/app.js | 37 +++++++++++++++++++--------- frontend/src/components/Game-Core.js | 4 ++- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/frontend/src/app.js b/frontend/src/app.js index 3166403..bbcceed 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -16,22 +16,35 @@ class App { export const root = new App(); export const routes = createRoutes(root); +export let socketList = []; + +const closeAllSockets = () => { + socketList.forEach(socket => { + if (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING) { + socket.close(); + } + }); + socketList = []; +}; + const online = () => { const onlineSocket = new WebSocket( 'wss://' + "localhost:443" + '/ws/online/' - ); - console.log(onlineSocket); - onlineSocket.onopen = () => { - const token = getCookie("jwt"); - onlineSocket.send(JSON.stringify({ 'action': 'authenticate', 'token': token })); - }; - onlineSocket.onclose = () => { - console.log("online socket closed"); - changeUrl("/404", false); - }; - } - + ); + socketList.push(onlineSocket); + console.log(onlineSocket); + onlineSocket.onopen = () => { + const token = getCookie("jwt"); + onlineSocket.send(JSON.stringify({ 'action': 'authenticate', 'token': token })); + }; + onlineSocket.onclose = () => { + console.log("online socket closed"); + closeAllSockets(); + changeUrl("/404", false); + }; +} + initializeRouter(routes); online(); \ No newline at end of file diff --git a/frontend/src/components/Game-Core.js b/frontend/src/components/Game-Core.js index 54b17e2..1319cef 100644 --- a/frontend/src/components/Game-Core.js +++ b/frontend/src/components/Game-Core.js @@ -1,6 +1,7 @@ import { Component } from "../core/Component.js"; import { changeUrl } from "../core/router.js"; import { getCookie } from "../core/jwt.js"; +import { socketList } from "../app.js" export class GameCore extends Component { constructor($el, props) { @@ -9,13 +10,14 @@ export class GameCore extends Component { initState() { this.keysPressed = {}; - this.gameSocket = this.gameSocket = new WebSocket( + this.gameSocket = new WebSocket( 'wss://' + "localhost:443" + '/ws/game/' + this.props.uid + '/' ); + socketList.push(this.gameSocket); return {}; } From 5672a901fca9f992538415deb7cbe50999f37a41 Mon Sep 17 00:00:00 2001 From: michang Date: Tue, 27 Aug 2024 21:05:40 +0900 Subject: [PATCH 06/25] =?UTF-8?q?feat=20:=20=EA=B2=8C=EC=9E=84=20=EC=8A=B9?= =?UTF-8?q?=EB=A6=AC=20=ED=8C=A8=EB=B0=B0=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=9C=EC=83=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/game/consumers.py | 42 ++++++++++++++++++++++++++-- frontend/src/components/Game-Core.js | 11 +++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/backend/game/consumers.py b/backend/game/consumers.py index 1693a5d..896da9b 100644 --- a/backend/game/consumers.py +++ b/backend/game/consumers.py @@ -10,7 +10,7 @@ SCREEN_HEIGHT = 750 SCREEN_WIDTH = 1250 -MAX_SCORE = 150 +MAX_SCORE = 15 class GameState: @@ -75,7 +75,6 @@ async def receive(self, text_data): # Start ball movement if not already running if self.room_group_name not in GameConsumer.game_tasks: - print("starting game loop!") GameConsumer.game_tasks[self.room_group_name] = asyncio.create_task( self.game_loop() ) @@ -196,12 +195,51 @@ async def game_loop(self): await self.send_game_state() await asyncio.sleep(0.00390625) await state.map.init() + if state.score[0] >= MAX_SCORE: + await asyncio.sleep(0.1) + await self.send_game_result(0) + await asyncio.sleep(0.1) + await self.close() + break + elif state.score[1] >= MAX_SCORE: + await asyncio.sleep(0.1) + await self.send_game_result(1) + await asyncio.sleep(0.1) + await self.close() + break else: await asyncio.sleep(0.00390625) count += 1 except asyncio.CancelledError: # Handle the game loop cancellation print("Game loop cancelled for", self.room_group_name) + await self.close() + + async def send_game_result(self, winner): + state = GameConsumer.game_states[self.room_group_name] + await self.channel_layer.group_send( + self.room_group_name, + { + "type": "game_result_message", + "score": state.score, + "winner": winner, + }, + ) + + async def game_result_message(self, event): + score = event["score"] + winner = event["winner"] + + # Send the game result to the WebSocket + await self.send( + text_data=json.dumps( + { + "type": "game_result", + "score": score, + "winner": winner, + } + ) + ) async def send_game_state(self): state = GameConsumer.game_states[self.room_group_name] diff --git a/frontend/src/components/Game-Core.js b/frontend/src/components/Game-Core.js index 1319cef..0fbe335 100644 --- a/frontend/src/components/Game-Core.js +++ b/frontend/src/components/Game-Core.js @@ -145,7 +145,6 @@ export class GameCore extends Component { update(mapDiff) { mapDiff.forEach(diff => { - console.log(mapDiff) this.map[diff[0]][diff[1]] = diff[2]; new Particles(diff[1], diff[0], diff[2]); playSound('collision'); @@ -334,11 +333,21 @@ export class GameCore extends Component { penaltyTime = data.penalty_time; score.update(data.score); map.update(data.map_diff); + } else if (data.type === 'game_result') { + this.gameSocket.close(); + socketList.pop(); + if (data.winner === 0) { + alert(`${PLAYER[0]} wins!`); + } else { + alert(`${PLAYER[1]} wins!`); + } + changeUrl('/main', false); } }; let isPoolingLeft = false, isPoolingRight = false; const handleKeyPresses = () => { + if (this.gameSocket.readyState !== WebSocket.OPEN) return; if (this.keysPressed['w']) { this.gameSocket.send(JSON.stringify({ 'action': 'move_up', 'bar': 'left' })); leftBar.targetY -= 5; From 5a2f539825f5f2c51303f382b6fd1e9511d5d9e4 Mon Sep 17 00:00:00 2001 From: michang Date: Tue, 27 Aug 2024 21:06:57 +0900 Subject: [PATCH 07/25] =?UTF-8?q?feat=20:=20=EB=A1=9C=EC=BB=AC=20=20?= =?UTF-8?q?=EA=B2=8C=EC=9E=84=20=EC=99=84=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/game/consumers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/game/consumers.py b/backend/game/consumers.py index 896da9b..bbbf247 100644 --- a/backend/game/consumers.py +++ b/backend/game/consumers.py @@ -10,7 +10,7 @@ SCREEN_HEIGHT = 750 SCREEN_WIDTH = 1250 -MAX_SCORE = 15 +MAX_SCORE = 150 class GameState: @@ -225,7 +225,7 @@ async def send_game_result(self, winner): "winner": winner, }, ) - + async def game_result_message(self, event): score = event["score"] winner = event["winner"] From c5c711b03d9a7643a58b096fe5876889ba061bd9 Mon Sep 17 00:00:00 2001 From: michang Date: Thu, 29 Aug 2024 20:57:12 +0900 Subject: [PATCH 08/25] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=98=EB=A9=94?= =?UTF-8?q?=EC=9D=B4=ED=82=B9=20=EC=97=94=ED=84=B0=20=EC=8B=9C=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=EC=99=80=20=EC=86=8C=ED=86=B5=20=EC=84=B1=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/game/onlineConsumers.py | 52 +++++++++++++++++++++++++-- backend/game/srcs/Ball.py | 3 +- backend/game/srcs/Bar.py | 3 -- frontend/src/components/Match-Wait.js | 26 ++++++++++++++ 4 files changed, 76 insertions(+), 8 deletions(-) diff --git a/backend/game/onlineConsumers.py b/backend/game/onlineConsumers.py index 5a8e80a..a389369 100644 --- a/backend/game/onlineConsumers.py +++ b/backend/game/onlineConsumers.py @@ -7,6 +7,7 @@ class OnlineConsumer(AsyncWebsocketConsumer): online_user_list = set([]) + matching_queue = [] async def connect(self): # Wait for authentication before joining room group @@ -28,10 +29,14 @@ async def receive(self, text_data): self.authenticated = True OnlineConsumer.online_user_list.add(self.uid) print("Online user list: ", OnlineConsumer.online_user_list) - else: + elif not self.authenticated: print("Invalid action") await self.close(code=4001) return + elif action == "enter-matching": + await self.enter_matching() + elif action == "leave-matching": + await self.leave_matching() def authenticate(self, token): try: @@ -40,10 +45,9 @@ def authenticate(self, token): print("decoded: ", decoded) self.uid = decoded.get("id") print("uid: ", self.uid) - if (OnlineConsumer.online_user_list.__contains__(self.uid)): + if self.uid in OnlineConsumer.online_user_list: print("User already online") return False - # Check if uid matches the room_name return True except jwt.ExpiredSignatureError: print("Token has expired") @@ -52,9 +56,51 @@ def authenticate(self, token): print("Invalid token") return False + async def enter_matching(self): + # Add user to matching queue + if self not in OnlineConsumer.matching_queue: + OnlineConsumer.matching_queue.append(self) + print("Matching queue: ", [user.uid for user in OnlineConsumer.matching_queue]) + + # Check if we have enough users to start a game + if len(OnlineConsumer.matching_queue) >= 2: + await self.start_game() + + async def leave_matching(self): + # Remove user from matching queue + if self in OnlineConsumer.matching_queue: + OnlineConsumer.matching_queue.remove(self) + print(f"User {self.uid} removed from matching queue") + print("Matching queue: ", [user.uid for user in OnlineConsumer.matching_queue]) + + async def start_game(self): + # Take two users from the matching queue + if len(OnlineConsumer.matching_queue) >= 2: + user1 = OnlineConsumer.matching_queue.pop(0) + user2 = OnlineConsumer.matching_queue.pop(0) + + # Create a unique room name for the game + room_name = f"{user1.uid}_{user2.uid}" + + # Move users to the new room and start the game + await user1.channel_layer.group_add(room_name, user1.channel_name) + await user2.channel_layer.group_add(room_name, user2.channel_name) + + # Notify the users that they have been moved to a game room + await user1.send(text_data=json.dumps({ + 'action': 'start_game', + 'room_name': room_name + })) + await user2.send(text_data=json.dumps({ + 'action': 'start_game', + 'room_name': room_name + })) + + print(f"Started game in room: {room_name} with users: {user1.uid}, {user2.uid}") async def disconnect(self, close_code): # Leave room group if self.authenticated: OnlineConsumer.online_user_list.remove(self.uid) + await self.leave_matching() print("Online user list: ", OnlineConsumer.online_user_list) diff --git a/backend/game/srcs/Ball.py b/backend/game/srcs/Ball.py index e1870eb..6569854 100644 --- a/backend/game/srcs/Ball.py +++ b/backend/game/srcs/Ball.py @@ -52,8 +52,7 @@ def check_bar_collision(self, bar): if (bar.state == BarState.RELEASE): self.life = bar.get_ball_power() speed = self.SPEED[self.life] - self.x = bar.min_x + (bar.width if bar.id == 0 else 0) - print("speed", speed) + self.x = bar.min_x + (bar.width if bar.id == 0 else 0) # 바와의 충돌로 dx를 반전시키고, dy를 새로 계산 relative_intersect_y = (bar.y + (bar.height // 2)) - self.y normalized_relative_intersect_y = relative_intersect_y / ( diff --git a/backend/game/srcs/Bar.py b/backend/game/srcs/Bar.py index 2118704..bed20db 100644 --- a/backend/game/srcs/Bar.py +++ b/backend/game/srcs/Bar.py @@ -16,7 +16,6 @@ def __init__( self.min_x = x self.max_x = max_x self.max_power = abs(max_x - x) - print("max_power", self.max_power, " max_x", max_x, "x", x) self.x = x self.y = y self.width = width @@ -37,7 +36,6 @@ def pull(self): if self.power < self.max_power: self.power = abs(self.x - self.min_x) self.x += ((-2 / (self.max_power * 2)) * self.power + 3) * self.pull_speed - print("power", self.power) def set_release(self): if self.state != BarState.PULLING: @@ -46,7 +44,6 @@ def set_release(self): def release(self): self.x += -self.pull_speed * (self.power + 0.1) * 0.25 - print(-self.pull_speed * self.power * 0.25 + 0.1) if (self.id == 0 and self.x > self.min_x + 10) or ( self.id == 1 and self.x < self.min_x - 10 ): diff --git a/frontend/src/components/Match-Wait.js b/frontend/src/components/Match-Wait.js index 5dc27fb..1f098ec 100644 --- a/frontend/src/components/Match-Wait.js +++ b/frontend/src/components/Match-Wait.js @@ -1,8 +1,27 @@ import { Component } from "../core/Component.js"; import { changeUrl } from "../core/router.js"; +import { socketList } from "../app.js" + export class WaitForMatch extends Component { + initState() { + if (socketList[0] !== undefined) + { + console.log("send enter-matching"); + socketList[0].send(JSON.stringify({ 'action': 'enter-matching' })); + socketList[0].onmessage = (e) => { + const data = JSON.parse(e.data); + console.log(data); + if (data.action === 'start_game') { + console.log("start game on " + data.room_id); + changeUrl('/game/vs/' + data.room_id); + } + }; + } + return {}; + } + template () { return `
@@ -34,5 +53,12 @@ export class WaitForMatch extends Component { this.addEvent('click', '#goBack', (event) => { window.history.back(); }); + + const handleSocketClose = (e) => { + socketList[0].send(JSON.stringify({ 'action': 'leave-matching' })); + window.removeEventListener('popstate', handleSocketClose); + } + + window.addEventListener('popstate', handleSocketClose); } } From b0de9b00794206fdec52508d5811b1955ea3215b Mon Sep 17 00:00:00 2001 From: michang Date: Sun, 1 Sep 2024 14:34:32 +0900 Subject: [PATCH 09/25] =?UTF-8?q?feat=20:=20Game-matching=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=EC=9D=98=20=EC=B2=AB=20=EB=B0=9C=EC=9E=90=EA=B5=AD?= =?UTF-8?q?=EC=9D=B4=EB=9D=BC=EA=B3=A0=20=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=20=EC=BB=A4=EB=B0=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Game-matching.js | 8 ++++++++ frontend/src/components/Match-Wait.js | 4 ++-- frontend/src/core/router.js | 5 +++++ 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 frontend/src/components/Game-matching.js diff --git a/frontend/src/components/Game-matching.js b/frontend/src/components/Game-matching.js new file mode 100644 index 0000000..c2403f2 --- /dev/null +++ b/frontend/src/components/Game-matching.js @@ -0,0 +1,8 @@ +import { GameDefault } from "./Game-Default.js"; +import { GameCore } from "./Game-Core.js"; + +export class GameMatching extends GameDefault { + mounted(){ + new GameCore(document.querySelector("div#game"), this.props); + } +} \ No newline at end of file diff --git a/frontend/src/components/Match-Wait.js b/frontend/src/components/Match-Wait.js index 1f098ec..0ace02b 100644 --- a/frontend/src/components/Match-Wait.js +++ b/frontend/src/components/Match-Wait.js @@ -14,8 +14,8 @@ export class WaitForMatch extends Component { const data = JSON.parse(e.data); console.log(data); if (data.action === 'start_game') { - console.log("start game on " + data.room_id); - changeUrl('/game/vs/' + data.room_id); + console.log("start game on " + data.room_name); + changeUrl('/game/vs/' + data.room_name); } }; } diff --git a/frontend/src/core/router.js b/frontend/src/core/router.js index 7753175..487001c 100644 --- a/frontend/src/core/router.js +++ b/frontend/src/core/router.js @@ -9,6 +9,7 @@ import { Error } from "../components/Error.js"; import { Match } from "../components/Match.js"; import { Tournament } from "../components/Tournament.js"; import { GameLocal } from "../components/Game-Local.js"; +import { GameMatching } from "../components/Game-matching.js"; export const createRoutes = (root) => { return { @@ -43,6 +44,10 @@ export const createRoutes = (root) => { component: (props) => new GameLocal(root.app, props), props: { uid: "" } }, + "/game/vs/:room": { + component: (props) => new GameMatching(root.app, props), + props: { room: "" } + }, }; }; From df255cf04352268c2c0173766d0946abe9e639b7 Mon Sep 17 00:00:00 2001 From: michang Date: Sun, 1 Sep 2024 16:18:21 +0900 Subject: [PATCH 10/25] =?UTF-8?q?feat=20:=20=EB=A7=A4=EC=B9=98=EC=97=90=20?= =?UTF-8?q?=EC=B4=88=EB=8C=80=EB=8A=94=20=EB=90=98=EB=8A=94=EB=8D=B0=20loo?= =?UTF-8?q?p=EA=B0=80=20=EC=95=88=20=EB=8F=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/game/matchingConsumers.py | 299 +++++++++++ backend/game/onlineConsumers.py | 44 +- backend/game/routing.py | 2 + frontend/src/components/Game-matching-Core.js | 474 ++++++++++++++++++ frontend/src/components/Game-matching.js | 4 +- 5 files changed, 807 insertions(+), 16 deletions(-) create mode 100644 backend/game/matchingConsumers.py create mode 100644 frontend/src/components/Game-matching-Core.js diff --git a/backend/game/matchingConsumers.py b/backend/game/matchingConsumers.py new file mode 100644 index 0000000..a4bf62f --- /dev/null +++ b/backend/game/matchingConsumers.py @@ -0,0 +1,299 @@ +import json +import asyncio +from channels.generic.websocket import AsyncWebsocketConsumer +from game.srcs.Ball import Ball +from game.srcs.Bar import Bar +from game.srcs.GameMap import GameMap +import jwt +from django.conf import settings +from channels.exceptions import DenyConnection + +SCREEN_HEIGHT = 750 +SCREEN_WIDTH = 1250 +MAX_SCORE = 150 + + +class MatchingGameState: + def __init__(self, user1, user2): + print("initializing game state!") + self.user = [user1, user2] + self.user_athenticated = [False, False] + self.map = GameMap() + self.left_bar = Bar(0, Bar.X_GAP, SCREEN_HEIGHT // 2 - Bar.HEIGHT // 2, 0) + self.right_bar = Bar( + 1, + SCREEN_WIDTH - Bar.WIDTH - Bar.X_GAP, + SCREEN_HEIGHT // 2 - Bar.HEIGHT // 2, + SCREEN_WIDTH - Bar.WIDTH, + ) + self.left_ball = Ball(0, SCREEN_HEIGHT, SCREEN_WIDTH, self.left_bar) + self.right_ball = Ball(1, SCREEN_HEIGHT, SCREEN_WIDTH, self.right_bar) + self.score = [0, 0] + self.player = ["player1", "player2"] + self.penalty_time = [0, 0] + + +class MatchingGameConsumer(AsyncWebsocketConsumer): + game_tasks = {} + game_states = {} + client_counts = {} + + async def connect(self): + self.room_name = self.scope["url_route"]["kwargs"]["room_name"] + self.room_group_name = self.room_name + print(f"room_name: {self.room_group_name}") + + self.authenticated = False + + await self.accept() + + async def receive(self, text_data): + text_data_json = json.loads(text_data) + action = text_data_json["action"] + + if action == "authenticate": + token = text_data_json.get("token") + if not token or not self.authenticate(token): + print("authentication failed") + await self.close(code=4001) + return + self.authenticated = True + + # Join room group after successful authentication + await self.channel_layer.group_add(self.room_group_name, self.channel_name) + + # # Initialize game state for the room if it doesn't exist + # if self.room_group_name not in MatchingGameConsumer.game_states: + # print("new game!") + # MatchingGameConsumer.game_states[self.room_group_name] = GameState() + # MatchingGameConsumer.client_counts[self.room_group_name] = 0 + + # MatchingGameConsumer.client_counts[self.room_group_name] += 1 + + # # Send initialize game state to the client + await self.send_initialize_game() + + # Start ball movement if not already running + state = MatchingGameConsumer.game_states[self.room_group_name] + # if ( + # self.room_group_name not in MatchingGameConsumer.game_tasks + # and state.user_athenticated[0] + # and state.user_athenticated[1] + # ): + # print("Starting game loop") + # MatchingGameConsumer.game_tasks[self.room_group_name] = ( + # asyncio.create_task(self.game_loop()) + # ) + elif not self.authenticated: + await self.close(code=4001) + else: + bar = text_data_json.get("bar") + state = MatchingGameConsumer.game_states[self.room_group_name] + + # Update the bar position based on the action + if bar == "left": + if action == "move_up": + state.left_bar.move_up(Bar.SPEED) + elif action == "move_down": + state.left_bar.move_down(Bar.SPEED, SCREEN_HEIGHT) + elif action == "pull": + state.left_bar.pull() + elif action == "release": + state.left_bar.set_release() + elif bar == "right": + if action == "move_up": + state.right_bar.move_up(Bar.SPEED) + elif action == "move_down": + state.right_bar.move_down(Bar.SPEED, SCREEN_HEIGHT) + elif action == "pull": + state.right_bar.pull() + elif action == "release": + state.right_bar.set_release() + + # Update game state after moving bar or resetting game + await self.send_game_state() + + def authenticate(self, token): + try: + # Decode JWT token + decoded = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) + uid = decoded.get("id") + # Check if uid matches the room_name + state = MatchingGameConsumer.game_states[self.room_group_name] + if str(uid) == str(state.user[0]) or str(uid) == str(state.user[1]): + return True + else: + return False + except jwt.ExpiredSignatureError: + print("Token has expired") + return False + except jwt.InvalidTokenError: + print("Invalid token") + return False + + async def disconnect(self, close_code): + # Leave room group + if self.authenticated: + await self.channel_layer.group_discard( + self.room_group_name, self.channel_name + ) + # Decrease the client count for this room + if self.room_group_name in MatchingGameConsumer.client_counts: + MatchingGameConsumer.client_counts[self.room_group_name] -= 1 + if MatchingGameConsumer.client_counts[self.room_group_name] <= 0: + MatchingGameConsumer.game_tasks[self.room_group_name].cancel() + del MatchingGameConsumer.game_tasks[self.room_group_name] + del MatchingGameConsumer.game_states[self.room_group_name] + del MatchingGameConsumer.client_counts[self.room_group_name] + else: + MatchingGameConsumer.client_counts[self.room_group_name] = 0 + + async def send_initialize_game(self): + state = MatchingGameConsumer.game_states[self.room_group_name] + await self.send( + text_data=json.dumps( + { + "type": "initialize_game", + "left_bar_x": state.left_bar.x, + "left_bar_y": state.left_bar.y, + "right_bar_x": state.right_bar.x, + "right_bar_y": state.right_bar.y, + "screen_height": SCREEN_HEIGHT, + "screen_width": SCREEN_WIDTH, + "bar_height": Bar.HEIGHT, + "bar_width": Bar.WIDTH, + "bar_x_gap": Bar.X_GAP, + "left_ball_x": state.left_ball.x, + "left_ball_y": state.left_ball.y, + "right_ball_x": state.right_ball.x, + "right_ball_y": state.right_ball.y, + "ball_radius": Ball.RADIUS, + "map": state.map.map, + "player": state.player, + "max_score": MAX_SCORE, + } + ) + ) + + async def game_loop(self): + print("!!Game loop started for", self.room_group_name) + try: + count = 0 + while True: + state = MatchingGameConsumer.game_states[self.room_group_name] + state.left_bar.update() + state.right_bar.update() + + state.penalty_time[0] = state.left_ball.move(state.left_bar) + state.left_ball.check_bar_collision(state.left_bar) + state.left_ball.check_bar_collision(state.right_bar) + state.left_ball.check_collision(state.map, state.score) + + state.penalty_time[1] = state.right_ball.move(state.right_bar) + state.right_ball.check_bar_collision(state.left_bar) + state.right_ball.check_bar_collision(state.right_bar) + state.right_ball.check_collision(state.map, state.score) + + if count % 5 == 0: + await self.send_game_state() + await asyncio.sleep(0.00390625) + await state.map.init() + if state.score[0] >= MAX_SCORE: + await asyncio.sleep(0.1) + await self.send_game_result(0) + await asyncio.sleep(0.1) + await self.close() + break + elif state.score[1] >= MAX_SCORE: + await asyncio.sleep(0.1) + await self.send_game_result(1) + await asyncio.sleep(0.1) + await self.close() + break + else: + await asyncio.sleep(0.00390625) + count += 1 + except asyncio.CancelledError: + # Handle the game loop cancellation + print("Game loop cancelled for", self.room_group_name) + await self.close() + + async def send_game_result(self, winner): + state = MatchingGameConsumer.game_states[self.room_group_name] + await self.channel_layer.group_send( + self.room_group_name, + { + "type": "game_result_message", + "score": state.score, + "winner": winner, + }, + ) + + async def game_result_message(self, event): + score = event["score"] + winner = event["winner"] + + # Send the game result to the WebSocket + await self.send( + text_data=json.dumps( + { + "type": "game_result", + "score": score, + "winner": winner, + } + ) + ) + + async def send_game_state(self): + state = MatchingGameConsumer.game_states[self.room_group_name] + await self.channel_layer.group_send( + self.room_group_name, + { + "type": "update_game_state_message", + "left_bar_x": state.left_bar.x, + "left_bar_y": state.left_bar.y, + "right_bar_x": state.right_bar.x, + "right_bar_y": state.right_bar.y, + "left_ball_x": state.left_ball.x, + "left_ball_y": state.left_ball.y, + "right_ball_x": state.right_ball.x, + "right_ball_y": state.right_ball.y, + "map_diff": state.map.diff, + "score": state.score, + "penalty_time": state.penalty_time, + }, + ) + + async def update_game_state_message(self, event): + left_bar_x = event["left_bar_x"] + left_bar_y = event["left_bar_y"] + right_bar_x = event["right_bar_x"] + right_bar_y = event["right_bar_y"] + left_ball_x = event["left_ball_x"] + left_ball_y = event["left_ball_y"] + right_ball_x = event["right_ball_x"] + right_ball_y = event["right_ball_y"] + score = event["score"] + penalty_time = event["penalty_time"] + + # Send the updated game state to the WebSocket + await self.send( + text_data=json.dumps( + { + "type": "update_game_state", + "left_bar_x": left_bar_x, + "left_bar_y": left_bar_y, + "right_bar_x": right_bar_x, + "right_bar_y": right_bar_y, + "left_ball_x": int(left_ball_x), + "left_ball_y": int(left_ball_y), + "right_ball_x": int(right_ball_x), + "right_ball_y": int(right_ball_y), + "map_diff": MatchingGameConsumer.game_states[ + self.room_group_name + ].map.diff, + "score": score, + "penalty_time": penalty_time, + } + ) + ) diff --git a/backend/game/onlineConsumers.py b/backend/game/onlineConsumers.py index a389369..289972f 100644 --- a/backend/game/onlineConsumers.py +++ b/backend/game/onlineConsumers.py @@ -4,6 +4,8 @@ import jwt from django.conf import settings from channels.exceptions import DenyConnection +from .matchingConsumers import MatchingGameConsumer, MatchingGameState + class OnlineConsumer(AsyncWebsocketConsumer): online_user_list = set([]) @@ -60,7 +62,9 @@ async def enter_matching(self): # Add user to matching queue if self not in OnlineConsumer.matching_queue: OnlineConsumer.matching_queue.append(self) - print("Matching queue: ", [user.uid for user in OnlineConsumer.matching_queue]) + print( + "Matching queue: ", [user.uid for user in OnlineConsumer.matching_queue] + ) # Check if we have enough users to start a game if len(OnlineConsumer.matching_queue) >= 2: @@ -71,7 +75,9 @@ async def leave_matching(self): if self in OnlineConsumer.matching_queue: OnlineConsumer.matching_queue.remove(self) print(f"User {self.uid} removed from matching queue") - print("Matching queue: ", [user.uid for user in OnlineConsumer.matching_queue]) + print( + "Matching queue: ", [user.uid for user in OnlineConsumer.matching_queue] + ) async def start_game(self): # Take two users from the matching queue @@ -82,21 +88,31 @@ async def start_game(self): # Create a unique room name for the game room_name = f"{user1.uid}_{user2.uid}" - # Move users to the new room and start the game + # Initialize game state for the room + MatchingGameConsumer.game_states[room_name] = MatchingGameState( + user1.uid, user2.uid + ) + MatchingGameConsumer.client_counts[room_name] = 2 + + # Start the game loop + MatchingGameConsumer.game_tasks[room_name] = asyncio.create_task( + MatchingGameConsumer.game_loop(room_name) + ) + + # Notify the users that they have been moved to a game room await user1.channel_layer.group_add(room_name, user1.channel_name) await user2.channel_layer.group_add(room_name, user2.channel_name) - # Notify the users that they have been moved to a game room - await user1.send(text_data=json.dumps({ - 'action': 'start_game', - 'room_name': room_name - })) - await user2.send(text_data=json.dumps({ - 'action': 'start_game', - 'room_name': room_name - })) - - print(f"Started game in room: {room_name} with users: {user1.uid}, {user2.uid}") + await user1.send( + text_data=json.dumps({"action": "start_game", "room_name": room_name}) + ) + await user2.send( + text_data=json.dumps({"action": "start_game", "room_name": room_name}) + ) + + print( + f"Users {user1.uid} and {user2.uid} moved to game room: {room_name}" + ) async def disconnect(self, close_code): # Leave room group diff --git a/backend/game/routing.py b/backend/game/routing.py index 291d765..4aacd57 100644 --- a/backend/game/routing.py +++ b/backend/game/routing.py @@ -2,8 +2,10 @@ from . import consumers from . import onlineConsumers +from . import matchingConsumers websocket_urlpatterns = [ re_path(r"ws/game/(?P\w+)/$", consumers.GameConsumer.as_asgi()), + re_path(r"ws/game/vs/(?P\w+)/$", matchingConsumers.MatchingGameConsumer.as_asgi()), re_path(r"ws/online", onlineConsumers.OnlineConsumer.as_asgi()), ] diff --git a/frontend/src/components/Game-matching-Core.js b/frontend/src/components/Game-matching-Core.js new file mode 100644 index 0000000..6b730c4 --- /dev/null +++ b/frontend/src/components/Game-matching-Core.js @@ -0,0 +1,474 @@ +import { Component } from "../core/Component.js"; +import { changeUrl } from "../core/router.js"; +import { getCookie } from "../core/jwt.js"; +import { socketList } from "../app.js" + +export class GameMatchingCore extends Component { + constructor($el, props) { + super($el, props); + } + + initState() { + this.keysPressed = {}; + this.gameSocket = new WebSocket( + 'wss://' + + "localhost:443" + + '/ws/game/vs/' + + this.props.room + + '/' + ); + socketList.push(this.gameSocket); + return {}; + } + + gameStart() { + const COLOR = ["#f5f6fa", "#2f3640", "#f5f6fa"]; + const BALL_COLOR = ["#FF4C4C", "#FFF078"]; + const canvas = document.getElementById('game-canvas'); + const scoreCanvas = document.getElementById('game-score'); + canvas.setAttribute('tabindex', '0'); + const ctx = canvas.getContext('2d'); + const scoreCtx = scoreCanvas.getContext('2d'); + const sounds = { + 'collision': new Audio('../../img/key.mp3'), + }; + let SCREEN_HEIGHT, SCREEN_WIDTH, BAR_HEIGHT, BAR_WIDTH, BAR_X_GAP, + BALL_RADIUS, LEFT_BAR_X, LEFT_BAR_Y, RIGHT_BAR_X, RIGHT_BAR_Y, + CELL_WIDTH, CELL_HEIGHT, PLAYER, MAX_SCORE; + let counter = 0; + let leftBar, rightBar, leftBall, rightBall, map, score, penaltyTime = [0, 0]; + + console.log(canvas) + function playSound(soundName) { + var sound = sounds[soundName]; + if (sound) { + sound.currentTime = 0; + sound.play().catch(function (error) { + console.log('Autoplay was prevented:', error); + }); + } + } + + class Bar { + constructor(x, y, width, height, canvasHeight, id) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.targetX = x; + this.targetY = y; + this.canvasHeight = canvasHeight; + this.speed = 4; + this.id = id; + } + + draw() { + ctx.fillStyle = COLOR[this.id + 1]; + Map.strokeRoundedRect(ctx, this.x, this.y, this.width, this.height, this.width / 2); + ctx.fill(); + ctx.strokeStyle = COLOR[this.id]; + ctx.lineWidth = 2; + Map.strokeRoundedRect(ctx, this.x, this.y, this.width, this.height, this.width / 2); + ctx.stroke(); + if (penaltyTime[this.id] !== 0) { + ctx.font = "bold 30px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = BALL_COLOR[this.id]; + ctx.fillText(penaltyTime[this.id], this.x + this.width / 2 + (this.id == 0 ? 1 : -1) * 175, this.y + this.height / 2); + } + } + + update() { + if (Math.abs(this.targetY - this.y) < this.speed) { + this.y = this.targetY; + } else { + if (this.targetY > this.y) { + this.y += this.speed; + } else { + this.y -= this.speed; + } + } + this.x = this.targetX; + } + } + + class Ball { + constructor(x, y, radius, color) { + this.x = x; + this.y = y; + this.targetX = x; + this.targetY = y; + this.radius = radius; + this.color = color; + this.trail = []; // 잔상을 저장할 배열 + } + + draw() { + this.trail.forEach((pos, index) => { + const alpha = (index + 1) / (this.trail.length + 15); + const scale = (index / (this.trail.length + 1)); + const radius = this.radius * scale; + + ctx.globalAlpha = alpha; // 투명도 설정 + ctx.fillStyle = this.color; + ctx.fillRect(pos.x - radius, pos.y - radius, radius * 2, radius * 2); + }); + + ctx.globalAlpha = 1; + Map.strokeRoundedRect(ctx, this.x - this.radius, this.y - this.radius, this.radius * 2, this.radius * 2, 6); + ctx.fillStyle = this.color; + ctx.fill(); + } + update() { + this.x = this.targetX; + this.y = this.targetY; + // 10 프레임마다 잔상 저장 + if (counter % 3 === 0) { + this.trail.push({ x: this.x, y: this.y }); + + if (this.trail.length > 10) { + this.trail.shift(); // 오래된 잔상을 제거 + } + } + + } + } + + class Map { + constructor(map) { + this.map = map; + this.height = map.length; + this.width = map[0].length; + this.lintDash = [CELL_WIDTH / 5, CELL_WIDTH * 3 / 5]; + } + + update(mapDiff) { + mapDiff.forEach(diff => { + this.map[diff[0]][diff[1]] = diff[2]; + new Particles(diff[1], diff[0], diff[2]); + playSound('collision'); + }); + } + + draw() { + for (let i = 0; i < this.height; i++) { + for (let j = 0; j < this.width; j++) { + ctx.fillStyle = COLOR[this.map[i][j]]; + Map.strokeRoundedRect( + ctx, + j * CELL_WIDTH + 2, + i * CELL_HEIGHT + 2, + CELL_WIDTH - 4, + CELL_HEIGHT - 4, + 5 + ); + } + } + } + + static strokeRoundedRect(ctx, x, y, width, height, radius) { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.arcTo(x + width, y, x + width, y + height, radius); + ctx.arcTo(x + width, y + height, x, y + height, radius); + ctx.arcTo(x, y + height, x, y, radius); + ctx.arcTo(x, y, x + width, y, radius); + ctx.fill(); + ctx.closePath(); + } + } + + class Particle { + constructor(x, y, r, vx, vy, color) { + this.x = x; + this.y = y; + this.r = r; + this.vx = vx; + this.vy = vy; + this.color = color; + this.opacity = 0.5; + this.g = 0.05; + this.friction = 0.99; + } + + draw() { + ctx.fillStyle = this.color + `${this.opacity})`; + ctx.fillRect(this.x, this.y, this.r, this.r); + } + + update() { + this.vx *= this.friction; + this.vy *= this.friction; + this.vy += this.g; + this.x += this.vx; + this.y += this.vy; + this.opacity -= 0.004; + this.draw(); + } + } + + class Particles { + static array = []; + + constructor(x, y, blockId) { + this.x = x * CELL_WIDTH + CELL_WIDTH / 2; + this.y = y * CELL_HEIGHT + CELL_HEIGHT / 2; + this.blockId = blockId; + this.color = Particles.hexToRgba(COLOR[blockId + 1]); + this.particles = []; + const particleCount = 50; + const power = 20; + const radians = (Math.PI * 2) / particleCount; + this.state = 1; + + for (let i = 0; i < particleCount; i++) { + this.particles.push( + new Particle( + this.x, + this.y, + 5, + Math.cos(radians * i) * (Math.random() * power), + Math.sin(radians * i) * (Math.random() * power), + this.color + ) + ); + } + Particles.array.push(this); + } + + draw() { + for (let i = this.particles.length - 1; i >= 0; i--) { + if (this.particles[i].opacity >= 0) { + this.particles[i].update(); + } else { + this.particles.splice(i, 1); + } + } + if (this.particles.length <= 0) { + this.state = -1; + } + } + + static hexToRgba(hex) { + let cleanedHex = hex.replace('#', ''); + if (cleanedHex.length === 3) + cleanedHex = cleanedHex.split('').map(hex => hex + hex).join(''); + const bigint = parseInt(cleanedHex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + + return `rgba(${r}, ${g}, ${b},`; + } + + static drawAll() { + for (let i = Particles.array.length - 1; i >= 0; i--) { + if (Particles.array[i].state == 1) + Particles.array[i].draw(); + else + Particles.array.splice(i, 1); + } + } + } + + class Score { + constructor() { + this.score = [0, 0]; + } + + update(score) { + this.score = score; + } + + draw() { + scoreCtx.clearRect(0, 0, scoreCanvas.width, scoreCanvas.height); + scoreCtx.textAlign = "center"; + scoreCtx.font = "25px Arial"; + scoreCtx.fillStyle = COLOR[1]; + scoreCtx.fillText(PLAYER[0], 200, 50); + scoreCtx.fillStyle = COLOR[0]; + scoreCtx.fillText(PLAYER[1], scoreCanvas.width - 200, 50); + + scoreCtx.textAlign = "left"; + scoreCtx.font = "100px Arial"; + scoreCtx.fillStyle = COLOR[1]; + let firstScore = this.score[0].toString(); + scoreCtx.fillText(firstScore, 125, 140); + + let firstScoreWidth = scoreCtx.measureText(firstScore).width; + scoreCtx.font = "50px Arial"; + let scoreText = " / " + MAX_SCORE; + scoreCtx.fillText(scoreText, 125 + firstScoreWidth, 140); + + scoreCtx.textAlign = "right"; + scoreCtx.fillStyle = COLOR[0]; + scoreCtx.fillText(scoreText, scoreCanvas.width - 125, 140); + let secondScore = this.score[1].toString(); + let secondScoreX = scoreCanvas.width - 125 - scoreCtx.measureText(scoreText).width; + scoreCtx.font = "100px Arial"; + scoreCtx.fillText(secondScore, secondScoreX, 140); + } + } + + this.gameSocket.onopen = () => { + const token = getCookie("jwt"); + this.gameSocket.send(JSON.stringify({ 'action': 'authenticate', 'token': token })); + }; + + this.gameSocket.onmessage = (e) => { + const data = JSON.parse(e.data); + if (data.type === 'initialize_game') { + initializeGame(data); + drawGame(); + } else if (data.type === 'update_game_state') { + leftBar.targetX = data.left_bar_x; + leftBar.targetY = data.left_bar_y; + rightBar.targetX = data.right_bar_x; + rightBar.targetY = data.right_bar_y; + leftBall.targetX = data.left_ball_x; + leftBall.targetY = data.left_ball_y; + rightBall.targetX = data.right_ball_x; + rightBall.targetY = data.right_ball_y; + penaltyTime = data.penalty_time; + score.update(data.score); + map.update(data.map_diff); + } else if (data.type === 'game_result') { + this.gameSocket.close(); + socketList.pop(); + if (data.winner === 0) { + alert(`${PLAYER[0]} wins!`); + } else { + alert(`${PLAYER[1]} wins!`); + } + changeUrl('/main', false); + } + }; + + let isPoolingLeft = false, isPoolingRight = false; + const handleKeyPresses = () => { + if (this.gameSocket.readyState !== WebSocket.OPEN) return; + if (this.keysPressed['w']) { + this.gameSocket.send(JSON.stringify({ 'action': 'move_up', 'bar': 'left' })); + leftBar.targetY -= 5; + } + if (this.keysPressed['s']) { + this.gameSocket.send(JSON.stringify({ 'action': 'move_down', 'bar': 'left' })); + leftBar.targetY += 5; + } + if (this.keysPressed['a']) { + this.gameSocket.send(JSON.stringify({ 'action': 'pull', 'bar': 'left' })); + isPoolingLeft = true; + } else if (isPoolingLeft) { + this.gameSocket.send(JSON.stringify({ 'action': 'release', 'bar': 'left' })); + isPoolingLeft = false; + } + + if (this.keysPressed['ArrowUp']) { + this.gameSocket.send(JSON.stringify({ 'action': 'move_up', 'bar': 'right' })); + rightBar.targetY -= 5; + } + if (this.keysPressed['ArrowDown']) { + this.gameSocket.send(JSON.stringify({ 'action': 'move_down', 'bar': 'right' })); + rightBar.targetY += 5; + } + if (this.keysPressed['ArrowRight']) { + this.gameSocket.send(JSON.stringify({ 'action': 'pull', 'bar': 'right' })); + isPoolingRight = true; + } else if (isPoolingRight) { + this.gameSocket.send(JSON.stringify({ 'action': 'release', 'bar': 'right' })); + isPoolingRight = false; + } + } + + function drawGame() { + // ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "#92969D"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + map.draw(); + leftBar.draw(); + rightBar.draw(); + leftBall.draw(); + rightBall.draw(); + Particles.drawAll(); + score.draw(); + } + + function initializeGame(data) { + SCREEN_HEIGHT = data.screen_height; + SCREEN_WIDTH = data.screen_width; + LEFT_BAR_X = data.left_bar_x; + LEFT_BAR_Y = data.left_bar_y; + RIGHT_BAR_X = data.right_bar_x; + RIGHT_BAR_Y = data.right_bar_y; + BAR_HEIGHT = data.bar_height; + BAR_WIDTH = data.bar_width; + BAR_X_GAP = data.bar_x_gap; + BALL_RADIUS = data.ball_radius; + PLAYER = data.player; + MAX_SCORE = data.max_score; + canvas.width = SCREEN_WIDTH; + canvas.height = SCREEN_HEIGHT; + scoreCanvas.width = 1000; + scoreCanvas.height = 175; + CELL_WIDTH = canvas.width / data.map[0].length; + CELL_HEIGHT = canvas.height / data.map.length; + + map = new Map(data.map, COLOR[0], COLOR[1]); + leftBar = new Bar(LEFT_BAR_X, LEFT_BAR_Y, BAR_WIDTH, BAR_HEIGHT, SCREEN_HEIGHT, 0); + rightBar = new Bar(RIGHT_BAR_X, RIGHT_BAR_Y, BAR_WIDTH, BAR_HEIGHT, SCREEN_HEIGHT, 1); + leftBall = new Ball(data.left_ball_x, data.left_ball_y, BALL_RADIUS, BALL_COLOR[0]); + rightBall = new Ball(data.right_ball_x, data.right_ball_y, BALL_RADIUS, BALL_COLOR[1]); + score = new Score(); + + console.log(SCREEN_HEIGHT, SCREEN_WIDTH, BAR_HEIGHT, BAR_WIDTH, BALL_RADIUS); + setInterval(interpolate, 3); + } + + function interpolate() { + leftBar.update(); + rightBar.update(); + leftBall.update(); + rightBall.update(); + drawGame(); + + if (counter % 6 === 0) + handleKeyPresses(); + counter++; + } + } + + template() { + return ` + + + + `; + } + + setEvent() { + const handleKeyDown = (e) => { + this.keysPressed[e.key] = true; + } + const handleKeyUp = (e) => { + this.keysPressed[e.key] = false; + } + + const handleSocketClose = (e) => { + this.gameSocket.close(); + window.removeEventListener('popstate', handleSocketClose); + document.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('keyup', handleKeyUp); + } + + window.addEventListener('popstate', handleSocketClose); + document.addEventListener('keydown', handleKeyDown); + document.addEventListener('keyup', handleKeyUp); + } + + mounted() { + this.gameStart(); + } +} \ No newline at end of file diff --git a/frontend/src/components/Game-matching.js b/frontend/src/components/Game-matching.js index c2403f2..7ffc3d5 100644 --- a/frontend/src/components/Game-matching.js +++ b/frontend/src/components/Game-matching.js @@ -1,8 +1,8 @@ import { GameDefault } from "./Game-Default.js"; -import { GameCore } from "./Game-Core.js"; +import { GameMatchingCore } from "./Game-matching-Core.js"; export class GameMatching extends GameDefault { mounted(){ - new GameCore(document.querySelector("div#game"), this.props); + new GameMatchingCore(document.querySelector("div#game"), this.props); } } \ No newline at end of file From f7dff2ff13ec08d6b87981bf0e389a29d044ef37 Mon Sep 17 00:00:00 2001 From: michang Date: Sat, 14 Sep 2024 17:29:57 +0900 Subject: [PATCH 11/25] =?UTF-8?q?feat=20multi-game=20invite=20room=20:=20?= =?UTF-8?q?=EB=B0=A9=EC=97=90=20=EB=93=A4=EC=96=B4=EC=99=80=EC=84=9C=20?= =?UTF-8?q?=EC=84=9C=EB=A1=9C=20=EB=8F=99=EC=9D=BC=ED=95=9C=20=EA=B2=8C?= =?UTF-8?q?=EC=9E=84=20=EB=A3=A8=ED=94=84=20=EB=94=B0=EB=A5=B4=EB=8A=94=20?= =?UTF-8?q?=EA=B2=83=EA=B9=8C=EC=A7=80=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/game/matchingConsumers.py | 43 ++++++++--------- backend/game/onlineConsumers.py | 77 +++++++++++++++---------------- backend/transcendence/settings.py | 2 +- 3 files changed, 57 insertions(+), 65 deletions(-) diff --git a/backend/game/matchingConsumers.py b/backend/game/matchingConsumers.py index a4bf62f..6b63502 100644 --- a/backend/game/matchingConsumers.py +++ b/backend/game/matchingConsumers.py @@ -17,7 +17,7 @@ class MatchingGameState: def __init__(self, user1, user2): print("initializing game state!") self.user = [user1, user2] - self.user_athenticated = [False, False] + self.user_authenticated = [False, False] self.map = GameMap() self.left_bar = Bar(0, Bar.X_GAP, SCREEN_HEIGHT // 2 - Bar.HEIGHT // 2, 0) self.right_bar = Bar( @@ -41,8 +41,6 @@ class MatchingGameConsumer(AsyncWebsocketConsumer): async def connect(self): self.room_name = self.scope["url_route"]["kwargs"]["room_name"] self.room_group_name = self.room_name - print(f"room_name: {self.room_group_name}") - self.authenticated = False await self.accept() @@ -62,28 +60,21 @@ async def receive(self, text_data): # Join room group after successful authentication await self.channel_layer.group_add(self.room_group_name, self.channel_name) - # # Initialize game state for the room if it doesn't exist - # if self.room_group_name not in MatchingGameConsumer.game_states: - # print("new game!") - # MatchingGameConsumer.game_states[self.room_group_name] = GameState() - # MatchingGameConsumer.client_counts[self.room_group_name] = 0 - - # MatchingGameConsumer.client_counts[self.room_group_name] += 1 - # # Send initialize game state to the client await self.send_initialize_game() - # Start ball movement if not already running + # Update authentication status in the game state state = MatchingGameConsumer.game_states[self.room_group_name] - # if ( - # self.room_group_name not in MatchingGameConsumer.game_tasks - # and state.user_athenticated[0] - # and state.user_athenticated[1] - # ): - # print("Starting game loop") - # MatchingGameConsumer.game_tasks[self.room_group_name] = ( - # asyncio.create_task(self.game_loop()) - # ) + state.user_authenticated[self.user_index] = True + + # Check if both users are authenticated, then start the game_loop + if all(state.user_authenticated): + # Start the game loop if not already started + if self.room_group_name not in MatchingGameConsumer.game_tasks: + print("user_authenticated", state.user_authenticated) + MatchingGameConsumer.game_tasks[self.room_group_name] = ( + asyncio.create_task(self.game_loop()) + ) elif not self.authenticated: await self.close(code=4001) else: @@ -120,15 +111,17 @@ def authenticate(self, token): uid = decoded.get("id") # Check if uid matches the room_name state = MatchingGameConsumer.game_states[self.room_group_name] - if str(uid) == str(state.user[0]) or str(uid) == str(state.user[1]): + if str(uid) == str(state.user[0]): + self.user_index = 0 + return True + elif str(uid) == str(state.user[1]): + self.user_index = 1 return True else: return False except jwt.ExpiredSignatureError: - print("Token has expired") return False except jwt.InvalidTokenError: - print("Invalid token") return False async def disconnect(self, close_code): @@ -212,6 +205,7 @@ async def game_loop(self): break else: await asyncio.sleep(0.00390625) + # await asyncio.sleep(0.016) count += 1 except asyncio.CancelledError: # Handle the game loop cancellation @@ -275,7 +269,6 @@ async def update_game_state_message(self, event): right_ball_y = event["right_ball_y"] score = event["score"] penalty_time = event["penalty_time"] - # Send the updated game state to the WebSocket await self.send( text_data=json.dumps( diff --git a/backend/game/onlineConsumers.py b/backend/game/onlineConsumers.py index 289972f..bfed8e7 100644 --- a/backend/game/onlineConsumers.py +++ b/backend/game/onlineConsumers.py @@ -10,6 +10,38 @@ class OnlineConsumer(AsyncWebsocketConsumer): online_user_list = set([]) matching_queue = [] + matching_task = None + + @classmethod + async def start_matching_task(cls): + """중앙에서 매칭을 처리하는 task""" + while True: + await asyncio.sleep(2) + if len(cls.matching_queue) >= 2: + await cls.start_game() + + @classmethod + async def start_game(cls): + user1 = cls.matching_queue.pop(0) + user2 = cls.matching_queue.pop(0) + + # Create a unique room name for the game + room_name = f"{user1.uid}_{user2.uid}" + + # Initialize game state for the room + MatchingGameConsumer.game_states[room_name] = MatchingGameState( + user1.uid, user2.uid + ) + MatchingGameConsumer.client_counts[room_name] = 2 + + await user1.send( + text_data=json.dumps({"action": "start_game", "room_name": room_name}) + ) + await user2.send( + text_data=json.dumps({"action": "start_game", "room_name": room_name}) + ) + + print(f"Users {user1.uid} and {user2.uid} moved to game room: {room_name}") async def connect(self): # Wait for authentication before joining room group @@ -17,6 +49,12 @@ async def connect(self): self.authenticated = False await self.accept() + # 매칭 task가 없으면 시작 + if OnlineConsumer.matching_task is None: + OnlineConsumer.matching_task = asyncio.create_task( + OnlineConsumer.start_matching_task() + ) + async def receive(self, text_data): text_data_json = json.loads(text_data) action = text_data_json["action"] @@ -66,10 +104,6 @@ async def enter_matching(self): "Matching queue: ", [user.uid for user in OnlineConsumer.matching_queue] ) - # Check if we have enough users to start a game - if len(OnlineConsumer.matching_queue) >= 2: - await self.start_game() - async def leave_matching(self): # Remove user from matching queue if self in OnlineConsumer.matching_queue: @@ -79,41 +113,6 @@ async def leave_matching(self): "Matching queue: ", [user.uid for user in OnlineConsumer.matching_queue] ) - async def start_game(self): - # Take two users from the matching queue - if len(OnlineConsumer.matching_queue) >= 2: - user1 = OnlineConsumer.matching_queue.pop(0) - user2 = OnlineConsumer.matching_queue.pop(0) - - # Create a unique room name for the game - room_name = f"{user1.uid}_{user2.uid}" - - # Initialize game state for the room - MatchingGameConsumer.game_states[room_name] = MatchingGameState( - user1.uid, user2.uid - ) - MatchingGameConsumer.client_counts[room_name] = 2 - - # Start the game loop - MatchingGameConsumer.game_tasks[room_name] = asyncio.create_task( - MatchingGameConsumer.game_loop(room_name) - ) - - # Notify the users that they have been moved to a game room - await user1.channel_layer.group_add(room_name, user1.channel_name) - await user2.channel_layer.group_add(room_name, user2.channel_name) - - await user1.send( - text_data=json.dumps({"action": "start_game", "room_name": room_name}) - ) - await user2.send( - text_data=json.dumps({"action": "start_game", "room_name": room_name}) - ) - - print( - f"Users {user1.uid} and {user2.uid} moved to game room: {room_name}" - ) - async def disconnect(self, close_code): # Leave room group if self.authenticated: diff --git a/backend/transcendence/settings.py b/backend/transcendence/settings.py index dcbbd29..e9d719a 100644 --- a/backend/transcendence/settings.py +++ b/backend/transcendence/settings.py @@ -14,7 +14,7 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -setup_logging(level=logging.DEBUG) +# setup_logging(level=logging.DEBUG) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ From 21b5ea69d20abb5525e6708de8d03ffc3ec004f5 Mon Sep 17 00:00:00 2001 From: seonjo1 Date: Fri, 27 Sep 2024 15:58:35 +0900 Subject: [PATCH 12/25] =?UTF-8?q?feat:=20tournament=20game=20=EC=8B=9C?= =?UTF-8?q?=EC=9E=91=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Game-Core.js | 1 - .../src/components/Game-Tournament-Core.js | 399 ++++++++++++++++++ frontend/src/components/Game-Tournament.js | 29 ++ frontend/src/components/Tournament-Setting.js | 33 +- frontend/src/core/router.js | 6 +- frontend/style.css | 51 +++ 6 files changed, 516 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/Game-Tournament-Core.js create mode 100644 frontend/src/components/Game-Tournament.js diff --git a/frontend/src/components/Game-Core.js b/frontend/src/components/Game-Core.js index 5e0ede3..d10554e 100644 --- a/frontend/src/components/Game-Core.js +++ b/frontend/src/components/Game-Core.js @@ -1,5 +1,4 @@ import { Component } from "../core/Component.js"; -import { changeUrl } from "../core/router.js"; import { getCookie } from "../core/jwt.js"; export class GameCore extends Component { diff --git a/frontend/src/components/Game-Tournament-Core.js b/frontend/src/components/Game-Tournament-Core.js new file mode 100644 index 0000000..1c7d9f0 --- /dev/null +++ b/frontend/src/components/Game-Tournament-Core.js @@ -0,0 +1,399 @@ +import { Component } from "../core/Component.js"; +import { getCookie } from "../core/jwt.js"; + +export class GameTournamentCore extends Component { + constructor($el, props) { + super($el, props); + } + + initState() { + this.keysPressed = {}; + this.gameSocket = this.gameSocket = new WebSocket( + 'wss://' + + "localhost:443" + + '/ws/game/' + + this.props.uid + + '/' + ); + return {}; + } + + gameStart() { + const COLOR = ["#f5f6fa", "#2f3640", "#f5f6fa"]; + const BALL_COLOR = ["#FF4C4C", "#FFF078"]; + const canvas = document.getElementById('game-canvas'); + const scoreCanvas = document.getElementById('game-score'); + canvas.setAttribute('tabindex', '0'); + const ctx = canvas.getContext('2d'); + const scoreCtx = scoreCanvas.getContext('2d'); + const sounds = { + 'collision': new Audio('../../img/key.mp3'), + }; + let SCREEN_HEIGHT, SCREEN_WIDTH, BAR_HEIGHT, BAR_WIDTH, BAR_X_GAP, + BALL_RADIUS, LEFT_BAR_X, LEFT_BAR_Y, RIGHT_BAR_X, RIGHT_BAR_Y, + CELL_WIDTH, CELL_HEIGHT; + let counter = 0; + let leftBar, rightBar, leftBall, rightBall, map; + + console.log(canvas) + function playSound(soundName) { + var sound = sounds[soundName]; + if (sound) { + sound.currentTime = 0; + sound.play().catch(function (error) { + console.log('Autoplay was prevented:', error); + }); + } + } + + class Bar { + constructor(x, y, width, height, canvasHeight, id) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + this.targetX = x; + this.targetY = y; + this.canvasHeight = canvasHeight; + this.speed = 4; + this.id = id; + } + + draw() { + ctx.fillStyle = COLOR[this.id + 1]; + Map.strokeRoundedRect(ctx, this.x, this.y, this.width, this.height, this.width / 2); + ctx.fill(); + ctx.strokeStyle = COLOR[this.id]; + ctx.lineWidth = 2; + Map.strokeRoundedRect(ctx, this.x, this.y, this.width, this.height, this.width / 2); + ctx.stroke(); + } + + update() { + if (Math.abs(this.targetY - this.y) < this.speed) { + this.y = this.targetY; + } else { + if (this.targetY > this.y) { + this.y += this.speed; + } else { + this.y -= this.speed; + } + } + this.x = this.targetX; + } + } + + class Ball { + constructor(x, y, radius, color) { + this.x = x; + this.y = y; + this.targetX = x; + this.targetY = y; + this.radius = radius; + this.color = color; + } + + draw() { + Map.strokeRoundedRect(ctx, this.x - this.radius, this.y - this.radius, this.radius * 2, this.radius * 2, 6); + ctx.fillStyle = this.color; + ctx.fill(); + } + + update() { + this.x = this.targetX; + this.y = this.targetY; + } + } + + class Map { + constructor(map) { + this.map = map; + this.height = map.length; + this.width = map[0].length; + this.lintDash = [CELL_WIDTH / 5, CELL_WIDTH * 3 / 5]; + } + + update(mapDiff) { + mapDiff.forEach(diff => { + console.log(mapDiff) + this.map[diff[0]][diff[1]] = diff[2]; + new Particles(diff[1], diff[0], diff[2]); + playSound('collision'); + }); + } + + draw() { + for (let i = 0; i < this.height; i++) { + for (let j = 0; j < this.width; j++) { + ctx.fillStyle = COLOR[this.map[i][j]]; + Map.strokeRoundedRect( + ctx, + j * CELL_WIDTH + 2, + i * CELL_HEIGHT + 2, + CELL_WIDTH - 4, + CELL_HEIGHT - 4, + 5 + ); + } + } + } + + static strokeRoundedRect(ctx, x, y, width, height, radius) { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.arcTo(x + width, y, x + width, y + height, radius); + ctx.arcTo(x + width, y + height, x, y + height, radius); + ctx.arcTo(x, y + height, x, y, radius); + ctx.arcTo(x, y, x + width, y, radius); + ctx.fill(); + ctx.closePath(); + } + } + + class Particle { + constructor(x, y, r, vx, vy, color) { + this.x = x; + this.y = y; + this.r = r; + this.vx = vx; + this.vy = vy; + this.color = color; + this.opacity = 0.5; + this.g = 0.05; + this.friction = 0.99; + } + + draw() { + ctx.fillStyle = this.color + `${this.opacity})`; + ctx.fillRect(this.x, this.y, this.r, this.r); + } + + update() { + this.vx *= this.friction; + this.vy *= this.friction; + this.vy += this.g; + this.x += this.vx; + this.y += this.vy; + this.opacity -= 0.004; + this.draw(); + } + } + + class Particles { + static array = []; + + constructor(x, y, blockId) { + this.x = x * CELL_WIDTH + CELL_WIDTH / 2; + this.y = y * CELL_HEIGHT + CELL_HEIGHT / 2; + this.blockId = blockId; + this.color = Particles.hexToRgba(COLOR[blockId + 1]); + this.particles = []; + const particleCount = 50; + const power = 20; + const radians = (Math.PI * 2) / particleCount; + this.state = 1; + + for (let i = 0; i < particleCount; i++) { + this.particles.push( + new Particle( + this.x, + this.y, + 5, + Math.cos(radians * i) * (Math.random() * power), + Math.sin(radians * i) * (Math.random() * power), + this.color + ) + ); + } + Particles.array.push(this); + } + + draw() { + for (let i = this.particles.length - 1; i >= 0; i--) { + if (this.particles[i].opacity >= 0) { + this.particles[i].update(); + } else { + this.particles.splice(i, 1); + } + } + if (this.particles.length <= 0) { + this.state = -1; + } + } + + static hexToRgba(hex) { + let cleanedHex = hex.replace('#', ''); + if (cleanedHex.length === 3) + cleanedHex = cleanedHex.split('').map(hex => hex + hex).join(''); + const bigint = parseInt(cleanedHex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + + return `rgba(${r}, ${g}, ${b},`; + } + + static drawAll() { + for (let i = Particles.array.length - 1; i >= 0; i--) { + if (Particles.array[i].state == 1) + Particles.array[i].draw(); + else + Particles.array.splice(i, 1); + } + } + } + + this.gameSocket.onopen = () => { + const token = getCookie("jwt"); + this.gameSocket.send(JSON.stringify({ 'action': 'authenticate', 'token': token })); + }; + + this.gameSocket.onmessage = (e) => { + const data = JSON.parse(e.data); + if (data.type === 'initialize_game') { + initializeGame(data); + drawGame(); + } else if (data.type === 'update_game_state') { + leftBar.targetX = data.left_bar_x; + leftBar.targetY = data.left_bar_y; + rightBar.targetX = data.right_bar_x; + rightBar.targetY = data.right_bar_y; + leftBall.targetX = data.left_ball_x; + leftBall.targetY = data.left_ball_y; + rightBall.targetX = data.right_ball_x; + rightBall.targetY = data.right_ball_y; + map.update(data.map_diff); + } + }; + + let isPoolingLeft = false, isPoolingRight = false; + const handleKeyPresses = () => { + if (this.keysPressed['w']) { + this.gameSocket.send(JSON.stringify({ 'action': 'move_up', 'bar': 'left' })); + leftBar.targetY -= 5; + } + if (this.keysPressed['s']) { + this.gameSocket.send(JSON.stringify({ 'action': 'move_down', 'bar': 'left' })); + leftBar.targetY += 5; + } + if (this.keysPressed['a']) { + this.gameSocket.send(JSON.stringify({ 'action': 'pull', 'bar': 'left' })); + isPoolingLeft = true; + } else if (isPoolingLeft) { + this.gameSocket.send(JSON.stringify({ 'action': 'release', 'bar': 'left' })); + isPoolingLeft = false; + } + + if (this.keysPressed['ArrowUp']) { + this.gameSocket.send(JSON.stringify({ 'action': 'move_up', 'bar': 'right' })); + rightBar.targetY -= 5; + } + if (this.keysPressed['ArrowDown']) { + this.gameSocket.send(JSON.stringify({ 'action': 'move_down', 'bar': 'right' })); + rightBar.targetY += 5; + } + if (this.keysPressed['ArrowRight']) { + this.gameSocket.send(JSON.stringify({ 'action': 'pull', 'bar': 'right' })); + isPoolingRight = true; + } else if (isPoolingRight) { + this.gameSocket.send(JSON.stringify({ 'action': 'release', 'bar': 'right' })); + isPoolingRight = false; + } + } + + function drawGame() { + // ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.fillStyle = "#92969D"; + ctx.fillRect(0, 0, canvas.width, canvas.height); + map.draw(); + leftBar.draw(); + rightBar.draw(); + leftBall.draw(); + rightBall.draw(); + Particles.drawAll(); + } + + function initializeGame(data) { + SCREEN_HEIGHT = data.screen_height; + SCREEN_WIDTH = data.screen_width; + LEFT_BAR_X = data.left_bar_x; + LEFT_BAR_Y = data.left_bar_y; + RIGHT_BAR_X = data.right_bar_x; + RIGHT_BAR_Y = data.right_bar_y; + BAR_HEIGHT = data.bar_height; + BAR_WIDTH = data.bar_width; + BAR_X_GAP = data.bar_x_gap; + BALL_RADIUS = data.ball_radius; + canvas.width = SCREEN_WIDTH; + canvas.height = SCREEN_HEIGHT; + scoreCanvas.width = 500; + scoreCanvas.height = 150; + var text = "2 : 3" + // var blur = 10; + // var width = scoreCtx.measureText(text).width + blur * 2; + // scoreCtx.textBaseline = "top" + // scoreCtx.shadowColor = "#000" + // scoreCtx.shadowOffsetX = width; + // scoreCtx.shadowOffsetY = 0; + // scoreCtx.shadowBlur = blur; + scoreCtx.fillText(text, -10, 0); + CELL_WIDTH = canvas.width / data.map[0].length; + CELL_HEIGHT = canvas.height / data.map.length; + + map = new Map(data.map, COLOR[0], COLOR[1]); + leftBar = new Bar(LEFT_BAR_X, LEFT_BAR_Y, BAR_WIDTH, BAR_HEIGHT, SCREEN_HEIGHT, 0); + rightBar = new Bar(RIGHT_BAR_X, RIGHT_BAR_Y, BAR_WIDTH, BAR_HEIGHT, SCREEN_HEIGHT, 1); + leftBall = new Ball(data.left_ball_x, data.left_ball_y, BALL_RADIUS, BALL_COLOR[0]); + rightBall = new Ball(data.right_ball_x, data.right_ball_y, BALL_RADIUS, BALL_COLOR[1]); + + console.log(SCREEN_HEIGHT, SCREEN_WIDTH, BAR_HEIGHT, BAR_WIDTH, BALL_RADIUS); + setInterval(interpolate, 3); + } + + function interpolate() { + leftBar.update(); + rightBar.update(); + leftBall.update(); + rightBall.update(); + drawGame(); + + if (counter % 6 === 0) + handleKeyPresses(); + counter++; + } + } + + template() { + return ` + + + + `; + } + + setEvent() { + const handleKeyDown = (e) => { + this.keysPressed[e.key] = true; + } + const handleKeyUp = (e) => { + this.keysPressed[e.key] = false; + } + + const handleSocketClose = (e) => { + this.gameSocket.close(); + window.removeEventListener('popstate', handleSocketClose); + document.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('keyup', handleKeyUp); + } + + window.addEventListener('popstate', handleSocketClose); + document.addEventListener('keydown', handleKeyDown); + document.addEventListener('keyup', handleKeyUp); + } + + mounted() { + this.gameStart(); + } +} \ No newline at end of file diff --git a/frontend/src/components/Game-Tournament.js b/frontend/src/components/Game-Tournament.js new file mode 100644 index 0000000..6e535c2 --- /dev/null +++ b/frontend/src/components/Game-Tournament.js @@ -0,0 +1,29 @@ +import { GameDefault } from "./Game-Default.js"; +import { GameTournamentCore } from "./Game-Tournament-Core.js"; +import { changeUrl } from "../core/router.js"; + +export class GameTournament extends GameDefault { + mounted(){ + const nicknames = JSON.parse(localStorage.getItem("nickanems")); + if (!localStorage.getItem('game1')) { + this.props[game] = 1; + this.props[nick1] = nicknames[0]; + this.props[nick2] = nicknames[1]; + } else if (!localStorage.getItem('game2')) { + this.props[game] = 2; + this.props[nick1] = nicknames[2]; + this.props[nick2] = nicknames[3]; + } else if (!localStorage.getItem('game3')) { + this.props[game] = 3; + const game1 = JSON.parse(localStorage.getItem('game1')); + const game2 = JSON.parse(localStorage.getItem('game2')); + for (i = 0; i < 4; i++) { + if (game1.winner === nicknames[i]) this.props[nick1] = nicknames[i]; + else if (game2.winner === nicknames[i]) this.props[nick2] = nicknames[i]; + } + } else { + changeUrl("/main/tournament"); + } + new GameTournamentCore(document.querySelector("div#game"), this.props); + } +} \ No newline at end of file diff --git a/frontend/src/components/Tournament-Setting.js b/frontend/src/components/Tournament-Setting.js index 8a32fb4..16198c2 100644 --- a/frontend/src/components/Tournament-Setting.js +++ b/frontend/src/components/Tournament-Setting.js @@ -1,9 +1,15 @@ import { Component } from "../core/Component.js"; import { TournamentHistory } from "./Tournament-History.js"; +import { parseJWT } from "../core/jwt.js"; +import { changeUrl } from "../core/router.js"; export class TournamentSetting extends Component { template () { + const payload = parseJWT(); + if (!payload) this.uid = null; + else this.uid = payload.id; + // fetch(){ // tournament history 조회 //} @@ -34,6 +40,10 @@ export class TournamentSetting extends Component {
Take on the challenge
+
+
Please fill in all nickname fields
+
OK
+
@@ -69,9 +79,30 @@ export class TournamentSetting extends Component { this.addEvent('click', '#goBack', (event) => { window.history.back(); }); + + this.addEvent('click', '#tournament-nick-error-button', (event) => { + document.querySelector('#tournament-nick-error').style.display = 'none'; + }); this.addEvent('click', '#tournament-start-button', (event) => { - console.log("you press start button!!"); + const nick1 = document.querySelector('#tournament-nick1').value; + const nick2 = document.querySelector('#tournament-nick2').value; + const nick3 = document.querySelector('#tournament-nick3').value; + const nick4 = document.querySelector('#tournament-nick4').value; + + if (!nick1 || !nick2 || !nick3 || !nick4) { + document.querySelector('#tournament-nick-error').style.display = 'flex'; + return; + } + + localStorage.removeItem('game1'); + localStorage.removeItem('game2'); + localStorage.removeItem('game3'); + + const nicknames = {nick1, nick2, nick3, nick4}; + localStorage.setItem('nicknames', JSON.stringify(nicknames)); + + changeUrl(`/game/tournament/${this.uid}`); }); this.addEvent('click', '#tournament-game-menu', (event) => { diff --git a/frontend/src/core/router.js b/frontend/src/core/router.js index 4e23597..ebcc4c4 100644 --- a/frontend/src/core/router.js +++ b/frontend/src/core/router.js @@ -9,6 +9,7 @@ import { Error404 } from "../components/Error404.js"; import { Match } from "../components/Match.js"; import { Tournament } from "../components/Tournament.js"; import { GameLocal } from "../components/Game-Local.js"; +import { GameTournament } from "../components/Game-Tournament.js"; import { Error } from "../components/Error.js"; export const createRoutes = (root) => { @@ -42,7 +43,9 @@ export const createRoutes = (root) => { }, "/game/local/:uid": { component: (props) => new GameLocal(root.app, props), - props: { uid: "" } + }, + "/game/tournament/:uid": { + component: (props) => new GameTournament(root.app, props), }, "/error": { component: (props) => new Error(root.app, props) @@ -147,6 +150,7 @@ export async function parsePath(path) { keys.forEach((key, index) => { props[key.substring(1)] = values[index]; }); + console.log(props); route.component(props); return; } diff --git a/frontend/style.css b/frontend/style.css index c00e148..dfc46b6 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -1888,6 +1888,57 @@ div#tournament-crown-box { margin-bottom: 30px; } +div#tournament-nick-error { + position: absolute; + width: 400px; /* 더 넓게 조정 */ + height: auto; /* 자동 높이로 조정 */ + left: 50%; + top: 50%; + transform: translate(-50%, -50%); /* 중앙 정렬을 위한 transform 사용 */ + display: none; + flex-direction: column; + justify-content: center; + align-items: center; + background-color: rgba(0, 0, 0, 0.85); /* 더 어두운 배경색으로 변경 */ + border-radius: 12px; /* 모서리 둥글기 약간 추가 */ + padding: 15px; /* 여백을 적절히 조정 */ + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); /* 약간의 그림자 추가 */ +} + +div#tournament-nick-error-msg { + width: 100%; /* 가로 크기 100% */ + padding: 10px 0; /* 위아래 여백 추가 */ + font-size: 24px; /* 텍스트 크기를 조금 더 키움 */ + color: #f0f0f0; /* 약간 밝은 흰색 텍스트 */ + text-align: center; +} + +#tournament-nick-error-button { + width: 120px; /* 버튼을 약간 더 넓게 */ + height: 40px; /* 높이를 줄여서 비율 맞춤 */ + font-size: 20px; + margin-top: 10px; /* 메시지와의 간격 추가 */ + border: none; + border-radius: 8px; /* 모서리 둥글기 약간 추가 */ + background-color: #6886bd; + color: white; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + transition: background-color 0.3s ease, transform 0.2s ease; /* hover 시 효과 추가 */ +} + +#tournament-nick-error-button:hover { + background-color: #4f6ca8; /* 버튼 hover 시 색상 변화 */ + transform: scale(1.05); /* hover 시 살짝 확대 효과 */ +} + +#tournament-nick-error-button:hover { + background-color: #4f6ca8; + transition: background-color 0.3s ease; +} + img#crown { width: 80px; height: 80px; From 591d56a5e35a323f5ffc992f0879698b64015ed7 Mon Sep 17 00:00:00 2001 From: seonjo1 Date: Fri, 27 Sep 2024 16:09:46 +0900 Subject: [PATCH 13/25] =?UTF-8?q?feat:=20game=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20nick1,=20nick2,=20game=20=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Game-Tournament.js | 26 ++++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/Game-Tournament.js b/frontend/src/components/Game-Tournament.js index 6e535c2..98d0be5 100644 --- a/frontend/src/components/Game-Tournament.js +++ b/frontend/src/components/Game-Tournament.js @@ -4,26 +4,28 @@ import { changeUrl } from "../core/router.js"; export class GameTournament extends GameDefault { mounted(){ - const nicknames = JSON.parse(localStorage.getItem("nickanems")); + const nicknames = JSON.parse(localStorage.getItem("nicknames")); + console.log(nicknames[0]); if (!localStorage.getItem('game1')) { - this.props[game] = 1; - this.props[nick1] = nicknames[0]; - this.props[nick2] = nicknames[1]; + this.props["game"] = 1; + this.props["nick1"] = nicknames["nick1"]; + this.props["nick2"] = nicknames["nick2"]; } else if (!localStorage.getItem('game2')) { - this.props[game] = 2; - this.props[nick1] = nicknames[2]; - this.props[nick2] = nicknames[3]; + this.props["game"] = 2; + this.props["nick1"] = nicknames["nick3"]; + this.props["nick2"] = nicknames["nick4"]; } else if (!localStorage.getItem('game3')) { - this.props[game] = 3; + this.props["game"] = 3; const game1 = JSON.parse(localStorage.getItem('game1')); const game2 = JSON.parse(localStorage.getItem('game2')); - for (i = 0; i < 4; i++) { - if (game1.winner === nicknames[i]) this.props[nick1] = nicknames[i]; - else if (game2.winner === nicknames[i]) this.props[nick2] = nicknames[i]; - } + nicknames.forEach((nickname) => { + if (game1.winner === nickname) this.props["nick1"] = nickname; + if (game2.winner === nickname) this.props["nick2"] = nickname; + }); } else { changeUrl("/main/tournament"); } + console.log(this.props); new GameTournamentCore(document.querySelector("div#game"), this.props); } } \ No newline at end of file From fb5a86cbd88613f8e9f3022bceeed675a20cdbfc Mon Sep 17 00:00:00 2001 From: seonjo1 Date: Sat, 28 Sep 2024 19:56:55 +0900 Subject: [PATCH 14/25] =?UTF-8?q?feat:=20tournament=20=EA=B2=8C=EC=9E=84?= =?UTF-8?q?=20=EC=8B=A4=ED=96=89=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Game-Result-Page.js | 75 ++++++++++ frontend/src/components/Game-Result.js | 8 ++ .../src/components/Game-Tournament-Core.js | 133 +++++++++++++++--- frontend/src/components/Game-Tournament.js | 23 +-- frontend/src/core/router.js | 9 ++ frontend/style.css | 57 ++++++++ 6 files changed, 274 insertions(+), 31 deletions(-) create mode 100644 frontend/src/components/Game-Result-Page.js create mode 100644 frontend/src/components/Game-Result.js diff --git a/frontend/src/components/Game-Result-Page.js b/frontend/src/components/Game-Result-Page.js new file mode 100644 index 0000000..5d76be5 --- /dev/null +++ b/frontend/src/components/Game-Result-Page.js @@ -0,0 +1,75 @@ +import { Component } from "../core/Component.js"; +import { changeUrl } from "../core/router.js"; + +export class GameResultPage extends Component { + + template () { + return ` +
+
+
Winner is ${this.props.winner}!!!
+
OK
+
+
+ `; + } + + setEvent() { + + function checkNick(nicknames, nick) { + if (nicknames[nick1] !== nick && + nicknames[nick2] !== nick && + nicknames[nick3] !== nick && + nicknames[nick4] !== nick) { + return false; + } + return true; + } + + this.addEvent('click', '#game-result-button', (event) => { + if (this.props.isTournament){ + const game1 = localStorage.getItem('game1'); + const game2 = localStorage.getItem('game2'); + const game3 = localStorage.getItem('game3'); + if (game1 && !game2 && !game3) { + console.log("go game2!!"); + changeUrl(`/game/tournament/${this.props.uid}`); + } else if (game1 && game2 && !game3) { + console.log("go game3!!"); + changeUrl(`/game/tournament/${this.props.uid}`); + } else if (game1 && game2 && !game3) { + const nicknames = localStorage.getItem(`nicknames`); + if (!nicknames) changeUrl("/main/tournament"); + const parsedNicknames = JSON.parse(nicknames); + const parsedGame1 = JSON.parse(game1); + const parsedGame2 = JSON.parse(game2); + const parsedGame3 = JSON.parse(game3); + + if (!checkNick(nicknames, parsedGame1.winner) || + !checkNick(nicknames, parsedGame1.loser) || + !checkNick(nicknames, parsedGame2.winner) || + !checkNick(nicknames, parsedGame2.loser) || + !checkNick(nicknames, parsedGame3.winner) || + !checkNick(nicknames, parsedGame3.loser) ) { + changeUrl("/main/tournament"); + } + if (!(parsedGame1.winner === parsedGame3.winner && parsedGame2.winner === parsedGame3.loser) && + !(parsedGame1.winner === parsedGame3.winner && parsedGame2.winner === parsedGame3.loser)) { + changeUrl("/main/tournament"); + } + + console.log("game set!!"); + console.log(parsedGame1); + console.log(parsedGame2); + console.log(parsedGame3); + changeUrl("/main/tournament"); + + } else { + changeUrl("/main/tournament"); + } + }else { + changeUrl("/main", false); + } + }); + } +} diff --git a/frontend/src/components/Game-Result.js b/frontend/src/components/Game-Result.js new file mode 100644 index 0000000..9173aaf --- /dev/null +++ b/frontend/src/components/Game-Result.js @@ -0,0 +1,8 @@ +import { GameDefault } from "./Game-Default.js"; +import { GameResultPage } from "./Game-Result-Page.js"; + +export class GameResult extends GameDefault { + mounted(){ + new GameResultPage(document.querySelector("div#game"), this.props); + } +} \ No newline at end of file diff --git a/frontend/src/components/Game-Tournament-Core.js b/frontend/src/components/Game-Tournament-Core.js index 1c7d9f0..cf773ae 100644 --- a/frontend/src/components/Game-Tournament-Core.js +++ b/frontend/src/components/Game-Tournament-Core.js @@ -1,5 +1,7 @@ import { Component } from "../core/Component.js"; import { getCookie } from "../core/jwt.js"; +import { socketList } from "../app.js" +import { changeUrl } from "../core/router.js"; export class GameTournamentCore extends Component { constructor($el, props) { @@ -8,13 +10,14 @@ export class GameTournamentCore extends Component { initState() { this.keysPressed = {}; - this.gameSocket = this.gameSocket = new WebSocket( + this.gameSocket = new WebSocket( 'wss://' + "localhost:443" + '/ws/game/' - + this.props.uid + + this.props.uid + '/' ); + socketList.push(this.gameSocket); return {}; } @@ -30,11 +33,11 @@ export class GameTournamentCore extends Component { 'collision': new Audio('../../img/key.mp3'), }; let SCREEN_HEIGHT, SCREEN_WIDTH, BAR_HEIGHT, BAR_WIDTH, BAR_X_GAP, - BALL_RADIUS, LEFT_BAR_X, LEFT_BAR_Y, RIGHT_BAR_X, RIGHT_BAR_Y, - CELL_WIDTH, CELL_HEIGHT; + BALL_RADIUS, LEFT_BAR_X, LEFT_BAR_Y, RIGHT_BAR_X, RIGHT_BAR_Y, + CELL_WIDTH, CELL_HEIGHT, PLAYER, MAX_SCORE; let counter = 0; - let leftBar, rightBar, leftBall, rightBall, map; - + let leftBar, rightBar, leftBall, rightBall, map, score, penaltyTime = [0, 0]; + console.log(canvas) function playSound(soundName) { var sound = sounds[soundName]; @@ -67,6 +70,13 @@ export class GameTournamentCore extends Component { ctx.lineWidth = 2; Map.strokeRoundedRect(ctx, this.x, this.y, this.width, this.height, this.width / 2); ctx.stroke(); + if (penaltyTime[this.id] !== 0) { + ctx.font = "bold 30px Arial"; + ctx.textAlign = "center"; + ctx.textBaseline = "middle"; + ctx.fillStyle = BALL_COLOR[this.id]; + ctx.fillText(penaltyTime[this.id], this.x + this.width / 2 + (this.id == 0 ? 1 : -1) * 175, this.y + this.height / 2); + } } update() { @@ -91,17 +101,37 @@ export class GameTournamentCore extends Component { this.targetY = y; this.radius = radius; this.color = color; + this.trail = []; // 잔상을 저장할 배열 } draw() { + this.trail.forEach((pos, index) => { + const alpha = (index + 1) / (this.trail.length + 15); + const scale = (index / (this.trail.length + 1)); + const radius = this.radius * scale; + + ctx.globalAlpha = alpha; // 투명도 설정 + ctx.fillStyle = this.color; + ctx.fillRect(pos.x - radius, pos.y - radius, radius * 2, radius * 2); + }); + + ctx.globalAlpha = 1; Map.strokeRoundedRect(ctx, this.x - this.radius, this.y - this.radius, this.radius * 2, this.radius * 2, 6); ctx.fillStyle = this.color; ctx.fill(); } - update() { this.x = this.targetX; this.y = this.targetY; + // 10 프레임마다 잔상 저장 + if (counter % 3 === 0) { + this.trail.push({ x: this.x, y: this.y }); + + if (this.trail.length > 10) { + this.trail.shift(); // 오래된 잔상을 제거 + } + } + } } @@ -115,7 +145,6 @@ export class GameTournamentCore extends Component { update(mapDiff) { mapDiff.forEach(diff => { - console.log(mapDiff) this.map[diff[0]][diff[1]] = diff[2]; new Particles(diff[1], diff[0], diff[2]); playSound('collision'); @@ -243,6 +272,45 @@ export class GameTournamentCore extends Component { } } + class Score { + constructor() { + this.score = [0, 0]; + } + + update(score) { + this.score = score; + } + + draw() { + scoreCtx.clearRect(0, 0, scoreCanvas.width, scoreCanvas.height); + scoreCtx.textAlign = "center"; + scoreCtx.font = "25px Arial"; + scoreCtx.fillStyle = COLOR[1]; + scoreCtx.fillText(PLAYER[0], 200, 50); + scoreCtx.fillStyle = COLOR[0]; + scoreCtx.fillText(PLAYER[1], scoreCanvas.width - 200, 50); + + scoreCtx.textAlign = "left"; + scoreCtx.font = "100px Arial"; + scoreCtx.fillStyle = COLOR[1]; + let firstScore = this.score[0].toString(); + scoreCtx.fillText(firstScore, 125, 140); + + let firstScoreWidth = scoreCtx.measureText(firstScore).width; + scoreCtx.font = "50px Arial"; + let scoreText = " / " + MAX_SCORE; + scoreCtx.fillText(scoreText, 125 + firstScoreWidth, 140); + + scoreCtx.textAlign = "right"; + scoreCtx.fillStyle = COLOR[0]; + scoreCtx.fillText(scoreText, scoreCanvas.width - 125, 140); + let secondScore = this.score[1].toString(); + let secondScoreX = scoreCanvas.width - 125 - scoreCtx.measureText(scoreText).width; + scoreCtx.font = "100px Arial"; + scoreCtx.fillText(secondScore, secondScoreX, 140); + } + } + this.gameSocket.onopen = () => { const token = getCookie("jwt"); this.gameSocket.send(JSON.stringify({ 'action': 'authenticate', 'token': token })); @@ -251,7 +319,7 @@ export class GameTournamentCore extends Component { this.gameSocket.onmessage = (e) => { const data = JSON.parse(e.data); if (data.type === 'initialize_game') { - initializeGame(data); + initializeGame(data, this.props.player1, this.props.player2); drawGame(); } else if (data.type === 'update_game_state') { leftBar.targetX = data.left_bar_x; @@ -262,12 +330,40 @@ export class GameTournamentCore extends Component { leftBall.targetY = data.left_ball_y; rightBall.targetX = data.right_ball_x; rightBall.targetY = data.right_ball_y; + penaltyTime = data.penalty_time; + score.update(data.score); map.update(data.map_diff); + } else if (data.type === 'game_result') { + this.gameSocket.close(); + socketList.pop(); + console.log("winner!!"); + console.log(data.winner); + const winner = data.winner === 0 ? this.props.player1 : this.props.player2; + const loser = data.winner === 0 ? this.props.player2 : this.props.player1; + if (this.props.game === 1) { + const game1 = { winner: winner, loser: loser, + score1: data.score[0], score2: data.score[1]}; + localStorage.setItem('game1', JSON.stringify(game1)); + changeUrl(`/game/tournament/${this.props.uid}/result/${winner}`); + } else if (this.props.game === 2) { + const game2 = { winner: winner, loser: loser, + score1: data.score[0], score2: data.score[1]}; + localStorage.setItem('game2', JSON.stringify(game2)); + changeUrl(`/game/tournament/${this.props.uid}/result/${winner}`); + } else if (this.props.game === 3) { + const game3 = { winner: winner, loser: loser, + score1: data.score[0], score2: data.score[1]}; + localStorage.setItem('game3', JSON.stringify(game3)); + changeUrl(`/game/tournament/${this.props.uid}/result/${winner}`); + } else { + changeUrl('/main/tournament', false); + } } }; let isPoolingLeft = false, isPoolingRight = false; const handleKeyPresses = () => { + if (this.gameSocket.readyState !== WebSocket.OPEN) return; if (this.keysPressed['w']) { this.gameSocket.send(JSON.stringify({ 'action': 'move_up', 'bar': 'left' })); leftBar.targetY -= 5; @@ -311,9 +407,10 @@ export class GameTournamentCore extends Component { leftBall.draw(); rightBall.draw(); Particles.drawAll(); + score.draw(); } - function initializeGame(data) { + function initializeGame(data, player1, player2) { SCREEN_HEIGHT = data.screen_height; SCREEN_WIDTH = data.screen_width; LEFT_BAR_X = data.left_bar_x; @@ -324,19 +421,12 @@ export class GameTournamentCore extends Component { BAR_WIDTH = data.bar_width; BAR_X_GAP = data.bar_x_gap; BALL_RADIUS = data.ball_radius; + PLAYER = [ player1, player2 ]; + MAX_SCORE = data.max_score; canvas.width = SCREEN_WIDTH; canvas.height = SCREEN_HEIGHT; - scoreCanvas.width = 500; - scoreCanvas.height = 150; - var text = "2 : 3" - // var blur = 10; - // var width = scoreCtx.measureText(text).width + blur * 2; - // scoreCtx.textBaseline = "top" - // scoreCtx.shadowColor = "#000" - // scoreCtx.shadowOffsetX = width; - // scoreCtx.shadowOffsetY = 0; - // scoreCtx.shadowBlur = blur; - scoreCtx.fillText(text, -10, 0); + scoreCanvas.width = 1000; + scoreCanvas.height = 175; CELL_WIDTH = canvas.width / data.map[0].length; CELL_HEIGHT = canvas.height / data.map.length; @@ -345,6 +435,7 @@ export class GameTournamentCore extends Component { rightBar = new Bar(RIGHT_BAR_X, RIGHT_BAR_Y, BAR_WIDTH, BAR_HEIGHT, SCREEN_HEIGHT, 1); leftBall = new Ball(data.left_ball_x, data.left_ball_y, BALL_RADIUS, BALL_COLOR[0]); rightBall = new Ball(data.right_ball_x, data.right_ball_y, BALL_RADIUS, BALL_COLOR[1]); + score = new Score(); console.log(SCREEN_HEIGHT, SCREEN_WIDTH, BAR_HEIGHT, BAR_WIDTH, BALL_RADIUS); setInterval(interpolate, 3); diff --git a/frontend/src/components/Game-Tournament.js b/frontend/src/components/Game-Tournament.js index 98d0be5..84e179e 100644 --- a/frontend/src/components/Game-Tournament.js +++ b/frontend/src/components/Game-Tournament.js @@ -6,21 +6,24 @@ export class GameTournament extends GameDefault { mounted(){ const nicknames = JSON.parse(localStorage.getItem("nicknames")); console.log(nicknames[0]); - if (!localStorage.getItem('game1')) { + const game1 = localStorage.getItem('game1'); + const game2 = localStorage.getItem('game2'); + const game3 = localStorage.getItem('game3'); + if (!game1 && !game2 && !game3) { this.props["game"] = 1; - this.props["nick1"] = nicknames["nick1"]; - this.props["nick2"] = nicknames["nick2"]; - } else if (!localStorage.getItem('game2')) { + this.props["player1"] = nicknames["nick1"]; + this.props["player2"] = nicknames["nick2"]; + } else if (game1 && !game2 && !game3) { this.props["game"] = 2; - this.props["nick1"] = nicknames["nick3"]; - this.props["nick2"] = nicknames["nick4"]; - } else if (!localStorage.getItem('game3')) { + this.props["player1"] = nicknames["nick3"]; + this.props["player2"] = nicknames["nick4"]; + } else if (game1 && game2 && !game3) { this.props["game"] = 3; const game1 = JSON.parse(localStorage.getItem('game1')); const game2 = JSON.parse(localStorage.getItem('game2')); - nicknames.forEach((nickname) => { - if (game1.winner === nickname) this.props["nick1"] = nickname; - if (game2.winner === nickname) this.props["nick2"] = nickname; + Object.values(nicknames).forEach((nickname) => { + if (game1.winner === nickname) this.props["player1"] = nickname; + if (game2.winner === nickname) this.props["player2"] = nickname; }); } else { changeUrl("/main/tournament"); diff --git a/frontend/src/core/router.js b/frontend/src/core/router.js index 4ac83fe..2d7f39a 100644 --- a/frontend/src/core/router.js +++ b/frontend/src/core/router.js @@ -12,6 +12,7 @@ import { GameLocal } from "../components/Game-Local.js"; import { GameTournament } from "../components/Game-Tournament.js"; import { GameMatching } from "../components/Game-matching.js"; import { Error } from "../components/Error.js"; +import { GameResult } from "../components/Game-Result.js"; export const createRoutes = (root) => { return { @@ -48,6 +49,14 @@ export const createRoutes = (root) => { "/game/tournament/:uid": { component: (props) => new GameTournament(root.app, props), }, + "/game/tournament/:uid/result/:winner": { + component: (props) => { props["isTournament"] = true; + return new GameResult(root.app, props);} + }, + "/game/:uid/result/:winner": { + component: (props) => { props["isTournament"] = false; + return new GameResult(root.app, props);} + }, "/game/vs/:room": { component: (props) => new GameMatching(root.app, props), props: { room: "" } diff --git a/frontend/style.css b/frontend/style.css index dfc46b6..7b35ead 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -2175,3 +2175,60 @@ div#tournament-right-score{ .tournament-lose-score{ color:black; } + +div#game-result-box { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +div#game-winner-box { + min-width: 500px; + min-height: 300px; + width: 50%; /* 가로 크기 100% */ + height: 400px; + border-radius: 20px; + background-color: #1e3c72; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-bottom: 50px; /* 위아래 여백 추가 */ +} + +div#game-winner { + width: 100%; /* 가로 크기 100% */ + height: 200px; + font-size: 60px; /* 텍스트 크기를 조금 더 키움 */ + color: #ffffff; /* 약간 밝은 흰색 텍스트 */ + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + margin-bottom: 20px; +} + +div#game-result-button { + width: 20%; /* 버튼을 약간 더 넓게 */ + height: 40px; /* 높이를 줄여서 비율 맞춤 */ + font-size: 20px; + margin-top: 10px; /* 메시지와의 간격 추가 */ + border: none; + border-radius: 8px; /* 모서리 둥글기 약간 추가 */ + background-color: #6886bd; + color: white; + cursor: pointer; + display: flex; + justify-content: center; + align-items: center; + transition: background-color 0.3s ease, transform 0.2s ease; /* hover 시 효과 추가 */ +} + +div#game-result-button:hover { + background-color: #4f6ca8; /* 버튼 hover 시 색상 변화 */ + transform: scale(1.05); /* hover 시 살짝 확대 효과 */ +} \ No newline at end of file From 1bc00dbc19684cbda244c0c0b604cce4897c2e64 Mon Sep 17 00:00:00 2001 From: seonjo1 Date: Sat, 28 Sep 2024 20:16:01 +0900 Subject: [PATCH 15/25] =?UTF-8?q?fix:=20=ED=86=A0=EB=84=88=EB=A8=BC?= =?UTF-8?q?=ED=8A=B8=20=EB=A7=88=EC=A7=80=EB=A7=89=20=EB=9D=BC=EC=9A=B4?= =?UTF-8?q?=EB=93=9C=20=EC=98=A4=EB=A5=98=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/game/consumers.py | 2 +- frontend/src/components/Game-Result-Page.js | 29 ++++++++------------- frontend/src/components/Game-Tournament.js | 1 - 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/backend/game/consumers.py b/backend/game/consumers.py index bbbf247..d962626 100644 --- a/backend/game/consumers.py +++ b/backend/game/consumers.py @@ -10,7 +10,7 @@ SCREEN_HEIGHT = 750 SCREEN_WIDTH = 1250 -MAX_SCORE = 150 +MAX_SCORE = 10 class GameState: diff --git a/frontend/src/components/Game-Result-Page.js b/frontend/src/components/Game-Result-Page.js index 5d76be5..df54fc2 100644 --- a/frontend/src/components/Game-Result-Page.js +++ b/frontend/src/components/Game-Result-Page.js @@ -17,10 +17,10 @@ export class GameResultPage extends Component { setEvent() { function checkNick(nicknames, nick) { - if (nicknames[nick1] !== nick && - nicknames[nick2] !== nick && - nicknames[nick3] !== nick && - nicknames[nick4] !== nick) { + if (nicknames["nick1"] !== nick && + nicknames["nick2"] !== nick && + nicknames["nick3"] !== nick && + nicknames["nick4"] !== nick) { return false; } return true; @@ -32,12 +32,10 @@ export class GameResultPage extends Component { const game2 = localStorage.getItem('game2'); const game3 = localStorage.getItem('game3'); if (game1 && !game2 && !game3) { - console.log("go game2!!"); changeUrl(`/game/tournament/${this.props.uid}`); } else if (game1 && game2 && !game3) { - console.log("go game3!!"); changeUrl(`/game/tournament/${this.props.uid}`); - } else if (game1 && game2 && !game3) { + } else if (game1 && game2 && game3) { const nicknames = localStorage.getItem(`nicknames`); if (!nicknames) changeUrl("/main/tournament"); const parsedNicknames = JSON.parse(nicknames); @@ -45,23 +43,18 @@ export class GameResultPage extends Component { const parsedGame2 = JSON.parse(game2); const parsedGame3 = JSON.parse(game3); - if (!checkNick(nicknames, parsedGame1.winner) || - !checkNick(nicknames, parsedGame1.loser) || - !checkNick(nicknames, parsedGame2.winner) || - !checkNick(nicknames, parsedGame2.loser) || - !checkNick(nicknames, parsedGame3.winner) || - !checkNick(nicknames, parsedGame3.loser) ) { + if (!checkNick(parsedNicknames, parsedGame1.winner) || + !checkNick(parsedNicknames, parsedGame1.loser) || + !checkNick(parsedNicknames, parsedGame2.winner) || + !checkNick(parsedNicknames, parsedGame2.loser) || + !checkNick(parsedNicknames, parsedGame3.winner) || + !checkNick(parsedNicknames, parsedGame3.loser) ) { changeUrl("/main/tournament"); } if (!(parsedGame1.winner === parsedGame3.winner && parsedGame2.winner === parsedGame3.loser) && !(parsedGame1.winner === parsedGame3.winner && parsedGame2.winner === parsedGame3.loser)) { changeUrl("/main/tournament"); } - - console.log("game set!!"); - console.log(parsedGame1); - console.log(parsedGame2); - console.log(parsedGame3); changeUrl("/main/tournament"); } else { diff --git a/frontend/src/components/Game-Tournament.js b/frontend/src/components/Game-Tournament.js index 84e179e..878943b 100644 --- a/frontend/src/components/Game-Tournament.js +++ b/frontend/src/components/Game-Tournament.js @@ -5,7 +5,6 @@ import { changeUrl } from "../core/router.js"; export class GameTournament extends GameDefault { mounted(){ const nicknames = JSON.parse(localStorage.getItem("nicknames")); - console.log(nicknames[0]); const game1 = localStorage.getItem('game1'); const game2 = localStorage.getItem('game2'); const game3 = localStorage.getItem('game3'); From a6b917bfcc7718492d9d77e2ce76463405183af0 Mon Sep 17 00:00:00 2001 From: seonjo1 Date: Wed, 25 Dec 2024 18:07:42 +0900 Subject: [PATCH 16/25] =?UTF-8?q?fix:=20=EB=92=A4=EB=A1=9C=EA=B0=80?= =?UTF-8?q?=EA=B8=B0=20=EB=88=84=EB=A5=BC=EC=8B=9C=20=ED=99=88=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Friends-List.js | 2 +- frontend/src/components/Match-Wait.js | 2 +- frontend/src/components/Profile-Info.js | 2 +- frontend/src/components/Tournament-Setting.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/Friends-List.js b/frontend/src/components/Friends-List.js index 409c7a6..8c0c5cb 100644 --- a/frontend/src/components/Friends-List.js +++ b/frontend/src/components/Friends-List.js @@ -118,7 +118,7 @@ export class FriendsList extends Component { }); this.addEvent('click', '#goBack', (event) => { - window.history.back(); + changeUrl("/main"); }); this.addEvent('click', '.goProfile', (event) => { diff --git a/frontend/src/components/Match-Wait.js b/frontend/src/components/Match-Wait.js index 0ace02b..42cf17a 100644 --- a/frontend/src/components/Match-Wait.js +++ b/frontend/src/components/Match-Wait.js @@ -51,7 +51,7 @@ export class WaitForMatch extends Component { setEvent() { this.addEvent('click', '#goBack', (event) => { - window.history.back(); + changeUrl("/main", false); }); const handleSocketClose = (e) => { diff --git a/frontend/src/components/Profile-Info.js b/frontend/src/components/Profile-Info.js index 222c0fe..477f0bf 100644 --- a/frontend/src/components/Profile-Info.js +++ b/frontend/src/components/Profile-Info.js @@ -127,7 +127,7 @@ x } else { setEvent() { this.addEvent('click', '#goBack', () => { - window.history.back(); + changeUrl("/main"); }); this.addEvent('click', '.opNick', (event) => { diff --git a/frontend/src/components/Tournament-Setting.js b/frontend/src/components/Tournament-Setting.js index 16198c2..dbfa86c 100644 --- a/frontend/src/components/Tournament-Setting.js +++ b/frontend/src/components/Tournament-Setting.js @@ -77,7 +77,7 @@ export class TournamentSetting extends Component { setEvent() { this.addEvent('click', '#goBack', (event) => { - window.history.back(); + changeUrl("/main"); }); this.addEvent('click', '#tournament-nick-error-button', (event) => { From f86cf3f4b7b0e1094c24b1125c9d374bbbc4a7ea Mon Sep 17 00:00:00 2001 From: seonjo1 Date: Wed, 25 Dec 2024 22:09:43 +0900 Subject: [PATCH 17/25] fix: fix tournament api --- frontend/src/components/Game-Result-Page.js | 32 +++++++++++++++-- frontend/src/components/Profile-Info.js | 2 +- frontend/src/components/Tournament-Setting.js | 36 +++++++++++++------ 3 files changed, 55 insertions(+), 15 deletions(-) diff --git a/frontend/src/components/Game-Result-Page.js b/frontend/src/components/Game-Result-Page.js index df54fc2..0e0d712 100644 --- a/frontend/src/components/Game-Result-Page.js +++ b/frontend/src/components/Game-Result-Page.js @@ -55,12 +55,38 @@ export class GameResultPage extends Component { !(parsedGame1.winner === parsedGame3.winner && parsedGame2.winner === parsedGame3.loser)) { changeUrl("/main/tournament"); } - changeUrl("/main/tournament"); - + // fetch + const payload = { + game_info: JSON.stringify({ + game1: parsedGame1, + game2: parsedGame2, + game3: parsedGame3, + }) + }; + console.log("payload: "); + console.log(JSON.stringify(payload,null,2)); + fetch("https://localhost:443/api/game-history/tournament", { + method: 'POST', + credentials: 'include', // 쿠키를 포함하여 요청 + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(payload) + }) + .then(response => { + if (!response.ok) { + throw new Error('Failed to send tournament results'); + } + changeUrl("/main/tournament"); + }) + .catch(error => { + console.error("Error sending tournament results:", error); + changeUrl("/"); + }); } else { changeUrl("/main/tournament"); } - }else { + } else { changeUrl("/main", false); } }); diff --git a/frontend/src/components/Profile-Info.js b/frontend/src/components/Profile-Info.js index 477f0bf..c7e077e 100644 --- a/frontend/src/components/Profile-Info.js +++ b/frontend/src/components/Profile-Info.js @@ -42,7 +42,7 @@ export class ProfileInfo extends Component { this.rate = null; this.games = null; - fetch(`https://localhost:443/api/user/${this.props.uid}`, { + fetch(`https://localhost:443/api/user/${this.props.uid}`, { method: 'GET', credentials: 'include', // 쿠키를 포함하여 요청 (사용자 인증 필요 시) }) diff --git a/frontend/src/components/Tournament-Setting.js b/frontend/src/components/Tournament-Setting.js index dbfa86c..1e23498 100644 --- a/frontend/src/components/Tournament-Setting.js +++ b/frontend/src/components/Tournament-Setting.js @@ -10,18 +10,32 @@ export class TournamentSetting extends Component { if (!payload) this.uid = null; else this.uid = payload.id; - // fetch(){ - // tournament history 조회 - //} + fetch("https://localhost:443/api/game-history/tournament", { + method: "GET", + credentials: "include", // 쿠키 포함 + }) + .then(response => { + if (!response.ok) { + throw new Error(`Failed to fetch tournament history: ${response.status}`); + } + return response.json(); + }) + .then(data => { + this.games = data.games; // 서버에서 받아온 데이터 구조에 맞게 설정 + }) + .catch(error => { + console.error("Error fetching tournament history:", error); + this.games = {}; // 오류 발생 시 기본값 설정 + }); - this.games = { - "09/02": '{ "game1": {"Seonjo": 2, "Michang": 1}, "game2": {"Jiko": 3, "Jaehejun": 2}, "game3": {"Seonjo": 2, "Jiko": 3} }', - "10/02": '{ "game1": {"Jaehejun": 1, "Seonjo": 2}, "game2": {"Michang": 2, "Seunan": 1}, "game3": {"Seonjo": 2, "Michang": 3} }', - "10/12": '{ "game1": {"Michang": 2, "Jiko": 1}, "game2": {"Seonjo": 3, "Seunan": 2}, "game3": {"Michang": 1, "Seonjo": 3} }', - "10/25": '{ "game1": {"Jaehejun": 2, "Seunan": 3}, "game2": {"Michang": 3, "Seonjo": 2}, "game3": {"Seunan": 1, "Michang": 2} }', - "11/12": '{ "game1": {"Jiko": 2, "Jaehejun": 1}, "game2": {"Seunan": 3, "Michang": 2}, "game3": {"Jiko": 2, "Seunan": 3} }', - "12/25": '{ "game1": {"Michang": 2, "Seunan": 3}, "game2": {"Jiko": 3, "Seonjo": 2}, "game3": {"Seunan": 1, "Jiko": 2} }', - }; + // this.games = { + // "09/02": '{ "game1": {"Seonjo": 2, "Michang": 1}, "game2": {"Jiko": 3, "Jaehejun": 2}, "game3": {"Seonjo": 2, "Jiko": 3} }', + // "10/02": '{ "game1": {"Jaehejun": 1, "Seonjo": 2}, "game2": {"Michang": 2, "Seunan": 1}, "game3": {"Seonjo": 2, "Michang": 3} }', + // "10/12": '{ "game1": {"Michang": 2, "Jiko": 1}, "game2": {"Seonjo": 3, "Seunan": 2}, "game3": {"Michang": 1, "Seonjo": 3} }', + // "10/25": '{ "game1": {"Jaehejun": 2, "Seunan": 3}, "game2": {"Michang": 3, "Seonjo": 2}, "game3": {"Seunan": 1, "Michang": 2} }', + // "11/12": '{ "game1": {"Jiko": 2, "Jaehejun": 1}, "game2": {"Seunan": 3, "Michang": 2}, "game3": {"Jiko": 2, "Seunan": 3} }', + // "12/25": '{ "game1": {"Michang": 2, "Seunan": 3}, "game2": {"Jiko": 3, "Seonjo": 2}, "game3": {"Seunan": 1, "Jiko": 2} }', + // }; for (let date in this.games) { this.games[date] = JSON.parse(this.games[date]); From e080c0dbba42d483459f153e949ef118b5b813bf Mon Sep 17 00:00:00 2001 From: michang Date: Wed, 25 Dec 2024 22:10:28 +0900 Subject: [PATCH 18/25] =?UTF-8?q?Update=20:=20=EB=AA=A8=EB=93=A0=20?= =?UTF-8?q?=EA=B2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/game/consumers.py | 2 +- backend/game/matchingConsumers.py | 113 ++++++++++++++++++++++++------ backend/game/onlineConsumers.py | 24 +++---- backend/users/serializers.py | 7 +- backend/users/views.py | 11 +++ run_backend.sh | 18 ++--- 6 files changed, 132 insertions(+), 43 deletions(-) diff --git a/backend/game/consumers.py b/backend/game/consumers.py index d962626..bbbf247 100644 --- a/backend/game/consumers.py +++ b/backend/game/consumers.py @@ -10,7 +10,7 @@ SCREEN_HEIGHT = 750 SCREEN_WIDTH = 1250 -MAX_SCORE = 10 +MAX_SCORE = 150 class GameState: diff --git a/backend/game/matchingConsumers.py b/backend/game/matchingConsumers.py index 6b63502..1286de7 100644 --- a/backend/game/matchingConsumers.py +++ b/backend/game/matchingConsumers.py @@ -7,30 +7,61 @@ import jwt from django.conf import settings from channels.exceptions import DenyConnection +from django.shortcuts import get_object_or_404 +from users.models import User, Game +from asgiref.sync import sync_to_async +from django.utils import timezone SCREEN_HEIGHT = 750 SCREEN_WIDTH = 1250 -MAX_SCORE = 150 +MAX_SCORE = 10 + + +@sync_to_async +def get_user_nicknames(user1, user2): + user1_nickname = "Unknown" + user2_nickname = "Unknown" + + # Check user1 + if User.objects.filter(user_id=user1).exists(): + user1_nickname = User.objects.get(user_id=user1).nickname + else: + print(f"User with user_id={user1} does NOT exist.") + + # Check user2 + if User.objects.filter(user_id=user2).exists(): + user2_nickname = User.objects.get(user_id=user2).nickname + else: + print(f"User with user_id={user2} does NOT exist.") + + print("user1_nickname:", user1_nickname, "user2_nickname:", user2_nickname) + return user1_nickname, user2_nickname class MatchingGameState: - def __init__(self, user1, user2): - print("initializing game state!") - self.user = [user1, user2] - self.user_authenticated = [False, False] - self.map = GameMap() - self.left_bar = Bar(0, Bar.X_GAP, SCREEN_HEIGHT // 2 - Bar.HEIGHT // 2, 0) - self.right_bar = Bar( - 1, - SCREEN_WIDTH - Bar.WIDTH - Bar.X_GAP, - SCREEN_HEIGHT // 2 - Bar.HEIGHT // 2, - SCREEN_WIDTH - Bar.WIDTH, - ) - self.left_ball = Ball(0, SCREEN_HEIGHT, SCREEN_WIDTH, self.left_bar) - self.right_ball = Ball(1, SCREEN_HEIGHT, SCREEN_WIDTH, self.right_bar) - self.score = [0, 0] - self.player = ["player1", "player2"] - self.penalty_time = [0, 0] + async def initialize(self, user1, user2): + try: + self.user = [user1, user2] + self.user_authenticated = [False, False] + self.map = GameMap() + self.left_bar = Bar(0, Bar.X_GAP, SCREEN_HEIGHT // 2 - Bar.HEIGHT // 2, 0) + self.right_bar = Bar( + 1, + SCREEN_WIDTH - Bar.WIDTH - Bar.X_GAP, + SCREEN_HEIGHT // 2 - Bar.HEIGHT // 2, + SCREEN_WIDTH - Bar.WIDTH, + ) + self.left_ball = Ball(0, SCREEN_HEIGHT, SCREEN_WIDTH, self.left_bar) + self.right_ball = Ball(1, SCREEN_HEIGHT, SCREEN_WIDTH, self.right_bar) + self.score = [0, 0] + print("user1:", user1, "user2:", user2) + user1_nickname, user2_nickname = await get_user_nicknames(user1, user2) + print("user1_nickname:", user1_nickname, "user2_nickname:", user2_nickname) + self.player = [user1_nickname, user2_nickname] + self.penalty_time = [0, 0] + print("initializing game state finished successfully!") + except Exception as e: + print("Error in MatchingGameState initialization:", e) class MatchingGameConsumer(AsyncWebsocketConsumer): @@ -42,6 +73,7 @@ async def connect(self): self.room_name = self.scope["url_route"]["kwargs"]["room_name"] self.room_group_name = self.room_name self.authenticated = False + self.start_time = timezone.now() await self.accept() @@ -56,6 +88,7 @@ async def receive(self, text_data): await self.close(code=4001) return self.authenticated = True + self.my_uid = 0 # Join room group after successful authentication await self.channel_layer.group_add(self.room_group_name, self.channel_name) @@ -82,7 +115,7 @@ async def receive(self, text_data): state = MatchingGameConsumer.game_states[self.room_group_name] # Update the bar position based on the action - if bar == "left": + if bar == "left" and self.user_index == 0: if action == "move_up": state.left_bar.move_up(Bar.SPEED) elif action == "move_down": @@ -91,7 +124,7 @@ async def receive(self, text_data): state.left_bar.pull() elif action == "release": state.left_bar.set_release() - elif bar == "right": + elif bar == "right" and self.user_index == 1: if action == "move_up": state.right_bar.move_up(Bar.SPEED) elif action == "move_down": @@ -214,6 +247,8 @@ async def game_loop(self): async def send_game_result(self, winner): state = MatchingGameConsumer.game_states[self.room_group_name] + + await self.save_game_result(state, winner) await self.channel_layer.group_send( self.room_group_name, { @@ -222,6 +257,44 @@ async def send_game_result(self, winner): "winner": winner, }, ) + + @sync_to_async + def save_game_result(self, state, winner): + try: + user1_obj = User.objects.get(user_id=state.user[0]) + except User.DoesNotExist: + user1_obj = None + try: + user2_obj = User.objects.get(user_id=state.user[1]) + except User.DoesNotExist: + user2_obj = None + + score1, score2 = state.score + + game = Game.objects.create( + game_type="PvP", + user1=user1_obj, + user2=user2_obj, + score1=score1, + score2=score2, + start_timestamp=self.start_time, + end_timestamp=timezone.now(), + ) + + if winner == 0: + if user1_obj: + user1_obj.win += 1 + user1_obj.save() + if user2_obj: + user2_obj.lose += 1 + user2_obj.save() + else: + if user2_obj: + user2_obj.win += 1 + user2_obj.save() + if user1_obj: + user1_obj.lose += 1 + user1_obj.save() async def game_result_message(self, event): score = event["score"] diff --git a/backend/game/onlineConsumers.py b/backend/game/onlineConsumers.py index bfed8e7..a7db3ed 100644 --- a/backend/game/onlineConsumers.py +++ b/backend/game/onlineConsumers.py @@ -18,6 +18,7 @@ async def start_matching_task(cls): while True: await asyncio.sleep(2) if len(cls.matching_queue) >= 2: + print("2 users in queue") await cls.start_game() @classmethod @@ -25,13 +26,11 @@ async def start_game(cls): user1 = cls.matching_queue.pop(0) user2 = cls.matching_queue.pop(0) - # Create a unique room name for the game room_name = f"{user1.uid}_{user2.uid}" - # Initialize game state for the room - MatchingGameConsumer.game_states[room_name] = MatchingGameState( - user1.uid, user2.uid - ) + game_state = MatchingGameState() + await game_state.initialize(user1.uid, user2.uid) # 비동기 초기화 + MatchingGameConsumer.game_states[room_name] = game_state MatchingGameConsumer.client_counts[room_name] = 2 await user1.send( @@ -44,12 +43,10 @@ async def start_game(cls): print(f"Users {user1.uid} and {user2.uid} moved to game room: {room_name}") async def connect(self): - # Wait for authentication before joining room group self.uid = None self.authenticated = False await self.accept() - # 매칭 task가 없으면 시작 if OnlineConsumer.matching_task is None: OnlineConsumer.matching_task = asyncio.create_task( OnlineConsumer.start_matching_task() @@ -114,8 +111,11 @@ async def leave_matching(self): ) async def disconnect(self, close_code): - # Leave room group - if self.authenticated: - OnlineConsumer.online_user_list.remove(self.uid) - await self.leave_matching() - print("Online user list: ", OnlineConsumer.online_user_list) + try: + if self.uid in OnlineConsumer.online_user_list: + OnlineConsumer.online_user_list.remove(self.uid) + print(f"Disconnected user {self.uid}. Updated list: {OnlineConsumer.online_user_list}") + else: + print(f"User {self.uid} not found in the online user list.") + except Exception as e: + print(f"Error during disconnect: {e}") diff --git a/backend/users/serializers.py b/backend/users/serializers.py index ebb6ff9..49bf778 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -1,12 +1,17 @@ from rest_framework import serializers from .models import User, Friend, Game, Tournament - +from game.onlineConsumers import OnlineConsumer class UserSerializer(serializers.ModelSerializer): + is_online = serializers.SerializerMethodField() + class Meta: model = User fields = ["user_id", "nickname", "img_url", "is_2FA", "is_online", "win", "lose"] + def get_is_online(self, obj): + return obj.user_id in OnlineConsumer.online_user_list + class LanguageSerializer(serializers.ModelSerializer): class Meta: model = User diff --git a/backend/users/views.py b/backend/users/views.py index 4dae52b..3102691 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -13,6 +13,7 @@ ) from login.views import decode_jwt from drf_yasg.utils import swagger_auto_schema +from game.onlineConsumers import OnlineConsumer class UserDetailView(APIView): @@ -47,6 +48,16 @@ def post(self, request): # Delete the JWT cookie response = Response({"message": "Logged out successfully"}) response.delete_cookie("jwt") + + user_id = decode_jwt(request).get("id") + try: + if user_id in OnlineConsumer.online_user_list: + OnlineConsumer.online_user_list.remove(user_id) + print(f"User {user_id} removed. Online user list: {OnlineConsumer.online_user_list}") + else: + print(f"User {user_id} not found in the online user list.") + except Exception as e: + print(f"Error while logging out: {e}") return response def delete(self, request): diff --git a/run_backend.sh b/run_backend.sh index c4558c8..cf0278f 100755 --- a/run_backend.sh +++ b/run_backend.sh @@ -40,7 +40,7 @@ else fi if [ ! -d "$ENV_DIR" ]; then - python3.12 -m venv $ENV_DIR + python3 -m venv $ENV_DIR echo "Virtual environment created at $ENV_DIR." else echo "Virtual environment already exists at $ENV_DIR." @@ -49,11 +49,11 @@ fi source $ENV_DIR/bin/activate echo "Virtual environment activated." -python3.12 -m pip install -q --upgrade pip +python3 -m pip install -q --upgrade pip if [ -f "requirements.txt" ]; then - cnt=$(python3.12 -m pip freeze | grep -f requirements.txt | wc -l) + cnt=$(python3 -m pip freeze | grep -f requirements.txt | wc -l) if [ $cnt -lt $(cat requirements.txt | wc -l) ]; then - python3.12 -m pip install -q -r requirements.txt + python3 -m pip install -q -r requirements.txt echo "Installed packages from requirements.txt." else echo "Packages from requirements.txt are already installed." @@ -64,15 +64,15 @@ else fi echo "Applying migrations..." -python3.12 manage.py makemigrations -python3.12 manage.py migrate +python3 manage.py makemigrations +python3 manage.py migrate # Create a superuser DJANGO_SUPERUSER_USERNAME=$DJANGO_SUPERUSER_USERNAME DJANGO_SUPERUSER_PASSWORD=$DJANGO_SUPERUSER_PASSWORD DJANGO_SUPERUSER_EMAIL=$DJANGO_SUPERUSER_EMAIL -USER_EXISTS=$(python3.12 manage.py shell -c " +USER_EXISTS=$(python3 manage.py shell -c " from django.contrib.auth import get_user_model; User = get_user_model(); print(User.objects.filter(username='admin').exists()) @@ -80,10 +80,10 @@ print(User.objects.filter(username='admin').exists()) if [ "$USER_EXISTS" = "False" ]; then echo "Creating superuser..." - python3.12 manage.py createsuperuser --username $DJANGO_SUPERUSER_USERNAME --email $DJANGO_SUPERUSER_EMAIL --noinput || true + python3 manage.py createsuperuser --username $DJANGO_SUPERUSER_USERNAME --email $DJANGO_SUPERUSER_EMAIL --noinput || true fi echo "Starting development server..." # clear -python3.12 manage.py runserver 0.0.0.0:8000 +python3 manage.py runserver 0.0.0.0:8000 From 2f527c1075dec9421bfb5e8f21d35c56092a934c Mon Sep 17 00:00:00 2001 From: seonjo1 Date: Thu, 26 Dec 2024 18:25:21 +0900 Subject: [PATCH 19/25] dev: tournament complete --- frontend/src/components/Game-Result-Page.js | 26 +++++++++++++------ frontend/src/components/Tournament-History.js | 4 +-- frontend/src/components/Tournament-Setting.js | 18 ++++++------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/frontend/src/components/Game-Result-Page.js b/frontend/src/components/Game-Result-Page.js index 0e0d712..4f2cb38 100644 --- a/frontend/src/components/Game-Result-Page.js +++ b/frontend/src/components/Game-Result-Page.js @@ -55,16 +55,25 @@ export class GameResultPage extends Component { !(parsedGame1.winner === parsedGame3.winner && parsedGame2.winner === parsedGame3.loser)) { changeUrl("/main/tournament"); } - // fetch + const payload = { game_info: JSON.stringify({ - game1: parsedGame1, - game2: parsedGame2, - game3: parsedGame3, + "date": `${new Date().getMonth() + 1}/${new Date().getDate()}`, + game1: { + [parsedNicknames.nick1]: parsedGame1.score1, + [parsedNicknames.nick2]: parsedGame1.score2 + }, + game2: { + [parsedNicknames.nick3]: parsedGame2.score1, + [parsedNicknames.nick4]: parsedGame2.score2 + }, + game3: { + [parsedGame1.winner]: parsedGame3.score1, + [parsedGame2.winner]: parsedGame3.score2 + } }) }; - console.log("payload: "); - console.log(JSON.stringify(payload,null,2)); + fetch("https://localhost:443/api/game-history/tournament", { method: 'POST', credentials: 'include', // 쿠키를 포함하여 요청 @@ -77,13 +86,14 @@ export class GameResultPage extends Component { if (!response.ok) { throw new Error('Failed to send tournament results'); } - changeUrl("/main/tournament"); }) .catch(error => { console.error("Error sending tournament results:", error); changeUrl("/"); }); - } else { + changeUrl("/main/tournament"); + } + else { changeUrl("/main/tournament"); } } else { diff --git a/frontend/src/components/Tournament-History.js b/frontend/src/components/Tournament-History.js index a6c67b5..ba8ae1c 100644 --- a/frontend/src/components/Tournament-History.js +++ b/frontend/src/components/Tournament-History.js @@ -11,10 +11,10 @@ export class TournamentHistory extends Component { const { gameInfo } = this.props; const game = Object.values(gameInfo)[this.state.idx]; - const day = Object.keys(gameInfo)[this.state.idx]; this.size = Object.keys(gameInfo).length - 1; // 게임 1의 데이터 + const date = game.date; const game1_nick1 = Object.keys(game.game1)[0]; const game1_score1 = game.game1[game1_nick1]; const game1_nick2 = Object.keys(game.game1)[1]; @@ -68,7 +68,7 @@ export class TournamentHistory extends Component {
-
${day} - Game
+
${date} - Game
`; diff --git a/frontend/src/components/Tournament-Setting.js b/frontend/src/components/Tournament-Setting.js index 1e23498..718f86b 100644 --- a/frontend/src/components/Tournament-Setting.js +++ b/frontend/src/components/Tournament-Setting.js @@ -21,21 +21,15 @@ export class TournamentSetting extends Component { return response.json(); }) .then(data => { - this.games = data.games; // 서버에서 받아온 데이터 구조에 맞게 설정 + this.games = data.tournaments_list; // 서버에서 받아온 데이터 구조에 맞게 설정 + this.games = this.games.filter(item => { + return item.game1 && item.game2 && item.game3 && item.date; + }); }) .catch(error => { console.error("Error fetching tournament history:", error); this.games = {}; // 오류 발생 시 기본값 설정 }); - - // this.games = { - // "09/02": '{ "game1": {"Seonjo": 2, "Michang": 1}, "game2": {"Jiko": 3, "Jaehejun": 2}, "game3": {"Seonjo": 2, "Jiko": 3} }', - // "10/02": '{ "game1": {"Jaehejun": 1, "Seonjo": 2}, "game2": {"Michang": 2, "Seunan": 1}, "game3": {"Seonjo": 2, "Michang": 3} }', - // "10/12": '{ "game1": {"Michang": 2, "Jiko": 1}, "game2": {"Seonjo": 3, "Seunan": 2}, "game3": {"Michang": 1, "Seonjo": 3} }', - // "10/25": '{ "game1": {"Jaehejun": 2, "Seunan": 3}, "game2": {"Michang": 3, "Seonjo": 2}, "game3": {"Seunan": 1, "Michang": 2} }', - // "11/12": '{ "game1": {"Jiko": 2, "Jaehejun": 1}, "game2": {"Seunan": 3, "Michang": 2}, "game3": {"Jiko": 2, "Seunan": 3} }', - // "12/25": '{ "game1": {"Michang": 2, "Seunan": 3}, "game2": {"Jiko": 3, "Seonjo": 2}, "game3": {"Seunan": 1, "Jiko": 2} }', - // }; for (let date in this.games) { this.games[date] = JSON.parse(this.games[date]); @@ -104,6 +98,10 @@ export class TournamentSetting extends Component { const nick3 = document.querySelector('#tournament-nick3').value; const nick4 = document.querySelector('#tournament-nick4').value; + if (nick1 === nick2 || nick1 === nick3 || nick1 === nick4 || nick2 === nick3 || nick2 === nick4 || nick3 === nick4){ + return; + } + if (!nick1 || !nick2 || !nick3 || !nick4) { document.querySelector('#tournament-nick-error').style.display = 'flex'; return; From a6ff529806330dc7863c08521495060f3a55018c Mon Sep 17 00:00:00 2001 From: seonjo1 Date: Thu, 26 Dec 2024 18:37:29 +0900 Subject: [PATCH 20/25] =?UTF-8?q?Update:=20=EA=B2=8C=EC=9E=84=20=EC=A2=85?= =?UTF-8?q?=EB=A3=8C=EC=8B=9C=20=ED=99=94=EB=A9=B4=20=EC=A0=84=ED=99=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Game-Core.js | 8 ++++---- frontend/src/components/Game-matching-Core.js | 5 ++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frontend/src/components/Game-Core.js b/frontend/src/components/Game-Core.js index f121cf7..e4253b3 100644 --- a/frontend/src/components/Game-Core.js +++ b/frontend/src/components/Game-Core.js @@ -1,6 +1,7 @@ import { Component } from "../core/Component.js"; import { getCookie } from "../core/jwt.js"; -import { socketList } from "../app.js" +import { socketList } from "../app.js"; +import { changeUrl } from "../core/router.js"; export class GameCore extends Component { constructor($el, props) { @@ -336,11 +337,10 @@ export class GameCore extends Component { this.gameSocket.close(); socketList.pop(); if (data.winner === 0) { - alert(`${PLAYER[0]} wins!`); + changeUrl(`/game/${this.props.uid}/result/${PLAYER[0]}`); } else { - alert(`${PLAYER[1]} wins!`); + changeUrl(`/game/${this.props.uid}/result/${PLAYER[1]}`); } - changeUrl('/main', false); } }; diff --git a/frontend/src/components/Game-matching-Core.js b/frontend/src/components/Game-matching-Core.js index 6b730c4..e4ddd85 100644 --- a/frontend/src/components/Game-matching-Core.js +++ b/frontend/src/components/Game-matching-Core.js @@ -337,11 +337,10 @@ export class GameMatchingCore extends Component { this.gameSocket.close(); socketList.pop(); if (data.winner === 0) { - alert(`${PLAYER[0]} wins!`); + changeUrl(`/game/${this.props.uid}/result/${PLAYER[0]}`); } else { - alert(`${PLAYER[1]} wins!`); + changeUrl(`/game/${this.props.uid}/result/${PLAYER[1]}`); } - changeUrl('/main', false); } }; From 7d47650722818bb445c6cc6d5700074354cac02a Mon Sep 17 00:00:00 2001 From: seonjo1 Date: Thu, 26 Dec 2024 18:46:05 +0900 Subject: [PATCH 21/25] =?UTF-8?q?Update:=20profile=20=EC=A0=90=EC=88=98=20?= =?UTF-8?q?=EA=B8=80=EC=9E=90=20=ED=81=AC=EA=B8=B0=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/style.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/style.css b/frontend/style.css index 7b35ead..51fa7e0 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -1006,6 +1006,7 @@ div#opImg { img#opImg { + margin-left: 15%; object-fit: cover; border-radius: 10px; width: 40%; @@ -1033,13 +1034,13 @@ div#matchScore { } span#myScorewin { - font-size: 30px; + font-size: 25px; font-weight: 700; color:rgb(20, 74, 188); } span#myScorelose { - font-size: 30px; + font-size: 25px; font-weight: 700; color:rgb(187, 23, 23) } @@ -1052,7 +1053,7 @@ span#vsSign { } span#opScore { - font-size: 30px; + font-size: 25px; font-weight: 700; } From b023e70ef4660a6011fa11da06a242ee099f29e8 Mon Sep 17 00:00:00 2001 From: seonjo1 Date: Thu, 26 Dec 2024 19:21:03 +0900 Subject: [PATCH 22/25] =?UTF-8?q?Update:=20=EC=99=84=EB=B2=BD=ED=95=9C=20?= =?UTF-8?q?=EB=B2=88=EC=97=AD=EA=B0=80=20=EC=A1=B0=EC=84=B1=ED=98=84=20?= =?UTF-8?q?=EB=93=B1=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/Game-Result-Page.js | 22 +++++- frontend/src/components/Main.js | 1 - frontend/src/components/Match-Wait.js | 21 +++++- frontend/src/components/Profile-Info.js | 11 ++- frontend/src/components/Profile-List.js | 3 +- frontend/src/components/Tournament-History.js | 2 +- frontend/src/components/Tournament-Setting.js | 74 ++++++++++++++++--- 7 files changed, 113 insertions(+), 21 deletions(-) diff --git a/frontend/src/components/Game-Result-Page.js b/frontend/src/components/Game-Result-Page.js index 4f2cb38..ecb7ff1 100644 --- a/frontend/src/components/Game-Result-Page.js +++ b/frontend/src/components/Game-Result-Page.js @@ -3,11 +3,31 @@ import { changeUrl } from "../core/router.js"; export class GameResultPage extends Component { + translate() { + const languages = { + 0: { + winnerText: ["Winner is "], + }, + + 1: { + winnerText: ["승자 "], + }, + + 2: { + winnerText: ["勝者 "], + } + }; + + this.translations = languages[this.props.lan.value]; + + } + template () { + const translations = this.translations; return `
-
Winner is ${this.props.winner}!!!
+
${translations.winnerText}${this.props.winner}!!!
OK
diff --git a/frontend/src/components/Main.js b/frontend/src/components/Main.js index d09f491..ef086c7 100644 --- a/frontend/src/components/Main.js +++ b/frontend/src/components/Main.js @@ -4,6 +4,5 @@ import { Menu } from "./Main-Menu.js"; export class Main extends Default { mounted(){ new Menu(document.querySelector("div#contents"), this.props); - console.log(this.props.lan.value); } } \ No newline at end of file diff --git a/frontend/src/components/Match-Wait.js b/frontend/src/components/Match-Wait.js index 42cf17a..b910e95 100644 --- a/frontend/src/components/Match-Wait.js +++ b/frontend/src/components/Match-Wait.js @@ -22,10 +22,29 @@ export class WaitForMatch extends Component { return {}; } + translate() { + const languages = { + 0: { + mathcingText: ["Finding Your Match..."], + }, + 1: { + mathcingText: ["게임 상대를 찾고있습니다..."], + }, + 2: { + mathcingText: ["ピッタリの相手を探してるよ..."], + } + }; + + this.translations = languages[this.props.lan.value]; + + } + template () { + const translations = this.translations; + return `
-
Finding Your Match...
+
${translations.mathcingText}
diff --git a/frontend/src/components/Profile-Info.js b/frontend/src/components/Profile-Info.js index c7e077e..fdc7d47 100644 --- a/frontend/src/components/Profile-Info.js +++ b/frontend/src/components/Profile-Info.js @@ -12,21 +12,24 @@ export class ProfileInfo extends Component { winText: "Win", loseText: "Lose", minText: "min", - editText: "edit" + editText: "edit", + dateText: "Date" }, 1: { headText: "프로필", winText: "승리", loseText: "패배", minText: "분", - editText: "수정" + editText: "수정", + dateText: "날짜" }, 2: { headText: "プロフィール", winText: "勝ち", loseText: "負け", minText: "分", - editText: "編集" + editText: "編集", + dateText: "日付" } }; this.translations = languages[this.props.lan.value]; @@ -119,7 +122,7 @@ x } else { } mounted() { - new MatchList(document.querySelector("ul#matches"), { matches: this.state.games, minText: this.translations.minText }); + new MatchList(document.querySelector("ul#matches"), { matches: this.state.games, minText: this.translations.minText, dateText: this.translations.dateText }); this.drawBackgroundCircle(); this.drawProgressCircle(); this.updatePercentage(); diff --git a/frontend/src/components/Profile-List.js b/frontend/src/components/Profile-List.js index 78071a7..756c317 100644 --- a/frontend/src/components/Profile-List.js +++ b/frontend/src/components/Profile-List.js @@ -4,6 +4,7 @@ export class MatchList extends Component { template () { const { matches } = this.props; + const translations = this.translations; if (!matches) return ""; return ` ${matches.map(element => { @@ -18,7 +19,7 @@ export class MatchList extends Component { return `
  • - Date + ${this.props.dateText}
    ${month}/${day} ${hours}:${minutes}
    ${element.playtime}${this.props.minText}
    diff --git a/frontend/src/components/Tournament-History.js b/frontend/src/components/Tournament-History.js index ba8ae1c..c983318 100644 --- a/frontend/src/components/Tournament-History.js +++ b/frontend/src/components/Tournament-History.js @@ -68,7 +68,7 @@ export class TournamentHistory extends Component {
  • -
    ${date} - Game
    +
    ${date}
    `; diff --git a/frontend/src/components/Tournament-Setting.js b/frontend/src/components/Tournament-Setting.js index 718f86b..4d09ca1 100644 --- a/frontend/src/components/Tournament-Setting.js +++ b/frontend/src/components/Tournament-Setting.js @@ -5,11 +5,61 @@ import { changeUrl } from "../core/router.js"; export class TournamentSetting extends Component { + translate() { + const languages = { + 0: { + gameText: ["Game"], + historyText: ["History"], + startText: ["S T A R T"], + title: ["Tournament"], + centerText: ["TAKE ON THE CHALLENGE"], + winnerText: ["Winner"], + TBDText: ["TBD"], + nickText1: ["nickname1"], + nickText2: ["nickname2"], + nickText3: ["nickname3"], + nickText4: ["nickname4"], + }, + 1: { // 한국어 + gameText: ["게임"], + historyText: ["기록"], + startText: ["시 작"], + title: ["토너먼트"], + centerText: ["도전에 맞서라"], + winnerText: ["승자"], + TBDText: ["미정"], + nickText1: ["닉네임1"], + nickText2: ["닉네임2"], + nickText3: ["닉네임3"], + nickText4: ["닉네임4"], + }, + 2: { // 일본어 + gameText: ["ゲーム"], + historyText: ["履歴"], + startText: ["ス タ ー ト"], + title: ["トーナメント"], + centerText: ["挑戦を受けて立て"], + winnerText: ["勝者"], + TBDText: ["未定"], + nickText1: ["ニックネーム1"], + nickText2: ["ニックネーム2"], + nickText3: ["ニックネーム3"], + nickText4: ["ニックネーム4"], + } + }; + + this.translations = languages[this.props.lan.value]; + + } + template () { + const payload = parseJWT(); if (!payload) this.uid = null; else this.uid = payload.id; + const translations = this.translations; + fetch("https://localhost:443/api/game-history/tournament", { method: "GET", credentials: "include", // 쿠키 포함 @@ -39,13 +89,13 @@ export class TournamentSetting extends Component { return `
    -
    Game
    -
    History
    -
    Tournament
    +
    ${translations.gameText}
    +
    ${translations.historyText}
    +
    ${translations.title}
    -
    Take on the challenge
    +
    ${translations.centerText}
    @@ -56,27 +106,27 @@ export class TournamentSetting extends Component {
    -
    Winner
    +
    ${translations.winnerText}
    -
    TBD
    -
    TBD
    +
    ${translations.TBDText}
    +
    ${translations.TBDText}
    - - + +
    - - + +
    -
    S T A R T
    +
    ${translations.startText}
    From 27c3ffc17c0b83ea9151dc0cced16629741e203f Mon Sep 17 00:00:00 2001 From: Jaehee Jung Date: Fri, 27 Dec 2024 18:31:25 +0900 Subject: [PATCH 23/25] =?UTF-8?q?friend=20=EC=B6=94=EA=B0=80/=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EB=8B=A8=EB=B0=A9=ED=96=A5=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EC=B9=9C=EA=B5=AC=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20=EC=97=90=EB=9F=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/users/models.py | 7 +++---- backend/users/serializers.py | 2 +- backend/users/views.py | 40 +++++++++++++----------------------- 3 files changed, 18 insertions(+), 31 deletions(-) diff --git a/backend/users/models.py b/backend/users/models.py index cc11987..b187d40 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -24,12 +24,11 @@ def __str__(self): class Friend(models.Model): friend_id = models.AutoField(primary_key=True) - user1 = models.ForeignKey(User, on_delete=models.CASCADE, related_name="friends_as_user1") - user2 = models.ForeignKey(User, on_delete=models.CASCADE, related_name="friends_as_user2") + adder = models.ForeignKey(User, on_delete=models.CASCADE, related_name="friend_added") + friend_user = models.ForeignKey(User, on_delete=models.CASCADE) def __str__(self): - return f"Friend: {self.user1.nickname} - {self.user2.nickname}" - + return f"{self.adder.nickname} - {self.friend_user.nickname}" class Game(models.Model): game_id = models.AutoField(primary_key=True) diff --git a/backend/users/serializers.py b/backend/users/serializers.py index 49bf778..302a673 100644 --- a/backend/users/serializers.py +++ b/backend/users/serializers.py @@ -20,7 +20,7 @@ class Meta: class FriendSerializer(serializers.ModelSerializer): class Meta: model = Friend - fields = ["user1", "user2"] + fields = ["adder", "friend_user"] class FriendRequestSerializer(serializers.Serializer): diff --git a/backend/users/views.py b/backend/users/views.py index 3102691..4f72a01 100644 --- a/backend/users/views.py +++ b/backend/users/views.py @@ -103,25 +103,14 @@ def get(self, request): payload = decode_jwt(request) if not payload: return Response(status=status.HTTP_401_UNAUTHORIZED) + user = get_object_or_404(User, pk=payload.get("id")) + + # 단방향 친구 관계만 조회 + friends = Friend.objects.filter(adder=user) - # 유저와 친구 상태인 유저 모델들을 모두 가져옴 - friends_as_user1 = Friend.objects.filter(user1=user) - friends_as_user2 = Friend.objects.filter(user2=user) - - # 친구 유저 ID 수집 - friend_ids = set(friends_as_user1.values_list("user2", flat=True)) | set( - friends_as_user2.values_list("user1", flat=True) - ) - - # 친구 ID를 통해 친구 유저 정보 가져오기 - try: - friends = User.objects.filter(pk__in=friend_ids) - except User.DoesNotExist: - return Response({"[]"}, status=status.HTTP_200_OK) - - # 직렬화하여 JSON 응답으로 반환 - serializer = UserSerializer(friends, many=True) + # 친구 정보 직렬화해 JSON 응답으로 반환 + serializer = UserSerializer([friend.friend_user for friend in friends], many=True) return JsonResponse(serializer.data, safe=False) @swagger_auto_schema( @@ -132,6 +121,7 @@ def post(self, request): payload = decode_jwt(request) if not payload: return Response(status=status.HTTP_401_UNAUTHORIZED) + user = get_object_or_404(User, pk=payload.get("id")) serializer = FriendRequestSerializer(data=request.data) @@ -143,15 +133,12 @@ def post(self, request): return Response({"error": "Cannot be friend with yourself"}, status=status.HTTP_400_BAD_REQUEST) friend_user = get_object_or_404(User, pk=friend_user_id) - # 이미 존재하는 친구 관계 확인 - if ( - Friend.objects.filter(user1=user, user2=friend_user).exists() - or Friend.objects.filter(user1=friend_user, user2=user).exists() - ): + # 중복 친구 요청 방지(단방향만 체크) + if Friend.objects.filter(adder=user, friend_user=friend_user).exists(): return Response({"error": "Friendship already exists"}, status=status.HTTP_400_BAD_REQUEST) - # 새로운 친구 관계 생성 - friend = Friend(user1=user, user2=friend_user) + # 새로운 단방향 친구 관계 생성 + friend = Friend(adder=user, friend_user=friend_user) friend.save() response_serializer = FriendSerializer(friend) @@ -165,13 +152,14 @@ def delete(self, request): payload = decode_jwt(request) if not payload: return Response(status=status.HTTP_401_UNAUTHORIZED) + user = get_object_or_404(User, pk=payload.get("id")) friend_user_id = request.data.get("user_id") friend_user = get_object_or_404(User, pk=friend_user_id) - # 이미 존재하는 친구 관계 확인 - friend = Friend.objects.filter(user1=user, user2=friend_user) + # 요청한 친구 관계만 삭제 + friend = Friend.objects.filter(adder=user, friend_user=friend_user) if not friend.exists(): return Response({"error": "Friendship does not exist"}, status=status.HTTP_400_BAD_REQUEST) From d5fb073fb6ba9855e2729c5bc44115225594b155 Mon Sep 17 00:00:00 2001 From: Jaehee Jung Date: Sat, 28 Dec 2024 12:59:26 +0900 Subject: [PATCH 24/25] =?UTF-8?q?friend=20=EC=88=98=EC=A0=95=20=EC=9E=AC?= =?UTF-8?q?=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/users/management/__init__.py | 0 backend/users/management/commands/__init__.py | 0 .../users/management/commands/populatedb.py | 72 ------------------- 3 files changed, 72 deletions(-) delete mode 100644 backend/users/management/__init__.py delete mode 100644 backend/users/management/commands/__init__.py delete mode 100644 backend/users/management/commands/populatedb.py diff --git a/backend/users/management/__init__.py b/backend/users/management/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/users/management/commands/__init__.py b/backend/users/management/commands/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/users/management/commands/populatedb.py b/backend/users/management/commands/populatedb.py deleted file mode 100644 index 5b917f2..0000000 --- a/backend/users/management/commands/populatedb.py +++ /dev/null @@ -1,72 +0,0 @@ -from django.core.management.base import BaseCommand -from django.contrib.auth import get_user_model -from users.models import User, Friend, Game, Tournament -from django.utils import timezone -import random - -class Command(BaseCommand): - help = "Populates the database with sample data" - - def handle(self, *args, **kwargs): - self.stdout.write("Populating database...") - - # Create users - users = list(User.objects.all()) - - for i in range(5): - user, created = User.objects.update_or_create( - user_id=i, - defaults={ - "nickname": f"User{i}", - "email": f"user{i}@example.com", - "img_url": "https://picsum.photos/200", - "is_2FA": random.choice([True, False]), - "is_online": random.choice([True, False]), - }, - ) - users.append(user) - - - # Create friends - for i in range(20): - user1, user2 = random.sample(users, 2) - Friend.objects.create(user1=user1, user2=user2) - - # Create games - for i in range(50): - user1, user2 = random.sample(users, 2) - user1_score = random.randint(0, 5) - user2_score = random.randint(0, 5) - while user1_score == user2_score: - user2_score = random.randint(0, 5) - if (user1_score > user2_score): - user1.win += 1 - user2.lose += 1 - else: - user1.lose += 1 - user2.win += 1 - Game.objects.create( - game_type="PvP", - user1=user1, - user2=user2, - score1=user1_score, - score2=user2_score, - start_timestamp=timezone.now(), - end_timestamp=timezone.now() + timezone.timedelta(minutes=10), - ) - user1.save() - user2.save() - - # Create tournaments - games = list(Game.objects.all()) - for i in range(5): - game1, game2, game3 = random.sample(games, 3) - Tournament.objects.create( - game_id1=game1, - game_id2=game2, - game_id3=game3, - start_timestamp=timezone.now(), - end_timestamp=timezone.now() + timezone.timedelta(minutes=30), - ) - - self.stdout.write(self.style.SUCCESS("Database successfully populated!")) From 5704e06bf0c727602f7a58712a6d79273955c25f Mon Sep 17 00:00:00 2001 From: Jaehee Jung Date: Sat, 28 Dec 2024 14:03:29 +0900 Subject: [PATCH 25/25] =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=ED=83=88?= =?UTF-8?q?=ED=87=B4=20=EC=8B=9C=20=EA=B2=8C=EC=9E=84=20=EA=B8=B0=EB=A1=9D?= =?UTF-8?q?=EB=8F=84=20=EB=AA=A8=EB=91=90=20=EC=82=AD=EC=A0=9C=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD,=20=EC=8A=B9=EB=A5=A0?= =?UTF-8?q?=EB=A1=9C=EB=A7=8C=20=EB=82=A8=EC=95=84=EC=9E=88=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/users/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/users/models.py b/backend/users/models.py index b187d40..079b998 100644 --- a/backend/users/models.py +++ b/backend/users/models.py @@ -33,8 +33,8 @@ def __str__(self): class Game(models.Model): game_id = models.AutoField(primary_key=True) game_type = models.CharField(max_length=255) - user1 = models.ForeignKey(User, null=True, on_delete=models.SET_NULL, related_name="games_as_user1") - user2 = models.ForeignKey(User, null=True, on_delete=models.SET_NULL, related_name="games_as_user2") + user1 = models.ForeignKey(User, null=True, on_delete=models.CASCADE, related_name="games_as_user1") + user2 = models.ForeignKey(User, null=True, on_delete=models.CASCADE, related_name="games_as_user2") score1 = models.IntegerField() score2 = models.IntegerField() start_timestamp = models.DateTimeField() @@ -48,9 +48,9 @@ def __str__(self): class Tournament(models.Model): tournament_id = models.AutoField(primary_key=True) - game_id1 = models.ForeignKey(Game, null=True, on_delete=models.SET_NULL, related_name="tournaments_as_game1") - game_id2 = models.ForeignKey(Game, null=True, on_delete=models.SET_NULL, related_name="tournaments_as_game2") - game_id3 = models.ForeignKey(Game, null=True, on_delete=models.SET_NULL, related_name="tournaments_as_game3") + game_id1 = models.ForeignKey(Game, null=True, on_delete=models.CASCADE, related_name="tournaments_as_game1") + game_id2 = models.ForeignKey(Game, null=True, on_delete=models.CASCADE, related_name="tournaments_as_game2") + game_id3 = models.ForeignKey(Game, null=True, on_delete=models.CASCADE, related_name="tournaments_as_game3") start_timestamp = models.DateTimeField() end_timestamp = models.DateTimeField()