A strategic multiplayer card game built with Next.js, TypeScript, and a pure reducer-based architecture. Vinto features 4-player gameplay with AI opponents, action cards with special abilities, and cloud-ready game engine.
- 4-Player Game: One human player vs three AI opponents
- Strategic Gameplay: Action cards (7-K) with special abilities
- Cloud-Ready Architecture: Pure GameEngine that can be hosted remotely
- Responsive Design: Optimized for both mobile and desktop
- Type-Safe: Full TypeScript implementation with strict mode
- AI Opponents: MCTS-based bot decision making
- Each player starts with 5 cards
- Players get to peek at 2 of their own cards
- Goal: Achieve the lowest total score
On your turn, you can either:
- Draw from deck - Draw a new card and choose to swap or play
- Take from discard - Take an unplayed action card (7-K) from discard pile
- Call Vinto - End the game when you think you have the lowest score
- 7 (Peek Own): Look at one of your own cards
- 8 (Peek Own): Look at one of your own cards
- 9 (Peek Opponent): Look at one opponent's card
- 10 (Peek Opponent): Look at one opponent's card
- Jack (Swap Cards): Swap any two cards from two different players
- Queen (Peek & Swap): Peek at two cards from two different players, then optionally swap them
- King (Declare Rank): All players must toss in cards of declared rank
- Number cards (1-6): Face value
- Action cards (7-Q): 10 points each
- King: 0 point
- Ace: 1 point
- Joker: -1 point
- Game ends when someone calls Vinto - all cards revealed and scored
graph TB
subgraph "Client Side"
UI[UI Components<br/>React + Hooks]
GC[GameClient<br/>Observable Wrapper]
BotAI[BotAIAdapter<br/>MCTS Decision Engine]
Anim[AnimationService]
UIStore[UIStore<br/>Modals, Highlights]
AnimStore[CardAnimationStore<br/>Animation State]
end
subgraph "Game Engine (Cloud-Ready)"
GE[GameEngine<br/>Pure Reducers]
GS[(GameState<br/>Single Source of Truth)]
end
UI -->|dispatch GameAction| GC
BotAI -->|dispatch GameAction| GC
GC -->|action| GE
GE -->|new state| GS
GS -->|subscribe| UI
GS -->|subscribe| Anim
Anim --> AnimStore
UI --> UIStore
style GE fill:#90EE90
style GS fill:#87CEEB
style GC fill:#FFE4B5
sequenceDiagram
participant User
participant UI as UI Component
participant GC as GameClient
participant GE as GameEngine
participant State as GameState
User->>UI: Click "Draw Card"
UI->>GC: dispatch(GameActions.drawCard(playerId))
GC->>GE: handleAction(state, action)
GE->>GE: Validate action
GE->>GE: Execute game logic
GE->>State: Create new GameState
State-->>GC: Return new state
GC-->>UI: Notify subscribers
UI-->>User: Re-render with new state
sequenceDiagram
participant GC as GameClient
participant Bot as BotAIAdapter
participant MCTS as MCTS Algorithm
participant GE as GameEngine
Note over GC: Bot's turn detected
GC->>Bot: executeBotTurn()
Bot->>MCTS: decideTurnAction(context)
MCTS-->>Bot: decision (draw/take discard)
Bot->>GC: dispatch(GameActions.drawCard(botId))
GC->>GE: handleAction(state, action)
GE-->>GC: new GameState
Note over GC: Same path as human!
graph LR
subgraph "Inputs"
Human[Human Player]
Bot[Bot AI]
end
subgraph "Game Logic"
Action[GameAction<br/>Serializable JSON]
Engine[GameEngine<br/>Pure Functions]
State[GameState<br/>Immutable]
end
subgraph "Outputs"
UI[UI Render]
Animations[Card Animations]
end
Human --> Action
Bot --> Action
Action --> Engine
Engine --> State
State --> UI
State --> Animations
style Engine fill:#90EE90
style State fill:#87CEEB
- Framework: Next.js 15 (App Router)
- Language: TypeScript (strict mode)
- Game Engine: Pure reducer pattern (Redux-inspired)
- State Management: MobX (UI stores only)
- Dependency Injection: tsyringe
- AI: Monte Carlo Tree Search (MCTS)
- Styling: Tailwind CSS
- Build Tool: Nx monorepo
apps/
vinto/ # Main Next.js app (UI, integration, entrypoint)
packages/
engine/ # Core game logic and rules (pure, deterministic, cloud-ready)
bot/ # AI bot logic for automated players
local-client/ # Client-side state management for local games (React hooks, context, services)
shapes/ # Shared types, interfaces, and constants used by all packages
- apps/vinto: The main application, integrating all packages and providing the user interface.
- engine: Implements all game rules, state transitions, and reducers. No UI or side effects.
- bot: Provides AI player logic, decision-making, and strategy for bots.
- local-client: Manages client-side state, user actions, and UI integration for local games.
- shapes: Exports TypeScript types, interfaces, and constants used by all packages.
- Node.js 18+
- npm or yarn
npm installRun the dev server:
npx nx dev vintoOpen http://localhost:4200 in your browser.
Create a production bundle:
npx nx build vintoAll game state lives in GameState (immutable). No parallel state in stores.
interface GameState {
players: PlayerState[];
currentPlayerIndex: number;
phase: GamePhase;
drawPile: Card[];
discardPile: Card[];
// ... complete game state
}GameEngine contains only pure functions (reducers):
- No side effects
- No async operations
- Deterministic
- Easily testable
- Can be hosted in cloud
function handleDrawCard(state: GameState, action: DrawCardAction): GameState {
// Pure function - no mutations
const newState = copy(state);
// ... game logic
return newState;
}All game interactions are represented as serializable actions:
type GameAction = { type: 'DRAW_CARD'; payload: { playerId: string } } | { type: 'SWAP_CARD'; payload: { playerId: string; position: number } } | { type: 'USE_CARD_ACTION'; payload: { playerId: string; card: Card } };
// ... all game actionsCurrent (Local):
gameClient.dispatch(GameActions.drawCard(playerId));
// GameEngine runs locallyFuture (Cloud/Multiplayer):
networkClient.dispatch(GameActions.drawCard(playerId));
// ↓ WebSocket to cloud
// Cloud GameEngine processes
// ↓ Broadcast new state to all clientsBots use the same action dispatch path as humans:
- MCTS algorithm decides action (client-side currently)
- Dispatches regular GameAction to engine
- Engine validates and executes (same as human actions)
- Can be moved to cloud GameEngine in future
- Lives in GameEngine
- Immutable
- Single source of truth
- Used by all components
- UIStore: Modals, toasts, temporary highlights
- CardAnimationStore: Animation state
These stores contain UI-specific state that doesn't affect game logic.
- Define action type in
engine/types/GameAction.ts:
export interface MyNewAction {
type: 'MY_NEW_ACTION';
payload: {
/* action data */
};
}- Create handler in
engine/cases/my-new-action.ts:
export function handleMyNewAction(state: GameState, action: MyNewAction): GameState {
const newState = copy(state);
// ... pure logic
return newState;
}- Add to engine in
engine/GameEngine.ts:
case 'MY_NEW_ACTION':
return handleMyNewAction(state, action);- Dispatch from UI:
gameClient.dispatch({
type: 'MY_NEW_ACTION',
payload: {
/* data */
},
});GameEngine is easy to test (pure functions):
test('drawing card updates state correctly', () => {
const initialState = createGameState();
const action = { type: 'DRAW_CARD', payload: { playerId: 'player1' } };
const newState = GameEngine.handleAction(initialState, action);
expect(newState.drawPile.length).toBe(initialState.drawPile.length - 1);
expect(newState.players[0].pendingCard).toBeDefined();
});The architecture is ready for multiplayer:
- Replace
GameClientwithNetworkClient - Host
GameEngineon server - Use WebSocket for action dispatch and state broadcast
Move MCTS algorithm to cloud GameEngine:
- Reduces client bundle size
- Enables spectator mode (no bot code needed)
- Server-side bot computation
Game state is fully serializable:
const saveData = JSON.stringify(gameClient.state);
localStorage.setItem('saved_game', saveData);Record all actions for replay:
const history: GameAction[] = [];
// Replay entire game by applying actions in orderThis project uses Nx for build optimization and task running.
View project graph:
npx nx graphShow available tasks:
npx nx show project vinto- Fork the repository
- Create a feature branch
- Make your changes
- Ensure tests pass and build succeeds
- Submit a pull request
MIT