docs: add contributor and Colyseus guides

feat(client): parametrize WS/API via VITE_WS_URL and VITE_API_URL; add .env.example and README env section
This commit is contained in:
2025-08-07 17:10:08 -06:00
parent 5a273766a6
commit 1912b3a76f
5 changed files with 200 additions and 10 deletions

42
AGENTS.md Normal file
View File

@@ -0,0 +1,42 @@
# Repository Guidelines
## Project Structure & Module Organization
- `server/`: Colyseus + Express TypeScript backend (`src/index.ts`, rooms, schemas, utils, `adminApi.ts`).
- `client/`: Vue 3 + Vite TypeScript app (`src/views`, `services/colyseus.ts`, `router`, `main.ts`).
- `shared/`: Cross-cutting TypeScript types (`shared/types.ts`).
## Build, Test, and Development Commands
- From repo root:
- `npm run install:all`: Install client and server dependencies.
- `npm run dev`: Run server and client in watch mode.
- `npm run build`: Type-check and build server and client.
- Per package:
- Server: `cd server && npm run dev | build | start`.
- Client: `cd client && npm run dev | build | preview`.
## Coding Style & Naming Conventions
- Language: TypeScript (server/client). Indentation: 2 spaces.
- Quotes/Semicolons: double quotes and trailing semicolons (match existing code).
- Naming: `PascalCase` for classes and Vue SFC files (e.g., `Game.vue`), `camelCase` for variables/functions, `kebab-case` for non-component file names when appropriate.
- Keep server state changes server-authoritative; avoid mutating Colyseus state on client.
- No linter is configured; keep changes consistent with surrounding files.
## Testing Guidelines
- No tests yet. Prefer adding:
- Client: Vitest + Vue Test Utils for views/services.
- Server: Jest for room logic and schema helpers.
- Conventions: mirror source path and name tests `*.spec.ts` (e.g., `server/src/rooms/GameRoom.spec.ts`).
- CI-friendly: tests must run headless and not require a live server.
## Commit & Pull Request Guidelines
- Commits: follow Conventional Commits (`feat:`, `fix:`, `docs:`) as in history.
- PRs include:
- Clear description and rationale; link issues (e.g., `Fixes #123`).
- Screenshots/GIFs for UI changes (`client/src/views/*`).
- Steps to reproduce and test notes.
- Scope limited to one logical change; update README/QUICKSTART if behavior changes.
## Security & Configuration Tips
- Server port via `PORT` (default 3000). Monitor at `/colyseus`, REST at `/api`.
- Client WebSocket URL is set in `client/src/services/colyseus.ts`; consider env (`VITE_WS_URL`) for deployments.
- CORS is enabled; restrict origins in production.

117
COLYSEUS-GUIA.md Normal file
View File

@@ -0,0 +1,117 @@
# Colyseus: Guía de Integración para Snatchgame
Esta guía resume las APIs de Colyseus necesarias para: un lobby unificado, salas de juego (máx. 2 jugadores) y un dashboard para inspección/acciones por sala y globales.
## Conceptos Clave
- Rooms: unidades de ejecución con ciclo de vida (`onCreate`, `onJoin`, `onLeave`, `onDispose`).
- Schema: sincroniza estado autoritativo del servidor a clientes con `@type`.
- Mensajería: `room.onMessage(type, handler)`, `client.send(type, payload)`, `room.broadcast(type, payload)`.
- Matchmaker: descubrimiento/control de salas (`matchMaker.query`, `remoteRoomCall`, `stats`).
- Monitor: panel listo para usar en Express (`@colyseus/monitor`).
## Lobby Unificado
- Definir una sala pública única `lobby` que gestione nombres, matchmaking y listado.
- API útil: `gameServer.define("lobby", LobbyRoom)`, `setPrivate(false)`, `enableRealtimeListing()` en salas de juego para listar.
- Mensajes recomendados: `setName`, `quickPlay`, `joinRoom`.
Ejemplo (server/src/rooms/LobbyRoom.ts):
```ts
export class LobbyRoom extends Room<LobbyState> {
onCreate() {
this.setState(new LobbyState());
this.onMessage("setName", (client, name) => {/* validar + asignar */});
this.onMessage("quickPlay", async (client) => {
// Crear o elegir una sala "game" con cupo
const rooms = await matchMaker.query({ name: "game", locked: false });
const target = rooms.find(r => r.clients < r.maxClients) ||
await matchMaker.createRoom("game", { maxClients: 2 });
client.send("gameJoined", { roomId: target.roomId || target.roomId });
});
}
}
```
## Game Rooms (2 jugadores)
- Definición: `gameServer.define("game", GameRoom).filterBy(["maxClients"]).enableRealtimeListing()`.
- Capacidad: `maxClients = 2;` en la clase de la sala.
- Re-conexión: `await this.allowReconnection(client, 30);` para tolerancia a fallos.
- Ticks: usar `this.setSimulationInterval((deltaTime) => { /* actualizar timer */ }, 1000);`.
Ejemplo (server/src/rooms/GameRoom.ts):
```ts
export class GameRoom extends Room<GameState> {
maxClients = 2;
onCreate() {
this.setState(new GameState());
this.onMessage("click", (client) => {/* sumar clicks si PLAYING */});
}
onJoin(client, { playerName }) {
this.state.addPlayer(client.sessionId, playerName);
if (this.state.players.size === 2) this.state.startGame();
}
onLeave(client) {
this.state.players.get(client.sessionId)!.connected = false;
this.allowReconnection(client, 30);
}
}
```
## Estado Autoritativo (Schema)
- Definir modelos con `@type` y colecciones `MapSchema`/`ArraySchema`.
- Patrón: solo el servidor muta estado; clientes envían mensajes de intención.
Ejemplo:
```ts
export class GameState extends Schema {
@type({ map: Player }) players = new MapSchema<Player>();
@type("string") gameStatus: GameStatus = GameStatus.WAITING;
@type("number") timeRemaining = 600;
}
```
## Dashboard y Control Administrativo
- Monitor integrado: `app.use("/colyseus", monitor());` para ver salas/CCU.
- REST/Admin personalizado vía `matchMaker`:
- Listar: `await matchMaker.query({ name: "game" });`
- Llamar métodos remotos: `await matchMaker.remoteRoomCall(roomId, "getState");`
- Acciones por sala: `await matchMaker.remoteRoomCall(roomId, "broadcast", ["admin:pause"]);`
Ejemplos (server/src/adminApi.ts):
```ts
router.get("/rooms", async (_, res) => {
const rooms = await matchMaker.query({});
res.json(rooms);
});
router.post("/rooms/:roomId/restart", async (req, res) => {
await matchMaker.remoteRoomCall(req.params.roomId, "broadcast", ["admin:restart"]);
res.json({ success: true });
});
```
Acciones globales (todas las salas "game"):
```ts
const rooms = await matchMaker.query({ name: "game" });
await Promise.all(rooms.map(r => matchMaker.remoteRoomCall(r.roomId, "broadcast", ["admin:pause"])));
```
## Buenas Prácticas
- Validar inputs en `onMessage` y nunca confiar en el cliente.
- Usar `metadata`/`filterBy` para matchmaking por atributos.
- Mantener `maxClients` y reglas de inicio/pausa dentro del servidor.
- Exponer solo métodos necesarios para `remoteRoomCall` (p. ej., `getState`).
Referencias: documentación oficial Colyseus (Rooms, Schema, Matchmaker, Monitor).
## Configuración de WebSocket (Cliente)
- Variable de entorno en Vite: `VITE_WS_URL`.
- Si no se define, el cliente usa `ws(s)://<hostname>:3000` según el protocolo actual.
- Ejemplos:
- Desarrollo local: `VITE_WS_URL=ws://localhost:3000`
- Producción (TLS): `VITE_WS_URL=wss://api.midominio.com`
## Configuración de REST API (Cliente)
- Variable de entorno en Vite: `VITE_API_URL` con la base completa del API (incluye `/api`).
- Si no se define, el cliente usa `http(s)://<hostname>:3000/api` según el protocolo actual.
- Ejemplos:
- Desarrollo local: `VITE_API_URL=http://localhost:3000/api`
- Producción (TLS): `VITE_API_URL=https://api.midominio.com/api`

View File

@@ -188,6 +188,17 @@ This will start:
- Server on http://localhost:3000
- Client on http://localhost:5173
### Environment Variables (Client)
Create `client/.env` (or copy `client/.env.example`) to configure endpoints:
```
VITE_WS_URL=ws://localhost:3000
VITE_API_URL=http://localhost:3000/api
```
For production over HTTPS, use `wss://` for `VITE_WS_URL` and an HTTPS API base for `VITE_API_URL`.
## URLs
- **Game**: http://localhost:5173
@@ -247,4 +258,4 @@ The admin dashboard provides:
- `POST /api/rooms/:roomId/resume` - Resume a game
- `POST /api/rooms/:roomId/restart` - Restart a game
- `POST /api/rooms/:roomId/kick/:playerId` - Kick a player
- `GET /api/stats` - Get global server statistics
- `GET /api/stats` - Get global server statistics

10
client/.env.example Normal file
View File

@@ -0,0 +1,10 @@
# Client environment variables (copy to client/.env)
# WebSocket endpoint for Colyseus server
# e.g., ws://localhost:3000 (dev) or wss://api.my-domain.com (prod)
VITE_WS_URL=ws://localhost:3000
# REST API base (must include /api)
# e.g., http://localhost:3000/api (dev) or https://api.my-domain.com/api (prod)
VITE_API_URL=http://localhost:3000/api

View File

@@ -23,6 +23,7 @@ export interface AvailableRoom {
class ColyseusService {
private client: Client;
private currentRoom: Room | null = null;
private apiBase: string;
public lobbyRoom: Ref<Room | null> = ref(null);
public gameRoom: Ref<Room | null> = ref(null);
@@ -30,7 +31,16 @@ class ColyseusService {
public sessionId: Ref<string> = ref("");
constructor() {
this.client = new Client("ws://localhost:3000");
const defaultHost = typeof window !== "undefined" ? window.location.hostname : "localhost";
const defaultProtocol = typeof window !== "undefined" && window.location.protocol === "https:" ? "wss" : "ws";
const defaultPort = 3000;
const fallbackUrl = `${defaultProtocol}://${defaultHost}:${defaultPort}`;
const url = import.meta.env.VITE_WS_URL || fallbackUrl;
this.client = new Client(url);
const httpProtocol = typeof window !== "undefined" && window.location.protocol === "https:" ? "https" : "http";
const apiFallback = `${httpProtocol}://${defaultHost}:${defaultPort}/api`;
this.apiBase = (import.meta.env as any).VITE_API_URL || apiFallback;
}
async joinLobby(): Promise<Room> {
@@ -170,7 +180,7 @@ class ColyseusService {
async fetchRooms(): Promise<any[]> {
try {
const response = await fetch("http://localhost:3000/api/rooms");
const response = await fetch(`${this.apiBase}/rooms`);
return await response.json();
} catch (error) {
console.error("Failed to fetch rooms:", error);
@@ -180,7 +190,7 @@ class ColyseusService {
async fetchRoomStats(roomId: string): Promise<any> {
try {
const response = await fetch(`http://localhost:3000/api/rooms/${roomId}/stats`);
const response = await fetch(`${this.apiBase}/rooms/${roomId}/stats`);
return await response.json();
} catch (error) {
console.error("Failed to fetch room stats:", error);
@@ -189,24 +199,24 @@ class ColyseusService {
}
async pauseRoom(roomId: string): Promise<void> {
await fetch(`http://localhost:3000/api/rooms/${roomId}/pause`, { method: "POST" });
await fetch(`${this.apiBase}/rooms/${roomId}/pause`, { method: "POST" });
}
async resumeRoom(roomId: string): Promise<void> {
await fetch(`http://localhost:3000/api/rooms/${roomId}/resume`, { method: "POST" });
await fetch(`${this.apiBase}/rooms/${roomId}/resume`, { method: "POST" });
}
async restartRoom(roomId: string): Promise<void> {
await fetch(`http://localhost:3000/api/rooms/${roomId}/restart`, { method: "POST" });
await fetch(`${this.apiBase}/rooms/${roomId}/restart`, { method: "POST" });
}
async kickPlayer(roomId: string, playerId: string): Promise<void> {
await fetch(`http://localhost:3000/api/rooms/${roomId}/kick/${playerId}`, { method: "POST" });
await fetch(`${this.apiBase}/rooms/${roomId}/kick/${playerId}`, { method: "POST" });
}
async fetchGlobalStats(): Promise<any> {
try {
const response = await fetch("http://localhost:3000/api/stats");
const response = await fetch(`${this.apiBase}/stats`);
return await response.json();
} catch (error) {
console.error("Failed to fetch global stats:", error);
@@ -215,4 +225,4 @@ class ColyseusService {
}
}
export const colyseusService = new ColyseusService();
export const colyseusService = new ColyseusService();