Skip to content
Draft
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
296 changes: 161 additions & 135 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions srcs/.env.game.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

GAME_DB_PATH=/data/game.db
2 changes: 2 additions & 0 deletions srcs/game/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@
},
"dependencies": {
"alea": "^1.0.1",
"better-sqlite3": "^12.4.1",
"fastify": "^5.6.2",
"@fastify/websocket": "^11.0.0",
"simplex-noise": "^4.0.3",
"ws": "^8.18.0"
},
"devDependencies": {
"@types/node": "^20.14.10",
"@types/better-sqlite3": "^7.6.13",
"@types/ws": "^8.5.12",
"tsx": "^4.16.2",
"typescript": "^5.5.3"
Expand Down
5 changes: 5 additions & 0 deletions srcs/game/src/controllers/game.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,8 @@ export async function webSocketConnect(
// }
handleClientMessage.call(this, socket, sessionId);
}

export async function newTournament() {
// const tournament_id = createTournament();
// return reply.code(200).send(tournament_id);
}
140 changes: 140 additions & 0 deletions srcs/game/src/core/game.database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import Database from 'better-sqlite3';
import fs from 'fs';
import path from 'path';
import { MatchDTO } from '../types/game.dto.js';

// DB path
const DEFAULT_DIR = path.join(process.cwd(), 'data');
const DB_PATH = process.env.GAME_DB_PATH || path.join(DEFAULT_DIR, 'game.db');

// Check dir
try {
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
} catch (err) {
const e: any = new Error(
`Failed to ensure DB directory: ${(err as any)?.message || String(err)}`,
);
throw e;
}

export const db = new Database(DB_PATH);
console.log('Using SQLite file:', DB_PATH);

// Create table
try {
db.exec(`
CREATE TABLE IF NOT EXISTS match(
id INTEGER PRIMARY KEY AUTOINCREMENT,
tournament_id INTEGER, -- NULL if free match
player1 INTEGER NOT NULL,
player2 INTEGER NOT NULL,
score_player1 INTEGER NOT NULL DEFAULT 0,
score_player2 INTEGER NOT NULL DEFAULT 0,
winner_id INTEGER NOT NULL,
round TEXT, --NULL | SEMI_1 | SEMI_2 | LITTLE_FINAL | FINAL
created_at INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS tournament(
id INTEGER PRIMARY KEY AUTOINCREMENT,
creator_id INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT 'PENDING', -- PENDING | STARTED | FINISHED
created_at INTEGER NOT NULL
);

CREATE TABLE IF NOT EXISTS tournament_player(
tournament_id INTEGER NOT NULL,
player_id INTEGER NOT NULL,
final_position INTEGER,
PRIMARY KEY (tournament_id, player_id)
);

CREATE INDEX IF NOT EXISTS idx_match_tournament
ON match(tournament_id);

CREATE INDEX IF NOT EXISTS idx_tournament_player_tid
ON tournament_player(tournament_id);
`);
} catch (err) {
const e: any = new Error(
`Failed to initialize DB schema: ${(err as any)?.message || String(err)}`,
);
throw e;
}

const addMatchStmt = db.prepare(`
INSERT INTO match(tournament_id, player1, player2, score_player1, score_player2, winner_id, created_at)
VALUES (?,?,?,?,?,?,?)
`);

const createTournamentStmt = db.prepare(`
INSERT INTO tournament(creator_id, created_at)
VALUES (?,?)
`);

const addPlayerTournamentStmt = db.prepare(`
INSERT INTO tournament_player(player_id, tournament_id)
VALUES(?,?)
`);

const addPlayerPositionTournamentStmt = db.prepare(`
UPDATE tournament_player
SET
final_position = ?
WHERE tournament_id = ? and player_id = ?
`);

export function addMatch(match: MatchDTO): number {
try {
const idmatch = addMatchStmt.run(
match.tournament_id,
match.player1,
match.player2,
match.score_player1,
match.score_player2,
match.winner_id,
match.created_at,
);
return Number(idmatch.lastInsertRowid);
} catch (err: any) {
const error: any = new Error(`Error during Match storage: ${err?.message || err}`);
error.code = 'DB_INSERT_MATCH_ERR';
throw error;
}
}

export function createTournament(player: number): number {
try {
const idtournament = createTournamentStmt.run(player, Date.now());
return Number(idtournament.lastInsertRowid);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
const error = new Error(`Tournament creation failed: ${message}`) as Error & { code: string };
error.code = 'DB_INSERT_TOURNAMENT_ERR';
throw error;
}
}

export function addPlayerTournament(player: number, tournament: number) {
try {
addPlayerTournamentStmt.run(player, tournament);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
const error = new Error(`Add Player to a tournament failed: ${message}`) as Error & {
code: string;
};
error.code = 'DB_UPDATE_TOURNAMENT_ERROR';
throw error;
}
}

export function addPlayerPositionTournament(player: number, position: number, tournament: number) {
try {
addPlayerPositionTournamentStmt.run(position, tournament, player);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
const error = new Error(`Add player position failed: ${message}`) as Error & { code: string };
error.code = 'DB_UPDATE_PLAYER_POSITION';
throw error;
}
}
2 changes: 2 additions & 0 deletions srcs/game/src/routes/game.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
newGameSession,
healthCheck,
gameSettings,
newTournament,
} from '../controllers/game.controller.js';

export async function gameRoutes(app: FastifyInstance) {
Expand All @@ -13,4 +14,5 @@ export async function gameRoutes(app: FastifyInstance) {
app.post('/create-session', newGameSession);
app.get('/health', healthCheck);
app.get('/:sessionId', { websocket: true }, webSocketConnect);
app.get('/create-tournament', newTournament);
}
14 changes: 14 additions & 0 deletions srcs/game/src/types/game.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* ===========================
* Match DB
* =========================== */
export interface MatchDTO {
id: number;
tournament_id: number | null;
player1: number;
player2: number;
score_player1: number;
score_player2: number;
winner_id: number;
round: string | null;
created_at: number;
}
28 changes: 21 additions & 7 deletions srcs/nginx/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,33 @@ import { ProfilePage } from './pages/ProfilePage';
import { LoginPage } from './pages/LoginRegisterPage';
import { useAuth } from './providers/AuthProvider';
import { AnimationPage } from './pages/AnimationPage';
import TournamentRoutes from './router/TournamentRoutes';

const GuestRoute = ({ children }: { children: React.ReactNode }) => {
const { user, isLoggedIn } = useAuth();
if (user && isLoggedIn) {
const { user, isLoggedIn, isAuthChecked } = useAuth();

if (!isAuthChecked) {
return null; // ou loader
}

if (isLoggedIn && user?.username) {
return <Navigate to={`/profile/${user.username}`} replace />;
}

return children;
};

const MeRedirect = () => {
const { user } = useAuth();
// if (isLoading) return <div>Loading ...</div>;
if (!user) return <Navigate to="/" replace />;
return <Navigate to={`/profile/${user.username}`}></Navigate>;
const { user, isAuthChecked } = useAuth();

if (!isAuthChecked) {
return null; // ou loader
}

if (!user || !user.username) {
return <Navigate to="/" replace />;
}

return <Navigate to={`/profile/${user.username}`} replace />;
};

export const App = () => {
Expand All @@ -42,6 +55,7 @@ export const App = () => {
/>
<Route path="/me" element={<MeRedirect />}></Route>
<Route path="/profile/:username" element={<ProfilePage />}></Route>
<Route path="/tournaments/*" element={<TournamentRoutes />} />
</Routes>
</main>
);
Expand Down
19 changes: 17 additions & 2 deletions srcs/nginx/src/api/auth-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,23 @@ export const authApi = {
return data?.user?.username;
},

// me: async (): Promise<UserDTO> => {
// usernameSchema.parse(username);
// // const response = await api.get(`/auth/me/`);
// const response = {
// data: {
// authId: 1,
// email: 'toto@mail.com',
// username: 'Toto',
// },
// message: 'OK',
// };
// return response.data;
// },
me: async (): Promise<UserDTO> => {
const response = await api.get(`/auth/me/`);
return response.data;
const { data } = await api.get('/auth/me', {
withCredentials: true,
});
return data;
},
};
99 changes: 99 additions & 0 deletions srcs/nginx/src/components/atoms/BracketLines.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { RefObject, useEffect, useState } from 'react';

export interface BracketConnection {
from: RefObject<HTMLElement | null>;
to: RefObject<HTMLElement | null>;
}

interface Point {
x: number;
y: number;
}

interface ComputedLine {
from: Point;
to: Point;
}

interface BracketLinesProps {
// coordinate reference
containerRef: RefObject<HTMLElement | null>;
connections: BracketConnection[];
}

/* renvoi les coordonnées du centre de l'objet / div
* car getBoundingClientRect renvoie les coordonnées viewport du rectangle
* cette fontion utilitaire permet aux lignes de partir du centre des capsules
*/
function centerOf(rect: DOMRect): Point {
return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 };
}

/* BracketLines est un composant de rendu SVG qui dessine dynamiquement des lignes
* entre des éléments du DOM, en restant synchronisé avec le layout réel
* (responsive, scroll, resize).
*/
export function BracketLines({ containerRef, connections }: BracketLinesProps) {
const [lines, setLines] = useState<ComputedLine[]>([]);

useEffect(() => {
const compute = () => {
const container = containerRef.current;
if (!container) return;
// on récupère les coordonnées du container
const cRect = container.getBoundingClientRect();

const computed: ComputedLine[] = [];

for (const { from, to } of connections) {
const aEl = from.current;
const bEl = to.current;
if (!aEl || !bEl) continue;

const a = centerOf(aEl.getBoundingClientRect());
const b = centerOf(bEl.getBoundingClientRect());

//convert viewport -> container local coords
computed.push({
from: { x: a.x - cRect.left, y: a.y - cRect.top },
to: { x: b.x - cRect.left, y: b.y - cRect.top },
});
}

setLines(computed);
};

/*recalcul : au montage, au resize, au scroll (y compris scrolls internes)*/
compute();

window.addEventListener('resize', compute);
window.addEventListener('scroll', compute, true); // capte scroll de conteneurs
// évite les fuite de mémoire
return () => {
window.removeEventListener('resize', compute);
window.removeEventListener('scroll', compute, true);
};
}, [containerRef, connections]);

return (
<svg className="absolute inset-0 w-full h-full pointer-events-none">
{lines.map((line, i) => {
// courbe de bézier cubique
const cx = (line.from.x + line.to.x) / 2;

return (
<path
key={i}
d={`M ${line.from.x} ${line.from.y}
C ${cx} ${line.from.y},
${cx} ${line.to.y},
${line.to.x} ${line.to.y}`}
stroke="rgba(125, 211, 252, 0.9)"
strokeWidth="2"
fill="none"
/>
);
})}
</svg>
);
}
Loading
Loading