Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions backend/internal/game/chess.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package game

import (
"fmt"
"errors"
"log/slog"
"time"

"github.com/corentings/chess/v2"
Expand Down Expand Up @@ -71,27 +72,38 @@ func (c *chessGame) Move(sender string, mv *GameMove) error {
return err
}

moveStr := rowCol2Move(mv.From) + rowCol2Move(mv.To)
if mv.Change != "" {
moveStr += mv.Change
from := chess.Square((7-mv.From.Row)*8 + mv.From.Col)
to := chess.Square((7-mv.To.Row)*8 + mv.To.Col)
promo := change2Piece(mv.Change)

var move *chess.Move
for _, m := range c.game.ValidMoves() {
if m.S1() == from && m.S2() == to && m.Promo() == promo {
move = &m
break
}
}
notation := chess.UCINotation{}
move, err := notation.Decode(c.game.Position(), moveStr)
if err != nil {
return fmt.Errorf("invalid move format %q: %w", moveStr, err)
if move == nil {
return errors.New("invalid move: not legal in this position")
}
if c.game.Move(move, nil) != nil {
return fmt.Errorf("invalid move %q", moveStr)

if err := c.game.Move(move, nil); err != nil {
slog.Error("Internal error", "error", err)
return errors.New("internal error")
}

if c.game.Outcome() != chess.NoOutcome {
c.status = StatusFin
c.endedAt = time.Now()
switch c.game.Outcome() {
case chess.WhiteWon:
c.status = StatusFin
c.winner = c.players[0]
case chess.BlackWon:
c.status = StatusFin
c.winner = c.players[1]
case chess.Draw:
c.status = StatusFin
}
}
c.turn = 1 - c.turn
Expand All @@ -102,8 +114,18 @@ func (c *chessGame) Move(sender string, mv *GameMove) error {
return nil
}

func rowCol2Move(pos Position) string {
return fmt.Sprintf("%c%d", 'a'+pos.Col, 8-pos.Row)
func change2Piece(change string) chess.PieceType {
switch change {
case "q", "Q":
return chess.Queen
case "r", "R":
return chess.Rook
case "b", "B":
return chess.Bishop
case "n", "N":
return chess.Knight
}
return chess.NoPieceType
}

func pieceToCode(piece chess.Piece) int {
Expand Down
21 changes: 4 additions & 17 deletions backend/internal/game/game.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type Game interface {
getBoardLocked() any
getValidMovesLocked() []GameMove
updateLoop()
handleDisconnects()
handleDisconnectLocked()
}

type GameUpdate struct {
Expand Down Expand Up @@ -158,20 +158,7 @@ func (b *baseGame) Leave(player string, intentional bool) {
b.players = slices.Delete(b.players, idx, idx+1)
}

if len(b.players) == 0 {
b.status = StatusFin
b.endedAt = time.Now()
b.notify(GameUpdate{
State: b.stateLocked(),
Action: DeleteAction,
})
b.Stop()
return
}
b.notify(GameUpdate{
State: b.stateLocked(),
Action: UpdateAction,
})
b.handleDisconnectLocked()
}

func (b *baseGame) checkTurnLocked(sender string) (int, error) {
Expand Down Expand Up @@ -260,11 +247,11 @@ func (b *baseGame) updateLoop() {
b.players = slices.Delete(b.players, idx, idx+1)
}
}
b.self.handleDisconnects()
b.self.handleDisconnectLocked()
}
}

func (b *baseGame) handleDisconnects() {
func (b *baseGame) handleDisconnectLocked() {
// just stop game if player doesn't come back.
b.status = StatusFin
b.endedAt = time.Now()
Expand Down
20 changes: 17 additions & 3 deletions backend/internal/live/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,15 @@ func (c *client) start() {

func (c *client) stop() {
c.cancel()
c.hub.unregister <- c
close(c.send)
close(c.recv)
c.conn.Close(websocket.StatusNormalClosure, "client leaving")
}

func (c *client) readPump() {
defer c.stop()
defer func() {
c.hub.unregister <- c
}()

for {
msgType, msgRaw, err := c.conn.Read(c.ctx)
Expand Down Expand Up @@ -99,11 +100,18 @@ func (c *client) writePump() {
if !ok {
return
}

writeCtx, cancelWrite := context.WithTimeout(c.ctx, c.cfg.WriteTimeout)
err := c.conn.Write(writeCtx, websocket.MessageText, message)
cancelWrite()

if err != nil {
slog.Error("writePump: WebSocket write error", "error", err, "client", c.ID)
if websocket.CloseStatus(err) == websocket.StatusNormalClosure ||
websocket.CloseStatus(err) == websocket.StatusGoingAway {
slog.Debug("WebSocket connection closed", "client", c.ID)
} else {
slog.Error("writePump: WebSocket write error", "error", err, "client", c.ID)
}
c.cancel()
return
}
Expand Down Expand Up @@ -179,6 +187,12 @@ func (c *client) processPump() {
}

func (c *client) trySend(msg []byte) {
defer func() {
if r := recover(); r != nil {
slog.Warn("trySend: Attempted to send on closed channel", "client", c.ID, "recover", r)
}
}()

select {
case c.send <- msg:
default:
Expand Down
2 changes: 2 additions & 0 deletions backend/internal/live/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"letsgo/internal/config"
"letsgo/internal/game"
"log/slog"
"time"
)

type hub struct {
Expand Down Expand Up @@ -52,6 +53,7 @@ func (h *hub) run() {
delete(h.clients, client)
slog.Debug("Unregistered: ", "client", client.ID)
}
time.AfterFunc(100*time.Millisecond, client.stop)

case pair := <-h.joinRoom:
slog.Debug("join room")
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/app/live/boardgame/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { ChessBoard } from '@/components/games/chess/ChessBoard';
import { GameStatus } from '@/components/games/GameStatus';
import { CreateGame } from '@/components/games/CreateGame';
import { GameBoardProps } from '@/types/gameTypes';
import useBoardGame from '@/hooks/useBoardGame';
import { useWebSocket } from '@/hooks/webSocket';
import useBoardGame from '@/hooks/useWSGame';
import { useWebSocket } from '@/hooks/useWebsocket';
import { GAME_DISPLAY_NAMES } from '@/config/consts';
import { Trophy } from 'lucide-react';

Expand Down Expand Up @@ -36,7 +36,7 @@ export default function BoardGamePage() {
{(gameState.status === 'finished') && <div className='fixed inset-0 z-10 flex items-center justify-center pointer-events-none'>
<div className='text-6xl font-extrabold text-accent bg-secondary/70 px-8 py-6 rounded-xl -rotate-12 flex items-center gap-6 animate-pulse'>
<Trophy className='w-20 h-20 ' />
{gameState.winner ?? 'Nobody'} Wins!
{gameState.winner == '' ? 'Nobody' : gameState.winner} Wins!
<Trophy className='w-20 h-20 ' />
</div>
</div>}
Expand All @@ -56,7 +56,7 @@ export default function BoardGamePage() {
leaveGame={leaveGame}
/>

<div className='flex-1 flex flex-col justify-center items-center'>
<div className='flex-1 flex flex-col justify-center items-center touch-none select-none'>
{gameState.gameName !== ''
? GameBoard ?
<div className='w-full max-w-md'>
Expand Down
30 changes: 23 additions & 7 deletions frontend/src/app/live/draw/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import { useDraw } from '@/hooks/useDraw';
import { useWebSocket } from '@/hooks/webSocket';
import { useWebSocket } from '@/hooks/useWebsocket';
import { DrawToolbar } from '@/components/draw/DrawToolbar';
import { useState, useRef, useEffect } from 'react';
import { DRAW_CANVAS_WIDTH, DRAW_CANVAS_HEIGHT } from '@/config/consts';
Expand Down Expand Up @@ -65,7 +65,6 @@ export default function DrawPage() {
return () => window.removeEventListener('resize', updateZoom);
}, [containerRef]);


const getCanvasCoords = (clientX: number, clientY: number) => {
if (!containerRef.current) return { x: 0, y: 0 };
const rect = containerRef.current.getBoundingClientRect();
Expand All @@ -75,6 +74,24 @@ export default function DrawPage() {
};
};

const handleZoom = (zoom: number) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const centerX = rect.width / 2;
const centerY = rect.height / 2;

const x = ((centerX / containerZoom) - viewState.offset.x) / viewState.zoom;
const y = ((centerY / containerZoom) - viewState.offset.y) / viewState.zoom;

setViewState({
zoom: zoom,
offset: {
x: (centerX / containerZoom) - x * zoom,
y: (centerY / containerZoom) - y * zoom,
},
});
}

const startPan = (clientX: number, clientY: number) => {
setIsPanning(true);
panStartRef.current = {
Expand All @@ -88,7 +105,7 @@ export default function DrawPage() {
const handlePan = (clientX: number, clientY: number) => {
if (!isPanning || !panStartRef.current) return;

const scale = 1 / (containerZoom * viewState.zoom);
const scale = 1 / (containerZoom);
const dx = (clientX - panStartRef.current.x) * scale;
const dy = (clientY - panStartRef.current.y) * scale;

Expand Down Expand Up @@ -197,14 +214,14 @@ export default function DrawPage() {
currentZoom={viewState.zoom}
changeColor={color => setDrawState(s => ({ ...s, color }))}
changeWidth={lineWidth => setDrawState(s => ({ ...s, lineWidth }))}
zoomIn={() => setViewState(s => ({ ...s, zoom: Math.min(s.zoom * 1.1, 3) }))}
zoomOut={() => setViewState(s => ({ ...s, zoom: Math.max(s.zoom * 0.9, 1) }))}
zoomIn={() => handleZoom(Math.min(viewState.zoom * 1.1, 3))}
zoomOut={() => handleZoom(Math.max(viewState.zoom * 0.9, 1))}
zoomReset={() => setViewState({ zoom: 1, offset: { x: 0, y: 0 } })}
/>

<div
ref={containerRef}
className='border border-primary border-2 rounded-md overflow-hidden'
className='border border-primary border-2 rounded-md overflow-hidden touch-none'
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
Expand All @@ -216,7 +233,6 @@ export default function DrawPage() {
style={{
transform: `scale(${containerZoom})`,
transformOrigin: '0 0',
touchAction: 'none',
width: `${DRAW_CANVAS_WIDTH}px`,
height: `${DRAW_CANVAS_HEIGHT}px`,
}}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/live/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { useEffect, useRef } from 'react';
import { useRouter } from 'next/navigation';
import ChatIndicator from '@/components/UI/ChatIndicator';
import { useWebSocket } from '@/hooks/webSocket';
import { useWebSocket } from '@/hooks/useWebsocket';

export default function LiveLayout({
children,
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/app/live/video/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';

import React, { useRef, useState, useEffect } from 'react';
import { useWebSocket } from '@/hooks/webSocket';
import { useRef, useState, useEffect } from 'react';
import { useWebSocket } from '@/hooks/useWebsocket';
import { useGroupCall } from '@/hooks/useGroupCall';
import Button from '@/components/UI/Button';

Expand Down
8 changes: 3 additions & 5 deletions frontend/src/components/Chat/ChatBox.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
'use client';

import React, { useState, useEffect, useRef } from 'react';
import { useWebSocket } from '@/hooks/webSocket';
import { useUserStore } from '@/hooks/userStore';
import { useState, useEffect, useRef } from 'react';
import { useWebSocket } from '@/hooks/useWebsocket';
import { useUserStore } from '@/hooks/useUserStore';
import Input from '../UI/Input';
import Button from '../UI/Button';
import { msgError } from '@/types/wsTypes';
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/Chat/ChatWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
'use client';

import { useState } from 'react';
import { useUserStore } from '@/hooks/userStore';
import { useWSConnect } from '@/hooks/webSocket';
import { useUserStore } from '@/hooks/useUserStore';
import { useWSConnect } from '@/hooks/useWebsocket';
import LoginBox from './LoginBox';
import ChatBox from './ChatBox';
import Button from '../UI/Button';
Expand Down
6 changes: 2 additions & 4 deletions frontend/src/components/Chat/LoginBox.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
'use client';

import React, { useState } from 'react';
import { useUserStore } from '@/hooks/userStore'; // Adjust path if different
import { useState } from 'react';
import { useUserStore } from '@/hooks/useUserStore'; // Adjust path if different
import Input from '../UI/Input';
import Button from '../UI/Button';

Expand Down
5 changes: 1 addition & 4 deletions frontend/src/components/UI/ChatIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ import { Pointer } from 'lucide-react';
export default function ChatIndicator() {

return <div className='fixed bottom-15 right-15 z-50 animate-bounce'>
<Pointer
className='w-16 h-16 text-error'
style={{ transform: 'rotate(135deg)' }}
/>
<Pointer className='w-16 h-16 text-error rotate-135' />
</div>;
}
2 changes: 1 addition & 1 deletion frontend/src/components/UI/Dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client';

import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useState, useRef, useEffect, useCallback } from 'react';
import Link from 'next/link';
import Button from './Button';

Expand Down
1 change: 0 additions & 1 deletion frontend/src/components/UI/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import React from 'react';
import Link from 'next/link';
import Dropdown from './Dropdown';
import ThemeSwitcher from './ThemeSwitcher';
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/components/UI/ThemeSwitcher.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import React from 'react';
import { useThemeStore, Theme } from '@/hooks/themeStore';
import { useThemeStore, Theme } from '@/hooks/useTheme';

export default function ThemeSwitcher() {
const { theme, setTheme } = useThemeStore();
Expand Down
Loading