A real-time multiplayer game platform built with Svelte, TypeScript, and PeerJS. Features multiple strategic game modes with peer-to-peer connectivity, no central server required.
DEMO: https://lonsdale201.github.io/sveltethegamer/
git clone https://github.com/YOUR_USERNAME/shadow-games-p2p.git
cd shadow-games-p2p
# Install dependencies
npm install
# Start development server
npm run dev
# Build for production
npm run build
# Preview production build
npm run preview
# Type checking
npm run check
The development server will start at http://localhost:5173
src/
āāā components/ # Svelte components
ā āāā Lobby.svelte # Main lobby for creating/joining games
ā āāā PreLobby.svelte # Pre-game lobby with player info
āāā config/ # Configuration files
ā āāā debug.ts # Debug logging system
āāā core/ # Core game management
ā āāā GameManager.ts # Main game state management
ā āāā PeerConnection.ts # WebRTC peer-to-peer connection
ā āāā TurnManager.ts # Turn management system
āāā gameModes/ # Game mode implementations
ā āāā index.ts # Game mode registry
ā āāā colorDuel/ # Color Duel game mode
ā āāā towerWar/ # Tower War game mode
ā āāā shadowCode/ # Shadow Code game mode
ā āāā brainstorming/ # Brainstorming quiz game mode
āāā types/ # TypeScript type definitions
āāā utils/ # Utility functions
ā āāā sanitizer.ts # Input sanitization
ā āāā storage.ts # localStorage utilities
āāā main.ts # Application entry point
src/core/GameManager.ts
)src/core/PeerConnection.ts
)src/core/TurnManager.ts
)src/gameModes/
)*Board.svelte
: UI component for the game*Logic.ts
: Game rules and state managementsrc/types/
The application now automatically saves and loads player names using localStorage:
import { savePlayerName, loadPlayerName, clearPlayerName } from '../utils/storage';
// Save player name (automatically called when name is set)
savePlayerName('PlayerName');
// Load saved name (automatically called on app start)
const savedName = loadPlayerName();
// Clear saved name
clearPlayerName();
Features:
A flexible system that supports different turn modes:
Used by Color Duel, Tower War, and Shadow Code:
// Players take turns one after another
// Only the current player can make moves
// Turn automatically switches after each action
Used by Brainstorming:
// Both players can act at the same time
// Game waits for both players to submit actions
// Automatic synchronization and feedback phases
// In game logic
export function makeMove(gameState: GameState, moveData: any, player: Player): GameState {
return TurnManager.handlePlayerAction(gameState, player, (state) => {
// Your game-specific logic here
const newState = { ...state };
// Apply the move
return newState;
});
}
src/config/debug.ts
)A centralized debug logging system that can be toggled globally:
import { debugLog, debugError, debugWarn } from '../config/debug';
// Usage throughout the codebase
debugLog('Game state updated:', gameState);
debugError('Connection failed:', error);
debugWarn('Invalid move attempted:', moveData);
Configuration:
DEBUG_MODE = true
in src/config/debug.ts
for developmentDEBUG_MODE = false
for production buildsEnhanced peer-to-peer reliability with automatic connection monitoring:
Flexible configuration system for game modes with automatic UI generation:
// In game mode definition
settingsDisplay: {
turnTimer: {
label: 'Turn Timer',
getValue: (settings) => settings.turnTimeLimit === 0 ? 'Unlimited' : `${settings.turnTimeLimit}s`,
icon: 'ā±ļø'
},
targetScore: {
label: 'Target Score',
getValue: (settings) => `${settings.brainstormingSettings?.targetScore ?? 10} points to win`,
icon: 'šÆ'
}
}
Features:
The Brainstorming mode includes a comprehensive question system:
Edit src/gameModes/brainstorming/questions.ts
:
// Hungarian Questions
const hungarianQuestions: Question[] = [
{
id: 'hu-new',
text: 'Your question in Hungarian?',
type: 'select', // or 'number'
language: 'HU',
options: ['Option 1', 'Option 2', 'Option 3', 'Option 4'], // for select type
correctAnswer: 'Option 1', // or number for number type
exactPoints: 1, // points for correct answer
closePoints: 1 // optional: points for close answer (number type only)
}
];
// English Questions
const englishQuestions: Question[] = [
{
id: 'en-new',
text: 'Your question in English?',
type: 'number',
language: 'EN',
correctAnswer: 42,
exactPoints: 2,
closePoints: 1 // within 10% tolerance
}
];
Multiple Choice (select
):
Number Input (number
):
id
in the questions arrayid
is not referenced elsewhereCreate src/types/yourGameMode.ts
:
import type { BaseGameState, Player } from './core';
// Define your game-specific state
export interface YourGameState extends BaseGameState {
// Add your game-specific properties
customProperty: any;
}
// Define move data structure
export interface YourMoveData {
action: string;
player: Player;
// Add other move-specific data
}
// Export initial state
export const initialYourGameState: YourGameState = {
// Initialize your game state
customProperty: defaultValue,
// Required base properties
gameStarted: false,
currentTurn: 'red',
winner: null,
turnTimeLimit: 0,
turnStartTime: 0,
timeRemaining: 0,
turnState: undefined, // Will be initialized by TurnManager
};
Create src/gameModes/yourGameMode/YourGameLogic.ts
:
import { debugLog } from '../../config/debug';
import { TurnManager } from '../../core/TurnManager';
import type { YourGameState, YourMoveData } from '../../types/yourGameMode';
import type { GameSettings } from '../../types/core';
// Check if a player has won
export function checkWinner(gameState: YourGameState): Player | null {
// Implement win condition logic
return null;
}
// Validate if a move is legal
export function canMakeMove(gameState: YourGameState, moveData: any, player: Player): boolean {
debugLog('YourGame canMakeMove:', { moveData, player, gameState });
// Use TurnManager for turn validation
if (!TurnManager.canPlayerAct(gameState, player)) {
return false;
}
// Implement game-specific move validation
return true;
}
// Execute a move and return new state
export function makeMove(gameState: YourGameState, moveData: any, player: Player): YourGameState {
debugLog('YourGame makeMove:', { moveData, player });
// Use TurnManager to handle the move
return TurnManager.handlePlayerAction(gameState, player, (state) => {
// Implement your game-specific logic here
const newState = { ...state };
// Update game state based on move
newState.winner = checkWinner(newState);
return newState;
});
}
// Initialize game state
export function resetGame(gameSettings: GameSettings): YourGameState {
// Determine turn mode for your game
const turnMode = 'sequential'; // or 'simultaneous'
return {
...initialYourGameState,
turnTimeLimit: gameSettings.turnTimeLimit,
gameStarted: true,
turnState: TurnManager.initializeTurnState(turnMode),
};
}
// Handle turn timeout
export function skipTurn(gameState: YourGameState): YourGameState {
return TurnManager.handleTurnTimeout(gameState, (state) => {
// Handle timeout-specific logic (e.g., skip player's turn)
const newState = { ...state };
// Your timeout handling logic here
return newState;
});
}
Create src/gameModes/yourGameMode/YourGameBoard.svelte
:
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import { debugLog } from '../../config/debug';
import type { YourGameState } from '../../types/yourGameMode';
import type { PlayerInfo, GameSettings } from '../../types/core';
import { canMakeMove } from './YourGameLogic';
export let gameState: YourGameState;
export let myColor: Player;
export let connected: boolean;
export let myPlayerInfo: PlayerInfo;
export let opponentInfo: PlayerInfo | null;
export let gameSettings: GameSettings;
const dispatch = createEventDispatcher();
function handleMove(moveData: any) {
if (canMakeMove(gameState, moveData, myColor)) {
debugLog('YourGame dispatching move:', moveData);
dispatch('move', moveData);
}
}
function handleReset() {
dispatch('reset');
}
</script>
<!-- Your game UI here -->
<div class="game-container">
<!-- Implement your game interface -->
</div>
<style>
/* Your game styles */
</style>
Update src/gameModes/index.ts
:
import YourGameBoard from './yourGameMode/YourGameBoard.svelte';
import * as YourGameLogic from './yourGameMode/YourGameLogic';
import { initialYourGameState } from '../types/yourGameMode';
export const gameModes: GameMode[] = [
// ... existing game modes
{
id: 'your-game-mode',
name: 'Your Game Name',
description: 'Brief description of your game',
component: YourGameBoard,
initialState: () => ({ ...initialYourGameState }),
gameLogic: YourGameLogic,
turnMode: 'sequential', // or 'simultaneous'
settingsDisplay: {
// Optional: Define settings that should appear in UI
customSetting: {
label: 'Custom Setting',
getValue: (settings) => `${settings.yourCustomValue}`,
icon: 'āļø'
}
}
}
];
Perfect for games where players alternate moves:
// Example: Color Duel, Tower War, Shadow Code
export function makeMove(gameState: GameState, moveData: any, player: Player): GameState {
return TurnManager.handlePlayerAction(gameState, player, (state) => {
// Apply the move
const newState = { ...state };
newState.board[moveData.x][moveData.y] = player;
// Check for winner
newState.winner = checkWinner(newState);
return newState;
// TurnManager automatically switches turns
});
}
Perfect for games where both players act at the same time:
// Example: Brainstorming quiz
export function makeMove(gameState: GameState, moveData: any, player: Player): GameState {
return TurnManager.handlePlayerAction(gameState, player, (state) => {
// Record player's action
const newState = { ...state };
newState.playerAnswers[player] = moveData.answer;
// Check if both players have acted
const bothActed = newState.playerAnswers.red && newState.playerAnswers.blue;
if (bothActed) {
// Process both actions together
newState.showingResults = true;
// TurnManager will reset for next round
}
return newState;
});
}
For games with different phases:
// Example: Shadow Code (setup phase is simultaneous, guessing is sequential)
export function makeMove(gameState: GameState, moveData: any, player: Player): GameState {
if (moveData.type === 'setCode') {
// Setup phase - simultaneous
return TurnManager.handlePlayerAction(gameState, player, (state) => {
const newState = { ...state };
newState.playerCodes[player] = moveData.code;
// Switch to sequential mode when both codes are set
if (newState.playerCodes.red && newState.playerCodes.blue) {
newState.turnState.mode = 'sequential';
newState.gameStarted = true;
}
return newState;
});
} else {
// Guessing phase - sequential
return TurnManager.handlePlayerAction(gameState, player, (state) => {
// Handle guess logic
return state;
});
}
}
Update src/types/core.ts
:
export interface GameSettings {
turnTimeLimit: number;
gameMode: string;
turnMode?: TurnMode;
// Add your new settings
yourGameSettings?: {
customSetting: number;
anotherSetting: boolean;
};
}
Update src/components/Lobby.svelte
to add setting controls:
<!-- Add in the game mode settings section -->
{#if selectedGameMode === 'your-game-mode'}
<div class="your-game-setting">
<label for="customSetting">Custom Setting:</label>
<input
id="customSetting"
type="number"
bind:value={customSettingValue}
min="1"
max="100"
class="timer-input"
/>
</div>
{/if}
Add to your game mode definition:
settingsDisplay: {
customSetting: {
label: 'Custom Setting',
getValue: (settings) => `${settings.yourGameSettings?.customSetting ?? 10}`,
icon: 'āļø'
}
}
The settings will automatically appear in the PreLobby component with the specified label, icon, and computed value.
console.log
with debugLog
, debugError
, or debugWarn
DEBUG_MODE = false
for production builds*Logic.ts
filesThis project is automatically deployed to GitHub Pages when changes are pushed to the main branch.
Live Demo: https://YOUR_USERNAME.github.io/mygamename/
The GitHub Actions workflow will automatically:
npm run build
DEBUG_MODE = false
in productioncanMakeMove
logicnpm run check
for type checkingDEBUG_MODE
for troubleshootingThis project is open source. Feel free to use, modify, and distribute according to your needs.
For questions, issues, or contributions: