Skip to content

Commit c6d7e38

Browse files
Update instructions and reduce type union duplication
1 parent bf807ef commit c6d7e38

File tree

12 files changed

+196
-35
lines changed

12 files changed

+196
-35
lines changed

.github/instructions/mcp-server.instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ Implement consistent difficulty levels across all games:
145145
```typescript
146146
export function calculateTicTacToeMove(
147147
gameState: TicTacToeGameState,
148-
difficulty: 'easy' | 'medium' | 'hard' = 'medium'
148+
difficulty: Difficulty = 'medium'
149149
): TicTacToeMove {
150150
const validMoves = getValidMoves(gameState)
151151

.github/instructions/shared-library.instructions.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,89 @@ export function getGameDisplayName(type: GameType): string {
140140
}
141141
```
142142

143+
## Constants and Common Values
144+
145+
### Centralized Constants and Derived Types
146+
**Types are derived from constants using `as const` assertions - constants are the single source of truth:**
147+
148+
```typescript
149+
// ✅ Constants define the source of truth
150+
export const DIFFICULTIES = ['easy', 'medium', 'hard'] as const
151+
export const GAME_TYPES = ['tic-tac-toe', 'rock-paper-scissors'] as const
152+
export const PLAYER_IDS = { HUMAN: 'player1', PLAYER2: 'player2', AI: 'ai' } as const
153+
154+
// ✅ Types are derived from constants
155+
export type Difficulty = typeof DIFFICULTIES[number]
156+
export type GameType = typeof GAME_TYPES[number]
157+
export type PlayerId = typeof PLAYER_IDS[keyof typeof PLAYER_IDS]
158+
159+
// ✅ Import the derived types
160+
import type { Difficulty, GameType } from '@turn-based-mcp/shared'
161+
162+
// ❌ Don't define duplicate union types
163+
export type Difficulty = 'easy' | 'medium' | 'hard' // This duplicates the constants!
164+
```
165+
166+
### Available Constants
167+
Key constants provided by the shared library:
168+
169+
```typescript
170+
// Constants with derived types
171+
export const GAME_TYPES = ['tic-tac-toe', 'rock-paper-scissors'] as const
172+
export const DIFFICULTIES = ['easy', 'medium', 'hard'] as const
173+
export const PLAYER_IDS = { HUMAN: 'player1', PLAYER2: 'player2', AI: 'ai' } as const
174+
export const GAME_STATUSES = ['waiting', 'playing', 'finished'] as const
175+
176+
// Derived types (auto-generated from constants)
177+
export type GameType = typeof GAME_TYPES[number]
178+
export type Difficulty = typeof DIFFICULTIES[number]
179+
export type PlayerId = typeof PLAYER_IDS[keyof typeof PLAYER_IDS]
180+
export type GameStatus = typeof GAME_STATUSES[number]
181+
182+
// Default values
183+
export const DEFAULT_PLAYER_NAME = 'Player'
184+
export const DEFAULT_AI_DIFFICULTY: Difficulty = 'medium'
185+
186+
// UI display configuration
187+
export const DIFFICULTY_DISPLAY = {
188+
easy: { emoji: '😌', label: 'Easy' },
189+
medium: { emoji: '🎯', label: 'Medium' },
190+
hard: { emoji: '🔥', label: 'Hard' }
191+
} as const
192+
```
193+
194+
### Type Guards and Utilities
195+
Use provided validation functions that work with the constants:
196+
197+
```typescript
198+
// Type guards (check against the constant arrays)
199+
export function isSupportedGameType(gameType: string): gameType is GameType
200+
export function isValidDifficulty(difficulty: string): difficulty is Difficulty
201+
export function isValidPlayerId(playerId: string): playerId is PlayerId
202+
203+
// Display helpers
204+
export function getDifficultyDisplay(difficulty: Difficulty)
205+
```
206+
207+
### Architecture Benefits
208+
This approach ensures:
209+
- **Single source of truth**: Constants define what values are valid
210+
- **Type safety**: TypeScript derives exact types from the constant values
211+
- **Runtime validation**: Type guards check against the same arrays used to derive types
212+
- **Maintainability**: Add a new difficulty by updating one constant array
213+
214+
### Testing Constants
215+
For mocking and test data, use shared testing utilities:
216+
217+
```typescript
218+
// Test data from shared/src/testing/
219+
import {
220+
mockTicTacToeGameState,
221+
mockRPSGameState,
222+
createMockGameSession
223+
} from '@turn-based-mcp/shared/testing'
224+
```
225+
143226
## Testing Infrastructure
144227

145228
### Test Database Utilities

.github/instructions/testing.instructions.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,33 @@ Follow these testing patterns for the turn-based games platform:
3131
})
3232
```
3333

34+
## Shared Testing Utilities
35+
36+
**Use centralized mock data and test utilities from the shared package:**
37+
38+
```typescript
39+
// ✅ Import shared testing utilities
40+
import {
41+
mockTicTacToeGameState,
42+
mockRPSGameState,
43+
createMockGameSession,
44+
setupTestDatabase,
45+
clearTestDatabase
46+
} from '@turn-based-mcp/shared/testing'
47+
48+
// ✅ Use shared constants in tests
49+
import { DIFFICULTIES, GAME_TYPES } from '@turn-based-mcp/shared'
50+
51+
// ❌ Don't recreate mock data locally
52+
const localMockGameState = { /* duplicated data */ } // Use shared mocks instead!
53+
```
54+
55+
**Available shared testing utilities:**
56+
- Mock game states: `mockTicTacToeGameState`, `mockRPSGameState`
57+
- Factory functions: `createMockGameSession`, `createMockPlayer`
58+
- Database utilities: `setupTestDatabase`, `clearTestDatabase`, `teardownTestDatabase`
59+
- Type assertions and validation helpers
60+
3461
## Component Testing
3562

3663
- Always render components with realistic props

.github/instructions/typescript.instructions.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,37 @@ Follow these TypeScript patterns for consistent, type-safe code:
1515
- Use barrel exports (`index.ts`) for clean imports
1616
- Re-export shared types from `@turn-based-mcp/shared`
1717

18+
### Shared Types and Constants
19+
**Always import types derived from shared constants - don't duplicate union types:**
20+
21+
```typescript
22+
// ✅ Import types derived from constants
23+
import type { Difficulty, GameType, PlayerId } from '@turn-based-mcp/shared'
24+
import { DIFFICULTIES, DEFAULT_AI_DIFFICULTY, GAME_TYPES, PLAYER_IDS } from '@turn-based-mcp/shared'
25+
26+
// ✅ Use the imported types
27+
const [aiDifficulty, setAiDifficulty] = useState<Difficulty>('medium')
28+
const playerIds: PlayerId[] = Object.values(PLAYER_IDS)
29+
30+
// ❌ Don't define duplicate union types
31+
type Difficulty = 'easy' | 'medium' | 'hard' // This duplicates shared constants!
32+
type PlayerId = 'player1' | 'player2' | 'ai' // Use the derived type instead!
33+
```
34+
35+
**Key principle: Types are derived from constants using `as const` assertions:**
36+
```typescript
37+
// In shared/src/constants/game-constants.ts
38+
export const DIFFICULTIES = ['easy', 'medium', 'hard'] as const
39+
export type Difficulty = typeof DIFFICULTIES[number] // 'easy' | 'medium' | 'hard'
40+
```
41+
42+
**Common types available from shared package:**
43+
- `Difficulty` - AI difficulty levels (derived from `DIFFICULTIES`)
44+
- `GameType` - Supported game types (derived from `GAME_TYPES`)
45+
- `PlayerId` - Player identifiers (derived from `PLAYER_IDS`)
46+
- `GameStatus` - Game state values (derived from `GAME_STATUSES`)
47+
- Game-specific interfaces: `TicTacToeGameState`, `RPSGameState`, etc.
48+
1849
### Interface Design
1950
- Use interfaces for object shapes and component props
2051
- Include JSDoc comments for complex properties

mcp-server/src/ai/rock-paper-scissors-ai.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import type { RPSGameState, RPSMove, RPSChoice } from '@turn-based-mcp/shared'
1+
import type { RPSGameState, RPSMove, RPSChoice, Difficulty } from '@turn-based-mcp/shared'
22
import { RockPaperScissorsGame } from '@turn-based-mcp/shared'
33

44
export type Strategy = 'random' | 'adaptive' | 'pattern'
5-
export type Difficulty = 'easy' | 'medium' | 'hard'
65

76
/**
87
* AI opponent for Rock Paper Scissors with multiple strategic approaches

mcp-server/src/ai/tic-tac-toe-ai.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import type { TicTacToeGameState, TicTacToeMove } from '@turn-based-mcp/shared'
1+
import type { TicTacToeGameState, TicTacToeMove, Difficulty } from '@turn-based-mcp/shared'
22
import { TicTacToeGame } from '@turn-based-mcp/shared'
33

4-
export type Difficulty = 'easy' | 'medium' | 'hard'
5-
64
/**
75
* AI opponent for Tic-Tac-Toe with configurable difficulty levels
86
*

shared/src/constants/game-constants.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,46 @@
11
/**
22
* Shared game constants
3-
* Single source of truth for game types, difficulties, and default values
3+
* Single source of truth for game types, difficulties, player IDs, and default values
44
*/
55

6-
import type { GameType, Difficulty } from '../types/game'
7-
86
/**
97
* Supported game types
108
*/
11-
export const GAME_TYPES: readonly GameType[] = ['tic-tac-toe', 'rock-paper-scissors'] as const
9+
export const GAME_TYPES = ['tic-tac-toe', 'rock-paper-scissors'] as const
1210

1311
/**
1412
* Available AI difficulty levels
1513
*/
16-
export const DIFFICULTIES: readonly Difficulty[] = ['easy', 'medium', 'hard'] as const
17-
18-
/**
19-
* Default player configurations
20-
*/
21-
export const DEFAULT_PLAYER_NAME = 'Player'
22-
export const DEFAULT_AI_DIFFICULTY: Difficulty = 'medium'
14+
export const DIFFICULTIES = ['easy', 'medium', 'hard'] as const
2315

2416
/**
2517
* Standard player IDs used across the system
2618
*/
2719
export const PLAYER_IDS = {
2820
HUMAN: 'player1',
21+
PLAYER2: 'player2',
2922
AI: 'ai'
3023
} as const
3124

25+
/**
26+
* Game status values
27+
*/
28+
export const GAME_STATUSES = ['waiting', 'playing', 'finished'] as const
29+
30+
/**
31+
* Derive types from constants
32+
*/
33+
export type GameType = typeof GAME_TYPES[number]
34+
export type Difficulty = typeof DIFFICULTIES[number]
35+
export type PlayerId = typeof PLAYER_IDS[keyof typeof PLAYER_IDS]
36+
export type GameStatus = typeof GAME_STATUSES[number]
37+
38+
/**
39+
* Default player configurations
40+
*/
41+
export const DEFAULT_PLAYER_NAME = 'Player'
42+
export const DEFAULT_AI_DIFFICULTY: Difficulty = 'medium'
43+
3244
/**
3345
* Difficulty display configuration
3446
*/
@@ -52,6 +64,20 @@ export function isValidDifficulty(difficulty: string): difficulty is Difficulty
5264
return DIFFICULTIES.includes(difficulty as Difficulty)
5365
}
5466

67+
/**
68+
* Type guard to check if a string is a valid player ID
69+
*/
70+
export function isValidPlayerId(playerId: string): playerId is PlayerId {
71+
return Object.values(PLAYER_IDS).includes(playerId as PlayerId)
72+
}
73+
74+
/**
75+
* Type guard to check if a string is a valid game status
76+
*/
77+
export function isValidGameStatus(status: string): status is GameStatus {
78+
return GAME_STATUSES.includes(status as GameStatus)
79+
}
80+
5581
/**
5682
* Get difficulty display configuration
5783
*/

shared/src/testing/api-test-utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77

88
import { vi } from 'vitest'
9-
import type { GameSession, Player, BaseGameState } from '../types/game'
9+
import type { GameSession, Player, BaseGameState, GameType } from '../types/game'
1010
import type { TicTacToeGameState, RPSGameState, TicTacToeMove, RPSMove } from '../types/games'
1111

1212
/**
@@ -82,7 +82,7 @@ export function createMockRPSGameState(overrides: Partial<RPSGameState> = {}): R
8282
/**
8383
* Create a mock game session
8484
*/
85-
export function createMockGameSession<T extends BaseGameState>(gameState: T, gameType: 'tic-tac-toe' | 'rock-paper-scissors'): GameSession<T> {
85+
export function createMockGameSession<T extends BaseGameState>(gameState: T, gameType: GameType): GameSession<T> {
8686
return {
8787
gameState,
8888
gameType,
@@ -110,7 +110,7 @@ export function createSharedGameMocks(gameClass: string) {
110110
/**
111111
* Create storage function mocks for a specific game type
112112
*/
113-
export function createStorageMocks(gameType: 'tic-tac-toe' | 'rock-paper-scissors') {
113+
export function createStorageMocks(gameType: GameType) {
114114
if (gameType === 'tic-tac-toe') {
115115
return {
116116
getTicTacToeGame: vi.fn(),

shared/src/types/game.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
// Core game types
2-
export type PlayerId = 'player1' | 'player2' | 'ai';
2+
import type { PlayerId, GameStatus, GameType, Difficulty } from '../constants/game-constants'
3+
4+
export type { PlayerId, GameStatus, GameType, Difficulty }
35

46
export interface Player {
57
id: PlayerId;
68
name: string;
79
isAI: boolean;
810
}
911

10-
export type GameStatus = 'waiting' | 'playing' | 'finished';
11-
1212
export interface BaseGameState {
1313
id: string;
1414
players: Player[];
@@ -39,10 +39,6 @@ export interface Game<TGameState extends BaseGameState, TMove> {
3939
getInitialState(players: Player[], options?: any): TGameState;
4040
}
4141

42-
export type GameType = 'tic-tac-toe' | 'rock-paper-scissors';
43-
44-
export type Difficulty = 'easy' | 'medium' | 'hard';
45-
4642
// Generic game session for API communication
4743
export interface GameSession<TGameState extends BaseGameState = BaseGameState> {
4844
gameState: TGameState;

web/src/app/games/rock-paper-scissors/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { RPSGameBoard } from '../../../components/games/RPSGameBoard'
55
import { GameInfoPanel } from '../../../components/games/GameInfoPanel'
66
import { GameContainer, GameControls, ConfirmationModal } from '../../../components/ui'
77
import { MCPAssistantPanel } from '../../../components/shared'
8-
import type { RPSGameState, RPSMove } from '@turn-based-mcp/shared'
8+
import type { RPSGameState, RPSMove, Difficulty } from '@turn-based-mcp/shared'
99
import type { GameSession } from '@turn-based-mcp/shared'
1010

1111
export default function RockPaperScissorsPage() {
@@ -16,7 +16,7 @@ export default function RockPaperScissorsPage() {
1616
const [availableGames, setAvailableGames] = useState<GameSession<RPSGameState>[]>([])
1717
const [showCreateForm, setShowCreateForm] = useState(false)
1818
const [showJoinForm, setShowJoinForm] = useState(false)
19-
const [aiDifficulty, setAiDifficulty] = useState<'easy' | 'medium' | 'hard'>('medium')
19+
const [aiDifficulty, setAiDifficulty] = useState<Difficulty>('medium')
2020
const [gamesToShow, setGamesToShow] = useState(5)
2121
const [showDeleteModal, setShowDeleteModal] = useState(false)
2222
const [gameToDelete, setGameToDelete] = useState<string | null>(null)
@@ -431,7 +431,7 @@ export default function RockPaperScissorsPage() {
431431
</label>
432432
<select
433433
value={aiDifficulty}
434-
onChange={(e) => setAiDifficulty(e.target.value as 'easy' | 'medium' | 'hard')}
434+
onChange={(e) => setAiDifficulty(e.target.value as Difficulty)}
435435
className="w-full px-4 py-3 bg-white/60 dark:bg-slate-700/60 border border-slate-200 dark:border-slate-600 rounded-xl focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-transparent transition-all duration-200"
436436
>
437437
<option value="easy">🟢 Easy - Random moves</option>

0 commit comments

Comments
 (0)