Skip to content

Commit a3a3ba4

Browse files
committedMar 18, 2025
working
0 parents  commit a3a3ba4

22 files changed

+3562
-0
lines changed
 

‎.gitignore

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Dependencies
2+
node_modules
3+
.pnpm-store
4+
5+
# Build outputs
6+
dist
7+
build
8+
out
9+
10+
# Environment files
11+
.env
12+
.env.local
13+
.env.development.local
14+
.env.test.local
15+
.env.production.local
16+
17+
# IDE and editor files
18+
.vscode/*
19+
!.vscode/extensions.json
20+
.idea
21+
*.swp
22+
*.swo
23+
.DS_Store
24+
25+
# Debug logs
26+
npm-debug.log*
27+
yarn-debug.log*
28+
yarn-error.log*
29+
pnpm-debug.log*
30+
31+
# TypeScript
32+
*.tsbuildinfo
33+
34+
# Testing
35+
coverage
36+
37+
# Misc
38+
.cache
39+
.temp
40+
*.log

‎index.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>MathL33t - 3D Math Learning Game</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/client/index.tsx"></script>
12+
</body>
13+
</html>

‎package.json

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"name": "mathl33t",
3+
"version": "0.0.1",
4+
"type": "module",
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "tsc && vite build",
8+
"preview": "vite preview",
9+
"server": "tsx watch src/server/index.ts"
10+
},
11+
"keywords": [],
12+
"author": "Wes Bos (https://wesbos.com/)",
13+
"license": "MIT",
14+
"description": "",
15+
"packageManager": "pnpm@9.10.0+sha1.216899f511c8dfde183c7cb50b69009c779534a8",
16+
"dependencies": {
17+
"@hono/node-server": "^1.8.2",
18+
"@react-three/drei": "^9.102.3",
19+
"@react-three/fiber": "^8.15.19",
20+
"@react-three/rapier": "^1.3.0",
21+
"hono": "^4.1.3",
22+
"react": "^18.2.0",
23+
"react-dom": "^18.2.0",
24+
"three": "^0.162.0",
25+
"ws": "^8.16.0",
26+
"zustand": "^4.5.2"
27+
},
28+
"devDependencies": {
29+
"@types/react": "^18.2.64",
30+
"@types/react-dom": "^18.2.21",
31+
"@types/three": "^0.162.0",
32+
"@types/ws": "^8.5.10",
33+
"@vitejs/plugin-react": "^4.2.1",
34+
"autoprefixer": "^10.4.21",
35+
"postcss": "^8.5.3",
36+
"tailwindcss": "^3.4.17",
37+
"tsx": "^4.7.1",
38+
"typescript": "^5.4.2",
39+
"vite": "^5.1.5"
40+
}
41+
}

‎pnpm-lock.yaml

+2,807
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎postcss.config.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export default {
2+
plugins: {
3+
tailwindcss: {},
4+
autoprefixer: {},
5+
},
6+
}

‎src/client/App.tsx

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from 'react';
2+
import { Canvas } from '@react-three/fiber';
3+
import { Physics } from '@react-three/rapier';
4+
import { KeyboardControls } from '@react-three/drei';
5+
import GameScene from './game/GameScene';
6+
import HUD from './components/HUD';
7+
import { useGameStore } from './store/gameStore';
8+
9+
type Controls = {
10+
forward: boolean;
11+
backward: boolean;
12+
left: boolean;
13+
right: boolean;
14+
jump: boolean;
15+
};
16+
17+
export default function App() {
18+
const isLoggedIn = useGameStore(state => state.isLoggedIn);
19+
20+
if (!isLoggedIn) {
21+
return (
22+
<div className="flex items-center justify-center h-screen bg-gradient-to-b from-blue-500 to-purple-600">
23+
<div className="bg-white p-8 rounded-lg shadow-xl">
24+
<h1 className="text-3xl font-bold mb-4 text-center">MathL33t</h1>
25+
<button
26+
className="bg-blue-500 text-white px-6 py-2 rounded-full hover:bg-blue-600 transition"
27+
onClick={() => useGameStore.setState({ isLoggedIn: true })}
28+
>
29+
Start Playing
30+
</button>
31+
</div>
32+
</div>
33+
);
34+
}
35+
36+
return (
37+
<KeyboardControls<Controls>
38+
map={[
39+
{ name: 'forward', keys: ['ArrowUp', 'w', 'W'] },
40+
{ name: 'backward', keys: ['ArrowDown', 's', 'S'] },
41+
{ name: 'left', keys: ['ArrowLeft', 'a', 'A'] },
42+
{ name: 'right', keys: ['ArrowRight', 'd', 'D'] },
43+
{ name: 'jump', keys: ['Space'] },
44+
]}
45+
>
46+
<Canvas
47+
shadows
48+
camera={{ position: [0, 5, 10], fov: 75 }}
49+
className="w-screen h-screen"
50+
>
51+
<Physics>
52+
<GameScene />
53+
</Physics>
54+
</Canvas>
55+
<HUD />
56+
</KeyboardControls>
57+
);
58+
}

‎src/client/components/HUD.tsx

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import React, { useState } from 'react';
2+
import { useGameStore } from '../store/gameStore';
3+
4+
export default function HUD() {
5+
const score = useGameStore(state => state.score);
6+
const streak = useGameStore(state => state.streak);
7+
const currentProblem = useGameStore(state => state.currentProblem);
8+
const [selectedAnswer, setSelectedAnswer] = useState<number | null>(null);
9+
10+
const handleAnswer = (option: number) => {
11+
setSelectedAnswer(option);
12+
13+
if (option === currentProblem?.answer) {
14+
// Play success sound
15+
new Audio('/sounds/correct.mp3').play().catch(() => {});
16+
// Update score after delay
17+
setTimeout(() => {
18+
useGameStore.setState(state => ({
19+
score: state.score + 10,
20+
streak: state.streak + 1,
21+
currentProblem: null
22+
}));
23+
setSelectedAnswer(null);
24+
}, 1000);
25+
} else {
26+
// Play failure sound
27+
new Audio('/sounds/wrong.mp3').play().catch(() => {});
28+
// Update score after delay
29+
setTimeout(() => {
30+
useGameStore.setState(state => ({
31+
score: Math.max(0, state.score - 5),
32+
streak: 0,
33+
currentProblem: null
34+
}));
35+
setSelectedAnswer(null);
36+
}, 1000);
37+
}
38+
};
39+
40+
const getButtonClass = (option: number) => {
41+
if (selectedAnswer === null) {
42+
return "bg-blue-500 hover:bg-blue-600 text-white px-8 py-4 rounded-lg text-2xl transition-colors";
43+
}
44+
45+
if (option === currentProblem?.answer) {
46+
return "bg-green-500 text-white px-8 py-4 rounded-lg text-2xl transition-colors";
47+
}
48+
49+
if (selectedAnswer === option) {
50+
return "bg-red-500 text-white px-8 py-4 rounded-lg text-2xl transition-colors";
51+
}
52+
53+
return "bg-blue-500 opacity-50 text-white px-8 py-4 rounded-lg text-2xl transition-colors";
54+
};
55+
56+
return (
57+
<div className="absolute top-0 left-0 right-0 z-50">
58+
{/* Score */}
59+
<div className="absolute top-4 left-4 p-4 bg-black/50 text-white rounded-lg backdrop-blur-sm">
60+
<div className="text-2xl font-bold">Score: {score}</div>
61+
<div className="text-xl">Streak: {streak}</div>
62+
</div>
63+
64+
{/* Math Problem */}
65+
{currentProblem && (
66+
<div className="fixed top-20 left-0 right-0 mx-auto w-[400px] bg-black/70 text-white p-6 rounded-xl backdrop-blur-lg">
67+
<div className="text-3xl font-bold mb-4 text-center">{currentProblem.question}</div>
68+
<div className="grid grid-cols-2 gap-4">
69+
{currentProblem.options.map((option, index) => (
70+
<button
71+
key={index}
72+
className={getButtonClass(option)}
73+
onClick={() => !selectedAnswer && handleAnswer(option)}
74+
disabled={selectedAnswer !== null}
75+
>
76+
{option}
77+
</button>
78+
))}
79+
</div>
80+
<div className="text-center mt-6 text-gray-400 text-lg">
81+
{selectedAnswer === null ? "Click the correct answer!" :
82+
selectedAnswer === currentProblem.answer ? "Correct! ✨" : "Wrong! The correct answer was " + currentProblem.answer}
83+
</div>
84+
</div>
85+
)}
86+
</div>
87+
);
88+
}

‎src/client/game/GameScene.tsx

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React, { useRef } from 'react';
2+
import { Sky } from '@react-three/drei';
3+
import Player from './Player';
4+
import Ground from './Ground';
5+
import MathProblem from './MathProblem';
6+
7+
export default function GameScene() {
8+
const problemsRef = useRef<Array<{ position: [number, number, number] }>>([
9+
{ position: [10, 1, 10] },
10+
{ position: [-10, 1, -10] },
11+
{ position: [10, 1, -10] },
12+
{ position: [-10, 1, 10] },
13+
]);
14+
15+
return (
16+
<>
17+
<Sky sunPosition={[100, 20, 100]} />
18+
<ambientLight intensity={0.3} />
19+
<directionalLight
20+
castShadow
21+
position={[50, 50, 50]}
22+
intensity={1.5}
23+
shadow-mapSize={[4096, 4096]}
24+
/>
25+
26+
<Player />
27+
<Ground />
28+
29+
{problemsRef.current.map((problem, index) => (
30+
<MathProblem key={index} position={problem.position} />
31+
))}
32+
</>
33+
);
34+
}

‎src/client/game/Ground.tsx

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import React from 'react';
2+
import { RigidBody } from '@react-three/rapier';
3+
4+
export default function Ground() {
5+
return (
6+
<RigidBody type="fixed" colliders="trimesh">
7+
<mesh receiveShadow rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]}>
8+
<planeGeometry args={[100, 100]} />
9+
<meshStandardMaterial
10+
color="#458745"
11+
metalness={0}
12+
roughness={0.8}
13+
/>
14+
</mesh>
15+
</RigidBody>
16+
);
17+
}

‎src/client/game/MathProblem.tsx

+127
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import React, { useEffect, useRef } from 'react';
2+
import { useFrame } from '@react-three/fiber';
3+
import { Text, Float } from '@react-three/drei';
4+
import { RigidBody, CuboidCollider } from '@react-three/rapier';
5+
import { useGameStore } from '../store/gameStore';
6+
7+
interface MathProblemProps {
8+
position: [number, number, number];
9+
}
10+
11+
function generateMathProblem() {
12+
const operations = ['+', '-', '*', '/'];
13+
const operation = operations[Math.floor(Math.random() * operations.length)];
14+
let num1: number, num2: number, answer: number;
15+
16+
switch (operation) {
17+
case '+':
18+
num1 = Math.floor(Math.random() * 20) + 1;
19+
num2 = Math.floor(Math.random() * 20) + 1;
20+
answer = num1 + num2;
21+
break;
22+
case '-':
23+
num1 = Math.floor(Math.random() * 20) + 1;
24+
num2 = Math.floor(Math.random() * num1) + 1;
25+
answer = num1 - num2;
26+
break;
27+
case '*':
28+
num1 = Math.floor(Math.random() * 12) + 1;
29+
num2 = Math.floor(Math.random() * 12) + 1;
30+
answer = num1 * num2;
31+
break;
32+
case '/':
33+
num2 = Math.floor(Math.random() * 12) + 1;
34+
answer = Math.floor(Math.random() * 12) + 1;
35+
num1 = num2 * answer;
36+
break;
37+
default:
38+
num1 = 0;
39+
num2 = 0;
40+
answer = 0;
41+
}
42+
43+
const options = [
44+
answer,
45+
answer + Math.floor(Math.random() * 5) + 1,
46+
answer - Math.floor(Math.random() * 5) + 1,
47+
Math.abs(answer * 2 - Math.floor(Math.random() * 5))
48+
].sort(() => Math.random() - 0.5);
49+
50+
return {
51+
question: `${num1} ${operation} ${num2} = ?`,
52+
answer,
53+
options
54+
};
55+
}
56+
57+
export default function MathProblem({ position }: MathProblemProps) {
58+
const meshRef = useRef<any>();
59+
const [active, setActive] = React.useState(false);
60+
const currentProblem = useGameStore(state => state.currentProblem);
61+
const setProblem = useGameStore(state => state.setProblem);
62+
63+
useFrame((state, delta) => {
64+
if (meshRef.current) {
65+
meshRef.current.rotation.y += delta * 0.5;
66+
}
67+
});
68+
69+
const handleCollision = () => {
70+
if (!currentProblem) {
71+
setActive(true);
72+
const problem = generateMathProblem();
73+
setProblem(problem);
74+
75+
// Play activation sound
76+
new Audio('/sounds/activate.mp3').play().catch(() => {});
77+
78+
// Reset active state after 0.5 seconds
79+
setTimeout(() => {
80+
setActive(false);
81+
}, 500);
82+
}
83+
};
84+
85+
return (
86+
<Float
87+
speed={1.5}
88+
rotationIntensity={0.5}
89+
floatIntensity={0.5}
90+
>
91+
<RigidBody
92+
type="fixed"
93+
position={position}
94+
sensor
95+
onIntersectionEnter={handleCollision}
96+
>
97+
<mesh
98+
ref={meshRef}
99+
scale={active ? 1.2 : 1}
100+
>
101+
<boxGeometry args={[2, 2, 2]} />
102+
<meshStandardMaterial
103+
color={active ? "#4CAF50" : "#2196F3"}
104+
emissive={active ? "#4CAF50" : "#000000"}
105+
emissiveIntensity={active ? 0.8 : 0}
106+
metalness={0.5}
107+
roughness={0.2}
108+
transparent
109+
opacity={0.9}
110+
/>
111+
</mesh>
112+
<CuboidCollider args={[2, 2, 2]} sensor />
113+
<Text
114+
position={[0, 0, 1.1]}
115+
fontSize={0.3}
116+
color="white"
117+
anchorX="center"
118+
anchorY="middle"
119+
outlineWidth={0.05}
120+
outlineColor="#000000"
121+
>
122+
{active ? "Solving..." : "Math Problem!"}
123+
</Text>
124+
</RigidBody>
125+
</Float>
126+
);
127+
}

‎src/client/game/Player.tsx

+118
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React, { useRef } from 'react';
2+
import { useFrame, useThree } from '@react-three/fiber';
3+
import { RigidBody, CuboidCollider } from '@react-three/rapier';
4+
import { Vector3, Quaternion, Euler } from 'three';
5+
import { useKeyboardControls, OrbitControls } from '@react-three/drei';
6+
7+
type Controls = {
8+
forward: boolean;
9+
backward: boolean;
10+
left: boolean;
11+
right: boolean;
12+
jump: boolean;
13+
};
14+
15+
export default function Player() {
16+
const playerRef = useRef<any>();
17+
const { camera } = useThree();
18+
const [, getKeys] = useKeyboardControls<Controls>();
19+
20+
const MOVE_SPEED = 15;
21+
const JUMP_FORCE = 5;
22+
const cameraOffset = new Vector3(0, 3, 8);
23+
const cameraTarget = new Vector3();
24+
const playerRotation = new Quaternion();
25+
const targetRotation = new Quaternion();
26+
27+
useFrame(() => {
28+
if (!playerRef.current) return;
29+
30+
const { forward, backward, left, right, jump } = getKeys();
31+
const position = playerRef.current.translation();
32+
33+
// Simple directional movement
34+
const velocity = {
35+
x: 0,
36+
y: playerRef.current.linvel().y,
37+
z: 0
38+
};
39+
40+
if (forward) velocity.z = -MOVE_SPEED;
41+
if (backward) velocity.z = MOVE_SPEED;
42+
if (left) velocity.x = -MOVE_SPEED;
43+
if (right) velocity.x = MOVE_SPEED;
44+
45+
// Apply velocity
46+
playerRef.current.setLinvel(velocity);
47+
48+
// Rotate player to face movement direction if moving
49+
if (velocity.x !== 0 || velocity.z !== 0) {
50+
const angle = Math.atan2(velocity.x, velocity.z);
51+
targetRotation.setFromEuler(new Euler(0, angle, 0));
52+
playerRotation.slerp(targetRotation, 0.2);
53+
playerRef.current.setRotation(playerRotation);
54+
}
55+
56+
// Handle jumping
57+
if (jump && Math.abs(velocity.y) < 0.1) {
58+
playerRef.current.applyImpulse({ x: 0, y: JUMP_FORCE, z: 0 });
59+
}
60+
61+
// Reset position if player falls off the world
62+
if (position.y < -10) {
63+
playerRef.current.setTranslation({ x: 0, y: 3, z: 0 });
64+
playerRef.current.setLinvel({ x: 0, y: 0, z: 0 });
65+
}
66+
67+
// Update camera position and target
68+
cameraTarget.set(position.x, position.y + 1.5, position.z);
69+
camera.position.copy(cameraTarget).add(cameraOffset);
70+
camera.lookAt(cameraTarget);
71+
});
72+
73+
return (
74+
<>
75+
<OrbitControls
76+
target={cameraTarget}
77+
enablePan={false}
78+
maxPolarAngle={Math.PI / 2 - 0.1}
79+
minDistance={4}
80+
maxDistance={10}
81+
/>
82+
<RigidBody
83+
ref={playerRef}
84+
colliders={false}
85+
mass={1}
86+
type="dynamic"
87+
position={[0, 3, 0]}
88+
enabledRotations={[false, true, false]}
89+
lockRotations={true}
90+
friction={0.7}
91+
restitution={0}
92+
linearDamping={0.95}
93+
>
94+
<CuboidCollider args={[0.5, 1, 0.5]} />
95+
<group>
96+
<mesh castShadow position={[0, 1, 0]}>
97+
<capsuleGeometry args={[0.5, 1, 8]} />
98+
<meshStandardMaterial color="#1E88E5" />
99+
</mesh>
100+
{/* Head */}
101+
<mesh castShadow position={[0, 2, 0]}>
102+
<sphereGeometry args={[0.4, 16, 16]} />
103+
<meshStandardMaterial color="#1E88E5" />
104+
</mesh>
105+
{/* Eyes */}
106+
<mesh position={[0.2, 2.1, 0.3]}>
107+
<sphereGeometry args={[0.1, 16, 16]} />
108+
<meshStandardMaterial color="white" />
109+
</mesh>
110+
<mesh position={[-0.2, 2.1, 0.3]}>
111+
<sphereGeometry args={[0.1, 16, 16]} />
112+
<meshStandardMaterial color="white" />
113+
</mesh>
114+
</group>
115+
</RigidBody>
116+
</>
117+
);
118+
}

‎src/client/index.css

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
body {
6+
margin: 0;
7+
padding: 0;
8+
overflow: hidden;
9+
}
10+
11+
#root {
12+
width: 100vw;
13+
height: 100vh;
14+
}

‎src/client/index.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom/client';
3+
import App from './App';
4+
import './index.css';
5+
6+
ReactDOM.createRoot(document.getElementById('root')!).render(
7+
<React.StrictMode>
8+
<App />
9+
</React.StrictMode>
10+
);

‎src/client/store/gameStore.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { create } from 'zustand';
2+
3+
interface MathProblem {
4+
question: string;
5+
answer: number;
6+
options: number[];
7+
}
8+
9+
interface GameState {
10+
isLoggedIn: boolean;
11+
username: string | null;
12+
score: number;
13+
streak: number;
14+
highScore: number;
15+
currentProblem: MathProblem | null;
16+
setLoggedIn: (status: boolean) => void;
17+
setUsername: (username: string) => void;
18+
setScore: (score: number) => void;
19+
setProblem: (problem: MathProblem | null) => void;
20+
}
21+
22+
export const useGameStore = create<GameState>((set) => ({
23+
isLoggedIn: false,
24+
username: null,
25+
score: 0,
26+
streak: 0,
27+
highScore: 0,
28+
currentProblem: null,
29+
setLoggedIn: (status) => set({ isLoggedIn: status }),
30+
setUsername: (username) => set({ username }),
31+
setScore: (score) => set((state) => ({
32+
score,
33+
highScore: Math.max(state.highScore, score)
34+
})),
35+
setProblem: (problem) => set({ currentProblem: problem })
36+
}));

‎src/server/db/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

‎src/server/db/schema.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
2+
3+
export const users = sqliteTable('users', {
4+
id: text('id').primaryKey(),
5+
username: text('username').notNull().unique(),
6+
createdAt: integer('created_at', { mode: 'timestamp' }).notNull().default(Date.now),
7+
});
8+
9+
export const gameStats = sqliteTable('game_stats', {
10+
id: text('id').primaryKey(),
11+
userId: text('user_id').notNull().references(() => users.id),
12+
problemsSolved: integer('problems_solved').notNull().default(0),
13+
correctAnswers: integer('correct_answers').notNull().default(0),
14+
totalAttempts: integer('total_attempts').notNull().default(0),
15+
lastPlayed: integer('last_played', { mode: 'timestamp' }).notNull().default(Date.now),
16+
});

‎src/server/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

‎src/server/websocket/index.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { WebSocket } from 'ws';
2+
3+
interface Player {
4+
id: string;
5+
username: string;
6+
position: { x: number; y: number; z: number };
7+
rotation: { x: number; y: number; z: number };
8+
}
9+
10+
interface GameState {
11+
players: Map<string, Player>;
12+
}
13+
14+
const gameState: GameState = {
15+
players: new Map()
16+
};
17+
18+
export function setupWebSocketServer(ws: WebSocket) {
19+
ws.on('message', (message: string) => {
20+
try {
21+
const data = JSON.parse(message);
22+
23+
switch (data.type) {
24+
case 'JOIN':
25+
gameState.players.set(data.playerId, {
26+
id: data.playerId,
27+
username: data.username,
28+
position: { x: 0, y: 0, z: 0 },
29+
rotation: { x: 0, y: 0, z: 0 }
30+
});
31+
break;
32+
33+
case 'MOVE':
34+
const player = gameState.players.get(data.playerId);
35+
if (player) {
36+
player.position = data.position;
37+
player.rotation = data.rotation;
38+
}
39+
break;
40+
41+
case 'LEAVE':
42+
gameState.players.delete(data.playerId);
43+
break;
44+
}
45+
46+
// Broadcast updated game state to all connected clients
47+
const gameStateMessage = JSON.stringify({
48+
type: 'GAME_STATE',
49+
players: Array.from(gameState.players.values())
50+
});
51+
52+
ws.send(gameStateMessage);
53+
} catch (error) {
54+
console.error('Error processing WebSocket message:', error);
55+
}
56+
});
57+
58+
ws.on('close', () => {
59+
// Cleanup when connection closes
60+
});
61+
}

‎tailwind.config.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** @type {import('tailwindcss').Config} */
2+
export default {
3+
content: [
4+
"./index.html",
5+
"./src/**/*.{js,ts,jsx,tsx}",
6+
],
7+
theme: {
8+
extend: {},
9+
},
10+
plugins: [],
11+
}

‎tsconfig.json

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2020",
4+
"useDefineForClassFields": true,
5+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
6+
"module": "ESNext",
7+
"skipLibCheck": true,
8+
"moduleResolution": "bundler",
9+
"allowImportingTsExtensions": true,
10+
"resolveJsonModule": true,
11+
"isolatedModules": true,
12+
"noEmit": true,
13+
"jsx": "react-jsx",
14+
"strict": true,
15+
"noUnusedLocals": true,
16+
"noUnusedParameters": true,
17+
"noFallthroughCasesInSwitch": true,
18+
"baseUrl": ".",
19+
"paths": {
20+
"@/*": ["./src/*"]
21+
}
22+
},
23+
"include": ["src"],
24+
"references": [{ "path": "./tsconfig.node.json" }]
25+
}

‎tsconfig.node.json

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true,
4+
"skipLibCheck": true,
5+
"module": "ESNext",
6+
"moduleResolution": "bundler",
7+
"allowSyntheticDefaultImports": true
8+
},
9+
"include": ["vite.config.ts"]
10+
}

‎vite.config.ts

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { defineConfig } from 'vite';
2+
import react from '@vitejs/plugin-react';
3+
import { fileURLToPath } from 'url';
4+
import { dirname } from 'path';
5+
6+
const __filename = fileURLToPath(import.meta.url);
7+
const __dirname = dirname(__filename);
8+
9+
export default defineConfig({
10+
plugins: [react()],
11+
resolve: {
12+
alias: {
13+
'@': fileURLToPath(new URL('./src', import.meta.url))
14+
}
15+
},
16+
server: {
17+
proxy: {
18+
'/api': {
19+
target: 'http://localhost:3001',
20+
changeOrigin: true
21+
},
22+
'/ws': {
23+
target: 'ws://localhost:3001',
24+
ws: true
25+
}
26+
}
27+
}
28+
});

0 commit comments

Comments
 (0)
Please sign in to comment.