prettier
This commit is contained in:
5
.prettierignore
Normal file
5
.prettierignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
coverage
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"useTabs": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 100,
|
||||||
|
}
|
||||||
@@ -1,68 +1,72 @@
|
|||||||
"use client";
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { socket } from "@/socket";
|
import { socket } from '@/socket';
|
||||||
|
|
||||||
import Card from '@/components/Card';
|
import Card from '@/components/Card';
|
||||||
|
|
||||||
import type { GameUpdate, ClientUpdate, StandardGameCard, TarokkaGameCard } from '@/types';
|
import type { GameUpdate, ClientUpdate, StandardGameCard, TarokkaGameCard } from '@/types';
|
||||||
|
|
||||||
export default function GamePage() {
|
export default function GamePage() {
|
||||||
const { gameID: gameIDParam } = useParams();
|
const { gameID: gameIDParam } = useParams();
|
||||||
|
|
||||||
const [gameID, setGameID] = useState('');
|
const [gameID, setGameID] = useState('');
|
||||||
const [cards, setCards] = useState<StandardGameCard[] | TarokkaGameCard[]>([]);
|
const [cards, setCards] = useState<StandardGameCard[] | TarokkaGameCard[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gameIDParam) {
|
if (gameIDParam) {
|
||||||
setGameID(Array.isArray(gameIDParam) ? gameIDParam[0] : gameIDParam);
|
setGameID(Array.isArray(gameIDParam) ? gameIDParam[0] : gameIDParam);
|
||||||
}
|
}
|
||||||
}, [gameIDParam])
|
}, [gameIDParam]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (gameID) {
|
if (gameID) {
|
||||||
socket.emit('join', gameID);
|
socket.emit('join', gameID);
|
||||||
|
|
||||||
socket.on('init', (data: GameUpdate) => {
|
socket.on('init', (data: GameUpdate) => {
|
||||||
console.log('init', data);
|
console.log('init', data);
|
||||||
setCards(data.cards);
|
setCards(data.cards);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('card-flipped', (data: GameUpdate) => {
|
socket.on('card-flipped', (data: GameUpdate) => {
|
||||||
console.log('>>>', data);
|
console.log('>>>', data);
|
||||||
setCards(data.cards);
|
setCards(data.cards);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return gameID ? () => {
|
return gameID
|
||||||
socket.off('init');
|
? () => {
|
||||||
socket.off('card-flipped');
|
socket.off('init');
|
||||||
} : undefined;
|
socket.off('card-flipped');
|
||||||
}, [gameID]);
|
}
|
||||||
|
: undefined;
|
||||||
|
}, [gameID]);
|
||||||
|
|
||||||
const flipCard = (cardIndex: number) => {
|
const flipCard = (cardIndex: number) => {
|
||||||
const flip: ClientUpdate = {
|
const flip: ClientUpdate = {
|
||||||
gameID,
|
gameID,
|
||||||
cardIndex,
|
cardIndex,
|
||||||
};
|
};
|
||||||
|
|
||||||
socket.emit('flip-card', flip);
|
socket.emit('flip-card', flip);
|
||||||
};
|
};
|
||||||
|
|
||||||
return cards.length ? (
|
return cards.length ? (
|
||||||
<main className="min-h-screen flex flex-col items-center justify-center gap-4 bg-[url('/img/table3.png')] bg-cover bg-center">
|
<main className="min-h-screen flex flex-col items-center justify-center gap-4 bg-[url('/img/table3.png')] bg-cover bg-center">
|
||||||
<div className="grid grid-cols-3 grid-rows-3 gap-8 w-fit mx-auto">
|
<div className="grid grid-cols-3 grid-rows-3 gap-8 w-fit mx-auto">
|
||||||
{Array.from({ length: 9 }).map((_, i) => {
|
{Array.from({ length: 9 }).map((_, i) => {
|
||||||
const cardIndex = [1, 3, 4, 5, 7].indexOf(i);
|
const cardIndex = [1, 3, 4, 5, 7].indexOf(i);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={i} className="aspect-[2/3]}">
|
<div key={i} className="aspect-[2/3]}">
|
||||||
{cardIndex !== -1 && <Card card={cards[cardIndex]} flipAction={() => flipCard(cardIndex)} />}
|
{cardIndex !== -1 && (
|
||||||
</div>
|
<Card card={cards[cardIndex]} flipAction={() => flipCard(cardIndex)} />
|
||||||
)
|
)}
|
||||||
})}
|
</div>
|
||||||
</div>
|
);
|
||||||
</main>
|
})}
|
||||||
) : null;
|
</div>
|
||||||
|
</main>
|
||||||
|
) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
export default function B() {
|
|
||||||
return <div>b</div>;
|
|
||||||
}
|
|
||||||
@@ -1,26 +1,26 @@
|
|||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
:root {
|
:root {
|
||||||
--background: #0a0a0a;
|
--background: #0a0a0a;
|
||||||
--foreground: #ededed;
|
--foreground: #ededed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,30 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from 'next';
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from 'next/font/google';
|
||||||
import "./globals.css";
|
import './globals.css';
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: '--font-geist-sans',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
variable: "--font-geist-mono",
|
variable: '--font-geist-mono',
|
||||||
subsets: ["latin"],
|
subsets: ['latin'],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: 'Create Next App',
|
||||||
description: "Generated by create next app",
|
description: 'Generated by create next app',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>{children}</body>
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
</html>
|
||||||
>
|
);
|
||||||
{children}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
31
app/page.tsx
31
app/page.tsx
@@ -3,22 +3,21 @@ import { useRouter } from 'next/navigation';
|
|||||||
import generateID from '@/tools/simpleID';
|
import generateID from '@/tools/simpleID';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const handleCreateGame = () => {
|
const handleCreateGame = () => {
|
||||||
const id = generateID();
|
const id = generateID();
|
||||||
router.push(`/${id}`);
|
router.push(`/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen flex items-center justify-center bg-[url('/img/table3.png')] bg-cover bg-center">
|
<main className="min-h-screen flex items-center justify-center bg-[url('/img/table3.png')] bg-cover bg-center">
|
||||||
<button
|
<button
|
||||||
onClick={handleCreateGame}
|
onClick={handleCreateGame}
|
||||||
className="bg-blue-600 text-white text-lg px-6 py-3 rounded-xl shadow hover:bg-blue-700 transition"
|
className="bg-blue-600 text-white text-lg px-6 py-3 rounded-xl shadow hover:bg-blue-700 transition"
|
||||||
>
|
>
|
||||||
Create New Game
|
Create New Game
|
||||||
</button>
|
</button>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { StandardGameCard, TarokkaGameCard } from "@/types";
|
import { StandardGameCard, TarokkaGameCard } from '@/types';
|
||||||
|
|
||||||
type CardProps = {
|
type CardProps = {
|
||||||
card: StandardGameCard | TarokkaGameCard;
|
card: StandardGameCard | TarokkaGameCard;
|
||||||
flipAction: () => void;
|
flipAction: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Card({ card: { aria, url }, flipAction }: CardProps) {
|
export default function Card({ card: { aria, url }, flipAction }: CardProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-[21vh] w-[15vh] flex items-center justify-center cursor-pointer transform transition-transform duration-200 hover:scale-150 relative z-0 hover:z-10"
|
className="h-[21vh] w-[15vh] flex items-center justify-center cursor-pointer transform transition-transform duration-200 hover:scale-150 relative z-0 hover:z-10"
|
||||||
onClick={flipAction}
|
onClick={flipAction}
|
||||||
>
|
>
|
||||||
<img
|
<img className="rounded-xl" src={url} alt={aria} />
|
||||||
className="rounded-xl"
|
</div>
|
||||||
src={url}
|
);
|
||||||
alt={aria}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
103
lib/GameStore.ts
103
lib/GameStore.ts
@@ -1,75 +1,80 @@
|
|||||||
import Deck from './TarokkaDeck'
|
import Deck from './TarokkaDeck';
|
||||||
|
|
||||||
import { GameState, GameUpdate, TarokkaGameCard } from '../types'
|
import { GameState, GameUpdate, TarokkaGameCard } from '../types';
|
||||||
|
|
||||||
const deck = new Deck();
|
const deck = new Deck();
|
||||||
|
|
||||||
export default class GameStore {
|
export default class GameStore {
|
||||||
private games: Map<string, GameState>;
|
private games: Map<string, GameState>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.games = new Map();
|
this.games = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
createGame(id: string): GameState {
|
createGame(id: string): GameState {
|
||||||
if (this.games.has(id)) throw new Error(`Game ${id} already exists`);
|
if (this.games.has(id)) throw new Error(`Game ${id} already exists`);
|
||||||
|
|
||||||
const newGame: GameState = {
|
const newGame: GameState = {
|
||||||
id,
|
id,
|
||||||
players: new Set(),
|
players: new Set(),
|
||||||
cards: deck.select(5).map(card => ({ ...card, flipped: false })),
|
cards: deck.select(5).map((card) => ({ ...card, flipped: false })),
|
||||||
lastUpdated: Date.now(),
|
lastUpdated: Date.now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.games.set(id, newGame);
|
this.games.set(id, newGame);
|
||||||
|
|
||||||
return newGame;
|
return newGame;
|
||||||
}
|
}
|
||||||
|
|
||||||
joinGame(gameID: string, playerID: string): GameUpdate {
|
joinGame(gameID: string, playerID: string): GameUpdate {
|
||||||
const game = this.games.get(gameID) || this.createGame(gameID);
|
const game = this.games.get(gameID) || this.createGame(gameID);
|
||||||
|
|
||||||
game.players.add(playerID);
|
game.players.add(playerID);
|
||||||
game.lastUpdated = Date.now();
|
game.lastUpdated = Date.now();
|
||||||
|
|
||||||
return this.gameUpdate(game);
|
return this.gameUpdate(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
leaveGame(gameID: string, playerID: string): GameState {
|
leaveGame(gameID: string, playerID: string): GameState {
|
||||||
const game = this.getGame(gameID);
|
const game = this.getGame(gameID);
|
||||||
|
|
||||||
game.players.delete(playerID);
|
game.players.delete(playerID);
|
||||||
game.lastUpdated = Date.now();
|
game.lastUpdated = Date.now();
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
flipCard(gameID: string, cardIndex: number): GameUpdate {
|
flipCard(gameID: string, cardIndex: number): GameUpdate {
|
||||||
const game = this.getGame(gameID);
|
const game = this.getGame(gameID);
|
||||||
const card = game.cards[cardIndex];
|
const card = game.cards[cardIndex];
|
||||||
|
|
||||||
if (!card) throw new Error(`Card ${cardIndex} not found`);
|
if (!card) throw new Error(`Card ${cardIndex} not found`);
|
||||||
|
|
||||||
card.flipped = !card.flipped;
|
card.flipped = !card.flipped;
|
||||||
game.lastUpdated = Date.now();
|
game.lastUpdated = Date.now();
|
||||||
|
|
||||||
return this.gameUpdate(game);
|
return this.gameUpdate(game);
|
||||||
}
|
}
|
||||||
|
|
||||||
getGame(gameID: string): GameState {
|
getGame(gameID: string): GameState {
|
||||||
const game = this.games.get(gameID);
|
const game = this.games.get(gameID);
|
||||||
|
|
||||||
if (!game) throw new Error(`Game ${gameID} not found`);
|
if (!game) throw new Error(`Game ${gameID} not found`);
|
||||||
|
|
||||||
return game;
|
return game;
|
||||||
}
|
}
|
||||||
|
|
||||||
gameUpdate(game: GameState): GameUpdate {
|
gameUpdate(game: GameState): GameUpdate {
|
||||||
const { id, cards } = game;
|
const { id, cards } = game;
|
||||||
return { id, cards: (cards as TarokkaGameCard[]).map((card: TarokkaGameCard) => card.flipped ? card : { ...deck.getBack(), flipped: false }) };
|
return {
|
||||||
}
|
id,
|
||||||
|
cards: (cards as TarokkaGameCard[]).map((card: TarokkaGameCard) =>
|
||||||
|
card.flipped ? card : { ...deck.getBack(), flipped: false },
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
deleteGame(gameID: string): void {
|
deleteGame(gameID: string): void {
|
||||||
this.games.delete(gameID);
|
this.games.delete(gameID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,45 +3,45 @@ import cards from '../constants/standardCards';
|
|||||||
import type { StandardCard } from '../types';
|
import type { StandardCard } from '../types';
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
back: number;
|
back: number;
|
||||||
jokers: boolean;
|
jokers: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OptionProps {
|
export interface OptionProps {
|
||||||
back?: number;
|
back?: number;
|
||||||
jokers?: boolean;
|
jokers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {
|
const DEFAULT_OPTIONS = {
|
||||||
jokers: false,
|
jokers: false,
|
||||||
back: 1,
|
back: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class Cards {
|
export default class Cards {
|
||||||
private options: Options;
|
private options: Options;
|
||||||
|
|
||||||
private deck: StandardCard[] = [];
|
private deck: StandardCard[] = [];
|
||||||
private backs: StandardCard[] = [];
|
private backs: StandardCard[] = [];
|
||||||
private jokers: StandardCard[] = [];
|
private jokers: StandardCard[] = [];
|
||||||
|
|
||||||
constructor(options: OptionProps = {}) {
|
constructor(options: OptionProps = {}) {
|
||||||
this.options = { ...DEFAULT_OPTIONS, ...options };
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
||||||
|
|
||||||
this.deck = cards.filter(card => !card.back && (this.options.jokers || !card.joker));
|
this.deck = cards.filter((card) => !card.back && (this.options.jokers || !card.joker));
|
||||||
this.backs = cards.filter(card => card.back);
|
this.backs = cards.filter((card) => card.back);
|
||||||
this.jokers = cards.filter(card => card.joker);
|
this.jokers = cards.filter((card) => card.joker);
|
||||||
}
|
}
|
||||||
|
|
||||||
select(count: number): StandardCard[] {
|
select(count: number): StandardCard[] {
|
||||||
return getRandomItems(this.deck, count);
|
return getRandomItems(this.deck, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBack(style: number): StandardCard {
|
getBack(style: number): StandardCard {
|
||||||
style = style || this.options.back;
|
style = style || this.options.back;
|
||||||
return this.backs.find(card => card.id.startsWith(String(style))) || this.backs[0];
|
return this.backs.find((card) => card.id.startsWith(String(style))) || this.backs[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
getJokers(): StandardCard[] {
|
getJokers(): StandardCard[] {
|
||||||
return this.jokers;
|
return this.jokers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,19 +3,19 @@ import cards from '../constants/tarokkaCards';
|
|||||||
import type { TarokkaCard } from '../types';
|
import type { TarokkaCard } from '../types';
|
||||||
|
|
||||||
export default class TarokkaDeck {
|
export default class TarokkaDeck {
|
||||||
private deck: TarokkaCard[] = [];
|
private deck: TarokkaCard[] = [];
|
||||||
private backs: TarokkaCard[] = [];
|
private backs: TarokkaCard[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.deck = cards.filter(card => !card.back);
|
this.deck = cards.filter((card) => !card.back);
|
||||||
this.backs = cards.filter(card => card.back);
|
this.backs = cards.filter((card) => card.back);
|
||||||
}
|
}
|
||||||
|
|
||||||
select(count: number): TarokkaCard[] {
|
select(count: number): TarokkaCard[] {
|
||||||
return getRandomItems(this.deck, count);
|
return getRandomItems(this.deck, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBack(): TarokkaCard {
|
getBack(): TarokkaCard {
|
||||||
return this.backs[0];
|
return this.backs[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
package-lock.json
generated
50
package-lock.json
generated
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "custom-server-app",
|
"name": "tarokka",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
@@ -14,10 +14,11 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^22.14.0",
|
"@types/node": "^22.14.1",
|
||||||
"@types/react": "^19.1.0",
|
"@types/react": "^19.1.1",
|
||||||
"@types/react-dom": "^19.1.2",
|
"@types/react-dom": "^19.1.2",
|
||||||
"nodemon": "^3.1.9",
|
"nodemon": "^3.1.9",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
@@ -822,18 +823,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.14.0",
|
"version": "22.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
|
||||||
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==",
|
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.1.0",
|
"version": "19.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.1.tgz",
|
||||||
"integrity": "sha512-UaicktuQI+9UKyA4njtDOGBD/67t8YEBt2xdfqu8+gP9hqPUPsiXlNPcpS2gVdjmis5GKPG3fCxbQLVgxsQZ8w==",
|
"integrity": "sha512-ePapxDL7qrgqSF67s0h9m412d9DbXyC1n59O2st+9rjuuamWsZuD2w55rqY12CbzsZ7uVXb5Nw0gEp9Z8MMutQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
@@ -843,6 +846,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.2.tgz",
|
||||||
"integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==",
|
"integrity": "sha512-XGJkWF41Qq305SKWEILa1O8vzhb3aOo3ogBlSmiqNko/WmRb6QIaweuZCXjKygVDXpzXb5wyxKTSOsmkuqj+Qw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
@@ -1123,6 +1127,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||||
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
},
|
},
|
||||||
@@ -1738,6 +1743,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
|
||||||
"integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
|
"integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^3.5.2",
|
"chokidar": "^3.5.2",
|
||||||
"debug": "^4",
|
"debug": "^4",
|
||||||
@@ -1831,6 +1837,22 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "3.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||||
|
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin/prettier.cjs"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/pstree.remy": {
|
"node_modules/pstree.remy": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||||
@@ -1841,6 +1863,7 @@
|
|||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||||
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -1849,6 +1872,7 @@
|
|||||||
"version": "19.1.0",
|
"version": "19.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.26.0"
|
"scheduler": "^0.26.0"
|
||||||
},
|
},
|
||||||
@@ -1871,7 +1895,8 @@
|
|||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.26.0",
|
"version": "0.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="
|
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.1",
|
"version": "7.7.1",
|
||||||
@@ -1957,6 +1982,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
|
||||||
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
|
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": "^7.5.3"
|
"semver": "^7.5.3"
|
||||||
},
|
},
|
||||||
@@ -2219,6 +2245,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -2236,7 +2263,8 @@
|
|||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.21.0",
|
"version": "6.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/v8-compile-cache-lib": {
|
"node_modules/v8-compile-cache-lib": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
|
|||||||
17
package.json
17
package.json
@@ -8,19 +8,20 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"next": "latest",
|
"next": "latest",
|
||||||
"react": "^18.2.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^19.1.0",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"socket.io-client": "^4.8.1"
|
"socket.io-client": "^4.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^18.11.5",
|
"@types/node": "^22.14.1",
|
||||||
"@types/react": "^18.0.23",
|
"@types/react": "^19.1.1",
|
||||||
"@types/react-dom": "^18.0.7",
|
"@types/react-dom": "^19.1.2",
|
||||||
"nodemon": "^2.0.20",
|
"nodemon": "^3.1.9",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
"tailwindcss": "^4",
|
"tailwindcss": "^4",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^4.8.4"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
server.ts
57
server.ts
@@ -6,51 +6,50 @@ import GameStore from './lib/GameStore';
|
|||||||
import type { ClientUpdate } from './types';
|
import type { ClientUpdate } from './types';
|
||||||
|
|
||||||
const dev = process.env.NODE_ENV !== 'production';
|
const dev = process.env.NODE_ENV !== 'production';
|
||||||
const hostname = 'localhost';
|
const hostname = '0.0.0.0';
|
||||||
const port = 3000;
|
const port = 3000;
|
||||||
|
|
||||||
// when using middleware `hostname` and `port` must be provided
|
|
||||||
const app = next({ dev, hostname, port });
|
const app = next({ dev, hostname, port });
|
||||||
const handler = app.getRequestHandler();
|
const handler = app.getRequestHandler();
|
||||||
|
|
||||||
const gameStore = new GameStore();
|
const gameStore = new GameStore();
|
||||||
|
|
||||||
app.prepare().then(() => {
|
app.prepare().then(() => {
|
||||||
const httpServer = createServer(handler);
|
const httpServer = createServer(handler);
|
||||||
|
|
||||||
const io = new SocketIOServer(httpServer);
|
const io = new SocketIOServer(httpServer);
|
||||||
|
|
||||||
io.on('connection', (socket: Socket) => {
|
io.on('connection', (socket: Socket) => {
|
||||||
console.log(`Client connected: ${socket.id}`);
|
console.log(`Client connected: ${socket.id}`);
|
||||||
|
|
||||||
socket.on('join', (gameID) => {
|
socket.on('join', (gameID) => {
|
||||||
socket.join(gameID);
|
socket.join(gameID);
|
||||||
const gameUpdate = gameStore.joinGame(gameID, socket.id);
|
const gameUpdate = gameStore.joinGame(gameID, socket.id);
|
||||||
|
|
||||||
console.log(`Socket ${socket.id} joined game ${gameID}`)
|
console.log(`Socket ${socket.id} joined game ${gameID}`);
|
||||||
|
|
||||||
socket.emit('init', gameUpdate);
|
socket.emit('init', gameUpdate);
|
||||||
})
|
});
|
||||||
|
|
||||||
socket.on('flip-card', ({ gameID, cardIndex }: ClientUpdate) => {
|
socket.on('flip-card', ({ gameID, cardIndex }: ClientUpdate) => {
|
||||||
console.log('Card flipped:', { gameID, cardIndex });
|
console.log('Card flipped:', { gameID, cardIndex });
|
||||||
|
|
||||||
const gameUpdate = gameStore.flipCard(gameID, cardIndex);
|
const gameUpdate = gameStore.flipCard(gameID, cardIndex);
|
||||||
|
|
||||||
io.to(gameID).emit('card-flipped', gameUpdate);
|
io.to(gameID).emit('card-flipped', gameUpdate);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
socket.on('disconnect', () => {
|
||||||
console.log(`Client disconnected: ${socket.id}`);
|
console.log(`Client disconnected: ${socket.id}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
httpServer
|
httpServer
|
||||||
.once('error', (err) => {
|
.once('error', (err) => {
|
||||||
console.error('Server error:', err);
|
console.error('Server error:', err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
})
|
})
|
||||||
.listen(port, () => {
|
.listen(port, () => {
|
||||||
console.log(`> Ready on http://${hostname}:${port}`);
|
console.log(`> Ready on http://${hostname}:${port}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export default function getRandomItems<T>(items: T[], count: number): T[] {
|
export default function getRandomItems<T>(items: T[], count: number): T[] {
|
||||||
const shuffled = [...items];
|
const shuffled = [...items];
|
||||||
|
|
||||||
// Fisher-Yates shuffle
|
// Fisher-Yates shuffle
|
||||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||||
}
|
}
|
||||||
|
|
||||||
return count > shuffled.length ? shuffled : shuffled.slice(0, count);
|
return count > shuffled.length ? shuffled : shuffled.slice(0, count);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import getRandomItems from '@/tools/getRandomItems';
|
import getRandomItems from '@/tools/getRandomItems';
|
||||||
|
|
||||||
const alphabet =
|
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||||
'0123456789abcdefghijklmnopqrstuvwxyz';
|
|
||||||
|
|
||||||
const generateID = (length: number = 6) => {
|
const generateID = (length: number = 6) => {
|
||||||
return getRandomItems(alphabet.split(''), length).join('');
|
return getRandomItems(alphabet.split(''), length).join('');
|
||||||
};
|
};
|
||||||
|
|
||||||
export default generateID;
|
export default generateID;
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
export interface StandardCard {
|
export interface StandardCard {
|
||||||
id: string;
|
id: string;
|
||||||
aria: string;
|
aria: string;
|
||||||
back: boolean;
|
back: boolean;
|
||||||
face: boolean;
|
face: boolean;
|
||||||
joker: boolean;
|
joker: boolean;
|
||||||
suit: 'Clubs' | 'Diamonds' | 'Hearts' | 'Spades' | null;
|
suit: 'Clubs' | 'Diamonds' | 'Hearts' | 'Spades' | null;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StandardGameCard extends StandardCard {
|
export interface StandardGameCard extends StandardCard {
|
||||||
flipped: boolean;
|
flipped: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TarokkaCard {
|
export interface TarokkaCard {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
card: string;
|
card: string;
|
||||||
description: string;
|
description: string;
|
||||||
aria: string;
|
aria: string;
|
||||||
back: boolean;
|
back: boolean;
|
||||||
suit: 'Coins' | 'Glyphs' | 'High Deck' | 'Stars' | 'Swords' | null;
|
suit: 'Coins' | 'Glyphs' | 'High Deck' | 'Stars' | 'Swords' | null;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TarokkaGameCard extends TarokkaCard {
|
export interface TarokkaGameCard extends TarokkaCard {
|
||||||
flipped: boolean;
|
flipped: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameState {
|
export interface GameState {
|
||||||
id: string;
|
id: string;
|
||||||
players: Set<string>;
|
players: Set<string>;
|
||||||
cards: StandardGameCard[] | TarokkaGameCard[];
|
cards: StandardGameCard[] | TarokkaGameCard[];
|
||||||
lastUpdated: number;
|
lastUpdated: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameUpdate {
|
export interface GameUpdate {
|
||||||
id: string;
|
id: string;
|
||||||
cards: StandardGameCard[] | TarokkaGameCard[];
|
cards: StandardGameCard[] | TarokkaGameCard[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ClientUpdate {
|
export interface ClientUpdate {
|
||||||
gameID: string;
|
gameID: string;
|
||||||
cardIndex: number;
|
cardIndex: number;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user