Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
adc00e7
Move Chessboard moduel to core-board
micnil Nov 11, 2025
7d58967
Add another test to socket router for makeing action
micnil Nov 11, 2025
0c69cda
Add RestRouter tests
micnil Nov 11, 2025
a32355b
Fix jest config warning
micnil Nov 11, 2025
3e43a4d
Update nx dependencies
micnil Nov 11, 2025
f774e00
Update minor deps
micnil Nov 11, 2025
05f35ad
Add admin plugin to better auth
micnil Nov 19, 2025
fb58f99
Add BotService
micnil Nov 19, 2025
dcc9bba
Add bots rest API
micnil Nov 19, 2025
0553a2e
Fix ts error in test
micnil Nov 20, 2025
3da9e7c
Fix ts error in test
micnil Nov 20, 2025
972ce11
Add API to challenge bots
micnil Nov 21, 2025
1f7bf9b
WIP structure for bot gameplay
micnil Nov 22, 2025
0ccf8ed
Add isBot to PlayerInfoV1
micnil Nov 24, 2025
3fe89f5
Use Chessboard to detect whos turn it is
micnil Nov 24, 2025
aaa91d2
Make llm bot move happen in a job
micnil Nov 24, 2025
8391e66
Improve prompt
micnil Nov 24, 2025
e40d09b
Fix logging
micnil Nov 25, 2025
49c8898
Add UI for play card
micnil Nov 25, 2025
8de2020
List actualy bots from API
micnil Nov 25, 2025
09e67b7
Move TimeControlRadioCards to play feature
micnil Nov 25, 2025
f30f89c
Disable play button when random opponent is selected
micnil Nov 25, 2025
6ca906b
Call challenge boendpoint when press Play
micnil Nov 25, 2025
a1f85ef
Update move made event data
micnil Nov 26, 2025
0674b07
Include more info to move_made event to improve botservice move handling
micnil Nov 30, 2025
528fd7d
Fix error on frontend for duplicate move events
micnil Nov 30, 2025
cff260e
Remove comments
micnil Nov 30, 2025
f450ea1
Move EventEmitter to separate lib
micnil Nov 30, 2025
c9a87f2
Merge pull request #18 from micnil/feature/llm
micnil Nov 30, 2025
6b03ef9
Emit move event data when player makes a move
micnil Nov 30, 2025
36d8021
Remove text about whos turn it is when game is over
micnil Nov 30, 2025
daec798
MVP WIP of matchmaking
micnil Dec 2, 2025
840eb33
Review and fix issues with matchmaking
micnil Dec 2, 2025
388fb7c
Work on the styling
micnil Dec 2, 2025
098c187
Initilization
micnil Dec 2, 2025
a9479e0
Update welcome message
micnil Dec 3, 2025
103b173
Update to latest nx
micnil Dec 3, 2025
9e557ff
Upgrade vitest and fix issues
micnil Dec 3, 2025
42c8de5
add vitest config
micnil Dec 3, 2025
11d3d12
Upgrade faro package
micnil Dec 3, 2025
1d769ab
Undo creating vitest config
micnil Dec 3, 2025
8b274b2
Add code coveerage merging
micnil Dec 3, 2025
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Thumbs.db
.next
.nx/cache
.nx/workspace-data
.nyc_output
.cursor/rules/nx-rules.mdc
.github/instructions/nx.instructions.md

Expand Down
5 changes: 5 additions & 0 deletions .nycrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"report-dir": "coverage",
"temp-dir": ".nyc_output",
"reporter": ["html", "json-summary", "text"]
}
3 changes: 3 additions & 0 deletions apps/node-chess/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ EMAIL_PASS=testpass
GOOGLE_OAUTH_CLIENT_SECRET=foo
GOOGLE_OAUTH_CLIENT_ID=bar

# LLM Configuration
GEMINI_API_KEY=your_gemini_api_key_here

WEB_APP_URL=http://localhost:4200

APP_PORT=5000
Expand Down
2 changes: 1 addition & 1 deletion apps/node-chess/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const start = (app: App, appConfig: AppConfig): ServerType => {
});

app.init().catch((err) => {
logger.error('Failed to initialize app:', err);
logger.error(err, 'Failed to initialize app');
process.exit(1);
});

Expand Down
2 changes: 2 additions & 0 deletions apps/node-chess/src/config/model/AppConfig.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LlmConfig } from '@michess/api-service';
import { EmailConfig } from '@michess/infra-email';

export type AppConfig = {
Expand All @@ -22,4 +23,5 @@ export type AppConfig = {
clientSecret: string;
};
};
llm: LlmConfig;
};
3 changes: 3 additions & 0 deletions apps/node-chess/src/config/service/AppConfigService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ const getConfig = (): AppConfig => {
clientSecret: readEnvStrict('GOOGLE_OAUTH_CLIENT_SECRET'),
},
},
llm: {
geminiApiKey: readEnvStrict('GEMINI_API_KEY'),
},
};
};

Expand Down
8 changes: 7 additions & 1 deletion apps/node-chess/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ const main = async () => {
);

const repos = Repositories.from(pgClient, redis);
const api = Api.from(repos, pgClient, emailClient, appConfig.auth);
const api = Api.from(
repos,
pgClient,
emailClient,
appConfig.auth,
appConfig.llm,
);
const app = App.from(api, redis, { cors: appConfig.cors });

Server.start(app, appConfig);
Expand Down
2 changes: 1 addition & 1 deletion apps/web-chess/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
}
},
"test": {
"executor": "@nx/vite:test",
"executor": "@nx/vitest:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../coverage/apps/web-chess"
Expand Down
4 changes: 4 additions & 0 deletions apps/web-chess/src/app/api/Api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { AuthClient } from './infra/AuthClient';
import { RestClient } from './infra/RestClient';
import { SocketClient } from './infra/SocketClient';
import { AuthService } from './service/AuthService';
import { BotService } from './service/BotService';
import { GameService } from './service/GameService';
import { MetricsService } from './service/MetricsService';

export type Api = {
games: GameService;
auth: AuthService;
metrics: MetricsService;
bots: BotService;
};

export const Api = {
Expand All @@ -20,10 +22,12 @@ export const Api = {
const auth = new AuthService(authClient, socketClient);
const games = new GameService(restClient, socketClient, auth);
const metrics = new MetricsService(restClient, socketClient);
const bots = new BotService(restClient);
return {
games,
auth,
metrics,
bots,
};
},
};
10 changes: 10 additions & 0 deletions apps/web-chess/src/app/api/service/BotService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { BotInfoV1 } from '@michess/api-schema';
import { RestClient } from '../infra/RestClient';

export class BotService {
constructor(private restClient: RestClient) {}

async listBots(): Promise<BotInfoV1[]> {
return this.restClient.get('bots').json();
}
}
59 changes: 59 additions & 0 deletions apps/web-chess/src/app/api/service/GameService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,61 @@ export class GameService {
return response;
}

async challengeBot(params: {
botId: string;
timeControl: { initialSec: number; incrementSec: number };
}): Promise<GameDetailsV1> {
const response = await this.restClient
.post<GameDetailsV1>('games/challenge', {
json: {
opponentId: params.botId,
variant: 'standard',
timeControl: {
type: 'realtime',
initialSec: params.timeControl.initialSec,
incrementSec: params.timeControl.incrementSec,
},
},
})
.json();
return response;
}

async joinMatchmakingQueue(params: {
timeControl: { initialSec: number; incrementSec: number };
}): Promise<void> {
await this.restClient
.post('matchmaking/join', {
json: {
variant: 'standard',
timeControl: {
type: 'realtime',
initialSec: params.timeControl.initialSec,
incrementSec: params.timeControl.incrementSec,
},
},
})
.json();
}

async leaveMatchmakingQueue(): Promise<void> {
await this.restClient.delete('matchmaking/leave').json();
}

observeMatchFound(): Observable<string> {
return {
subscribe: (callback) => {
const handler = (data: { gameId: string }) => {
callback(data.gameId);
};
this.socketClient.on('match-found', handler);
return () => {
this.socketClient.off('match-found', handler);
};
},
};
}

async getLobbyGames(page: number) {
const queryParams = new URLSearchParams({
page: page.toString(),
Expand Down Expand Up @@ -166,6 +221,10 @@ export class GameService {
gameId: string,
side?: 'white' | 'black' | 'spectator',
): Promise<PlayerGameViewModel> {
this.leaveMatchmakingQueue().catch(() => {
// Ignore errors if not in queue
});

const authState = await this.auth.getSession();
const response = await this.socketClient.emitWithAck('join-game', {
gameId,
Expand Down
4 changes: 4 additions & 0 deletions apps/web-chess/src/app/features/game/RemoteGameContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export const RemoteGameContainer = ({
gameState;
orientation = playerSide !== 'spectator' ? playerSide : orientation;

const isGameInProgress =
gameState.status === 'IN_PROGRESS' || gameState.status === 'READY';
const blackPlayer = { ...players.black, color: Color.Black };
const whitePlayer = { ...players.white, color: Color.White };
const currentOrientation = orientation || Color.White;
Expand Down Expand Up @@ -67,6 +69,7 @@ export const RemoteGameContainer = ({
color={topPlayer.color}
rating={topPlayer?.rating}
ratingDiff={topPlayer?.ratingDiff}
isGameInProgress={isGameInProgress}
isPlayerTurn={chessboard.position.turn === topPlayer.color}
isLoading={isLoadingInitial}
/>
Expand Down Expand Up @@ -95,6 +98,7 @@ export const RemoteGameContainer = ({
color={bottomPlayer.color}
rating={bottomPlayer?.rating}
ratingDiff={bottomPlayer?.ratingDiff}
isGameInProgress={isGameInProgress}
isPlayerTurn={chessboard.position.turn === bottomPlayer.color}
isLoading={isLoadingInitial}
/>
Expand Down
18 changes: 11 additions & 7 deletions apps/web-chess/src/app/features/game/components/PlayerInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type PlayerInfoProps = {
isLoading?: boolean;
clock?: CountdownClock;
rating?: string;
isGameInProgress?: boolean;
ratingDiff?: string;
};

Expand All @@ -27,6 +28,7 @@ export const PlayerInfo: React.FC<PlayerInfoProps> = ({
isPlayerTurn = false,
clock,
rating,
isGameInProgress,
ratingDiff,
isLoading,
}) => {
Expand Down Expand Up @@ -102,13 +104,15 @@ export const PlayerInfo: React.FC<PlayerInfoProps> = ({
</Badge>
</Skeleton>
<Skeleton loading={isLoading}>
<Text
size="2"
weight={isPlayerTurn ? 'bold' : 'medium'}
color={isPlayerTurn ? 'amber' : 'gray'}
>
{isPlayerTurn ? 'Turn to play' : 'Waiting'}
</Text>
{isGameInProgress && (
<Text
size="2"
weight={isPlayerTurn ? 'bold' : 'medium'}
color={isPlayerTurn ? 'amber' : 'gray'}
>
{isPlayerTurn ? 'Turn to play' : 'Waiting'}
</Text>
)}
</Skeleton>
</Flex>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Chessboard } from '@michess/core-game';
import { Chessboard } from '@michess/core-board';
import { useState } from 'react';
import { PeekActions } from '../model/PeekHandlers';

Expand Down
3 changes: 1 addition & 2 deletions apps/web-chess/src/app/features/game/hooks/useRemoteGame.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { GameActionOptionV1 } from '@michess/api-schema';
import { Maybe } from '@michess/common-utils';
import { ChessPosition, Move } from '@michess/core-board';
import { Chessboard } from '@michess/core-game';
import { Chessboard, ChessPosition, Move } from '@michess/core-board';
import { MovePayload } from '@michess/react-chessboard';
import { useMutation } from '@tanstack/react-query';
import { useEffect, useMemo } from 'react';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('GameLobby', () => {
items: [
{
id: 'game-1',
opponent: { name: 'Alice', id: 'alice-id' },
opponent: { name: 'Alice', id: 'alice-id', isBot: false },
availableColor: 'white',
variant: 'standard',
createdAt: new Date().toISOString(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Button, Flex, SegmentedControl, Switch, Text } from '@radix-ui/themes';
import { FC, useState } from 'react';
import { CreateGameInput } from '../../../api/model/CreateGameInput';
import { Alert } from '../../../components/Alert';
import { TimeControlRadioCards } from './TimeControlRadioGroup';
import { TimeControlRadioCards } from '../../play/components/TimeControlRadioCards';

type Props = {
onSubmit: (formInput: CreateGameInput) => void;
Expand Down
88 changes: 88 additions & 0 deletions apps/web-chess/src/app/features/play/PlayCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Maybe } from '@michess/common-utils';
import { Button, Card, Flex, Heading } from '@radix-ui/themes';
import { useState } from 'react';
import { Alert } from '../../components/Alert';
import { BotSelector } from './components/BotSelector';
import { OpponentTypeSelector } from './components/OpponentTypeSelector';
import { TimeControlSelector } from './components/TimeControlSelector';

type TimeControlStr = `${number}|${number}`;
type OpponentType = 'random' | 'bot';

type Props = {
onPlay?: (params: {
timeControl: TimeControlStr;
opponentType: OpponentType;
botId?: string;
}) => void;
onCancel?: () => void;
error?: Maybe<string>;
loading?: boolean;
isInQueue?: boolean;
};

export const PlayCard = ({
onPlay,
onCancel,
error,
loading,
isInQueue,
}: Props) => {
const [timeControl, setTimeControl] = useState<TimeControlStr>('3|2');
const [opponentType, setOpponentType] = useState<OpponentType>('random');
const [botId, setBotId] = useState<string | undefined>(undefined);

const handlePlay = () => {
onPlay?.({
timeControl,
opponentType,
botId: opponentType === 'bot' ? botId : undefined,
});
};

return (
<Card size="3" style={{ padding: '24px' }}>
<Flex direction="column" gap="4">
<Heading size="4" weight="medium">
Play
</Heading>

<Alert text={error} />

<Flex align="center">
<TimeControlSelector value={timeControl} onChange={setTimeControl} />
</Flex>

<OpponentTypeSelector value={opponentType} onChange={setOpponentType} />

{opponentType === 'bot' && (
<BotSelector value={botId} onChange={setBotId} />
)}

<Flex align="center">
{isInQueue ? (
<Button
size="2"
onClick={onCancel}
variant="soft"
color="red"
style={{ minWidth: '120px' }}
>
Cancel
</Button>
) : (
<Button
size="2"
onClick={handlePlay}
disabled={opponentType === 'bot' && !botId}
loading={loading}
style={{ minWidth: '120px' }}
>
{loading ? 'Finding opponent...' : 'Play'}
</Button>
)}
</Flex>
</Flex>
</Card>
);
};
Loading