Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
10633bf
refactor: wrappers to oop entities
DomiIRL Mar 21, 2026
89bb639
refactor: use api in common
DomiIRL Mar 21, 2026
45d598c
refactor: use proper objects
DomiIRL Mar 21, 2026
03b0bc1
refactor: use constants
DomiIRL Mar 21, 2026
ae1376f
refactor: use game api in client
DomiIRL Mar 21, 2026
f778f71
chore: warnings
DomiIRL Mar 21, 2026
8237159
refactor: table uses game instance
DomiIRL Mar 22, 2026
f427f3b
refactor: bulldoze parameters in dom
DomiIRL Mar 22, 2026
be19b8a
refactor: move folders
DomiIRL Mar 22, 2026
269c377
refactor: enhance game logic and player management
DomiIRL Mar 22, 2026
64f1f98
refactor: streamline game context management and enhance chat functio…
DomiIRL Mar 22, 2026
ee8df8e
refactor: centralize card texture retrieval in TheGameClient
DomiIRL Mar 22, 2026
15422d9
refactor: add comprehensive codebase guide for Exploding Kittens project
DomiIRL Mar 22, 2026
756f075
refactor: reorganize animation system
DomiIRL Mar 22, 2026
ac01e08
fix: reimplement defuses simple
DomiIRL Mar 22, 2026
9513cc0
refactor: remove unnecessary whitespace in original-deck.ts
DomiIRL Mar 22, 2026
9d2abb9
refactor: add numMoves getter to turn manager
DomiIRL Mar 22, 2026
6bac658
refactor: reorganize overlay imports and enhance card access method
DomiIRL Mar 23, 2026
84df595
refactor: implement defuse overlay
DomiIRL Mar 24, 2026
effaa02
refactor: streamline player name retrieval and improve match data han…
DomiIRL Mar 24, 2026
2ca2db4
refactor: rename boardgame game file
DomiIRL Mar 24, 2026
fa8abe2
refactor: update card dealing logic and clean up imports in WinnerOve…
DomiIRL Mar 24, 2026
296e49a
refactor: remove unused card animation logic from Board component
DomiIRL Mar 24, 2026
ccf8966
refactor: fix conditional check for pending card resolution in Board …
DomiIRL Mar 24, 2026
0156df9
feat: simple animation system
DomiIRL Mar 26, 2026
3b60dd1
refactor: enhance width handling for animated card transitions
DomiIRL Mar 26, 2026
734203c
feat: use player hand instead of name as animation target
DomiIRL Mar 26, 2026
01ff8b6
fix: player cards too big
DomiIRL Mar 26, 2026
ff69a62
feat: implement animation context for managing card animations
DomiIRL Mar 26, 2026
c141efb
feat: simplify animation node references for discard and draw piles
DomiIRL Mar 26, 2026
258abdf
fix: remove unnecessary reference from registerNode call
DomiIRL Mar 27, 2026
eaace85
feat: add AnimationQueue for managing card animations
DomiIRL Mar 27, 2026
85b456b
feat: refactor animation handling to use metadata for card animations
DomiIRL Mar 27, 2026
c3ebc61
feat: streamline AnimationContext by simplifying setRef implementation
DomiIRL Mar 27, 2026
e88d6e7
feat: integrate game animations queue into AnimationContext for impro…
DomiIRL Mar 27, 2026
4933e19
feat: defuse card animations
DomiIRL Mar 27, 2026
18ed7a4
fix: winner overlay
DomiIRL Mar 27, 2026
30d620f
feat: enhance animation queue management and simplify animation retri…
DomiIRL Mar 28, 2026
642934a
refactor: optimize animation queue implementation and simplify data s…
DomiIRL Mar 28, 2026
acbb230
refactor: dont put in string
DomiIRL Mar 28, 2026
281a54b
refactor: streamline animation handling and remove unnecessary state …
DomiIRL Mar 28, 2026
f5e92fa
refactor: remove unnecessary type casting in background image assignment
DomiIRL Mar 28, 2026
3a72ef5
refactor: improve insert index calculation for defuse stage
DomiIRL Mar 28, 2026
4fc50cd
feat: animation visible to
DomiIRL Mar 28, 2026
cef6ef8
fix: cat card
DomiIRL Mar 28, 2026
69bf988
fix: cat card
DomiIRL Mar 28, 2026
d164e8f
fix: discard ref id
DomiIRL Mar 28, 2026
d1ba3e2
feat: enhance card handling and animation logic with id support
DomiIRL Mar 28, 2026
42a8280
feat: refactor card management by introducing CardList class for bett…
DomiIRL Mar 28, 2026
c5fe9dc
feat: rename CardList to CardHolder
DomiIRL Mar 28, 2026
7ce59a8
Merge pull request #2 from DomiIRL/feat/rework-animations
DomiIRL Mar 28, 2026
bae4bdf
refactor: remove animation interaction blocker and clean up imports
DomiIRL Mar 28, 2026
ae58063
feat: implement DiscardPile and DrawPile components for improved card…
DomiIRL Mar 28, 2026
151d584
refactor: enhance animation queue to support unique animation IDs and…
DomiIRL Mar 28, 2026
fbde51a
fix: attack using next player
DomiIRL Mar 28, 2026
aef6521
refactor: update AGENTS.md for improved clarity on game state managem…
DomiIRL Mar 28, 2026
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
73 changes: 73 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Exploding Kittens Codebase Guide

## 🧠 Core Architecture

This project implements Exploding Kittens using **React** (frontend) and **boardgame.io** (game engine).

### Key Directories
- **`src/common/`**: Shared game logic, state definitions, and move implementations. Start here to understand game mechanics.
- **`src/client/`**: React frontend.
- **`src/server/`**: `boardgame.io` game server.

### Game State Management
The project uses a custom wrapper pattern over standard `boardgame.io` state (`G` and `ctx`):
- **`TheGame` Class** (`src/common/entities/game.ts`): Wraps the raw context. Always instantiate this to interact with game state.
- **`IGameState`** (`src/common/models/game-state.model.ts`): Defines the shape of `G` (piles, deck type, etc.).
- **Moves with `inGame` HOC**: All game moves are wrapped with the `inGame` higher-order function (`src/common/moves/in-game.ts`).
- *Pattern*: Define moves as `(game: TheGame, ...args) => void`. The HOC handles context injection.
- *Example*: See `src/common/moves/draw-move.ts`.

### Client Context
The React app accesses game state via `GameContext` (`src/client/context/GameContext.tsx`).
- **`TheGameClient`**: Extends `TheGame` with client-specific features (match ID, chat, multiplayer flags).
- Use `useGame()` hook to access the current game instance in components.

## 🛠️ Developer Workflows

### Running the Project
The project requires two concurrent processes:
1. **Game Server**: `npm run server:dev` (Runs on `http://localhost:51399` usually, configurable)
2. **Client**: `npm run dev` (Runs on `http://localhost:5173`)

### Building
- **Client**: `npm run build`
- **Server**: `npm run build:server`

### Testing
- Currently, there is **no automated test suite**. Verification is manual via playing the game in browsers.

## 🧩 Patterns & Conventions

### Models & Entities (OOP Pattern)
The codebase strictly separates raw state from game logic using an OOP wrapper pattern:
- **Models (`src/common/models/`)**: Pure TypeScript `interface`s prefix with `I` (e.g., `ICard`, `IGameState`, `IPlayer`). These represent the plain JSON-serializable state strictly managed by `boardgame.io`. **Never add methods here.**
- **Entities (`src/common/entities/`)**: Object-Oriented wrapper classes (e.g., `Card`, `TheGame`, `Player`). They take the raw state interface in their constructor and provide getter/setter and mutation logic.
- *Rule*: When mutating state in moves, always instantiate or access the Entity wrapper, execute methods on it, and let it safely update the underlying raw Model state. Do not mutate the `IPlayer` or `ICard` interfaces directly.

### Move Mechanics
When implementing game moves (actions):
1. Create a function in `src/common/moves/`.
2. Function signature must be `(game: TheGame, ...args)`.
3. Mutate state via `game.piles`, `game.players`, or `game.gameState`.
4. Register the move in `src/common/exploding-kittens.ts` using `inGame(yourMove)`.
5. If the move transitions the game state, use constants from `src/common/constants/phases.ts` or `src/common/constants/stages.ts`.

**Example Move:**
```typescript
import { TheGame } from "../entities/game";

export const myMove = (game: TheGame, argument: string) => {
// Use entity helpers
if (game.players.actingPlayer.hasCard(argument)) {
game.players.actingPlayer.discard(argument);
}
};
```

### Component Structure
- **Game View**: `src/client/components/game-view/` handles the main game screen.
- **Board**: `src/client/components/board/` handles the visual representation of the table.

### Docker
- `docker-compose.yml` orchestrates both client and server containers.
- Environment variables are managed in `stack.env` and passed to containers.
76 changes: 76 additions & 0 deletions src/client/components/animations/AnimatedCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useEffect, useRef } from 'react';
import { TheGameClient } from '../../entities/game-client';
import {CardAnimation, useAnimationState} from '../../context/AnimationContext';

export function AnimatedCard({ animation }: { animation: CardAnimation }) {
const { getNode } = useAnimationState();
const cardRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const fromEl = getNode(String(animation.metadata.from));
const toEl = getNode(String(animation.metadata.to));

if (!fromEl || !toEl || !cardRef.current) {
console.warn(`Card Animation Failed!
fromId (${animation.metadata.from}):`, fromEl, `
toId (${animation.metadata.to}):`, toEl, `
cardRef:`, cardRef.current);
return;
}

const fromRect = fromEl.getBoundingClientRect();
const toRect = toEl.getBoundingClientRect();

// Determine target width to use for size transitioning
const getTargetWidth = (el: HTMLElement, rect: DOMRect) => {
// If it's explicitly a card or a pile, it has valid dimensions
if (el.classList.contains('pile') || el.classList.contains('card')) {
return el.offsetWidth || rect.width;
}
// If targeting a badge/generic element, assume standard player card size
const sampleCard = document.querySelector('.card:not(.pile):not(.animated-card)') as HTMLElement;
return sampleCard ? sampleCard.offsetWidth : 80;
};

const fromWidth = getTargetWidth(fromEl, fromRect);
const toWidth = getTargetWidth(toEl, toRect);

// Apply strictly to the DOM Ref instead of triggering standard React renders mid-animation

// 1. Set initial geometry
cardRef.current.style.transition = 'none';
cardRef.current.style.left = `${fromRect.left + fromRect.width / 2}px`;
cardRef.current.style.top = `${fromRect.top + fromRect.height / 2}px`;
cardRef.current.style.width = `${fromWidth}px`;
cardRef.current.style.backgroundImage = `url(${TheGameClient.getCardTexture(animation.metadata.card)})`;
cardRef.current.style.display = 'flex';

// Force a synchronous DOM reflow to ensure the browser registers the start position
void cardRef.current.offsetHeight;

// 2. Play transition explicitly targeting 'to' rect in next animation frames
const frame1 = requestAnimationFrame(() => {
if (!cardRef.current) return;
cardRef.current.style.transition = `all ${animation.metadata.durationMs}ms cubic-bezier(0.25, 0.8, 0.25, 1)`;
cardRef.current.style.left = `${toRect.left + toRect.width / 2}px`;
cardRef.current.style.top = `${toRect.top + toRect.height / 2}px`;
cardRef.current.style.width = `${toWidth}px`;
});

return () => cancelAnimationFrame(frame1);
}, [animation, getNode]);

return (
<div
ref={cardRef}
className={`animated-card card`}
style={{
position: 'fixed',
transform: 'translate(-50%, -50%) scale(1)',
zIndex: 65,
pointerEvents: 'none',
display: 'none'
}}
/>
);
}
5 changes: 5 additions & 0 deletions src/client/components/animations/AnimationOverlay.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.animated-card {
position: absolute !important; /* Using JS to apply left/top, so use explicit positioning */
transition-property: all;
will-change: transform, left, top, width, height;
}
16 changes: 16 additions & 0 deletions src/client/components/animations/AnimationOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useAnimationState } from '../../context/AnimationContext';
import { AnimatedCard } from './AnimatedCard';
import './AnimationOverlay.css';
import '../board/player/card/Card.css';

export function AnimationOverlay() {
const { animations } = useAnimationState();

if (animations.length === 0) return null;

return (
<>
{animations.map((anim: any) => <AnimatedCard key={anim.id} animation={anim} />)}
</>
);
}
Loading