@@ -3,6 +3,7 @@ import { Chess, Square } from "chess.js";
33import { InputJsonValue } from "@prisma/client/runtime/library" ;
44
55import {
6+ AuthProvider ,
67 ChatMessage ,
78 Game ,
89 GameStatus ,
@@ -18,6 +19,7 @@ import { WebSocketService } from "../../services/websocket";
1819import { redis } from "../../services/redis" ;
1920import { UserStatus } from "@prisma/client" ;
2021import { logger } from "../../services/logger" ;
22+ import { findBestMove } from "./bot/chess-bot" ;
2123
2224export class GameService {
2325 private ws : WebSocketService ;
@@ -97,6 +99,47 @@ export class GameService {
9799 logger . info ( "Master game timer has started" ) ;
98100 }
99101
102+ private async _persistGameEnd ( game : Game ) : Promise < void > {
103+ this . removeGameFromTimer ( game . id ) ;
104+
105+ try {
106+ await prisma . $transaction (
107+ async ( tx ) => {
108+ await tx . game . update ( {
109+ where : { id : game . id } ,
110+ data : {
111+ status : game . status ,
112+ winnerId : game . winnerId ,
113+ fen : game . fen ,
114+ moveHistory : game . moveHistory as unknown as InputJsonValue [ ] ,
115+ timers : game . timers as unknown as InputJsonValue ,
116+ } ,
117+ } ) ;
118+
119+ await tx . room . update ( {
120+ where : { id : game . roomId } ,
121+ data : { status : RoomStatus . CLOSED } ,
122+ } ) ;
123+
124+ await tx . user . updateMany ( {
125+ where : { id : { in : game . players . map ( ( p ) => p . userId ) } } ,
126+ data : { status : UserStatus . ONLINE } ,
127+ } ) ;
128+ } ,
129+ {
130+ maxWait : 10000 ,
131+ timeout : 20000 ,
132+ }
133+ ) ;
134+
135+ logger . info (
136+ `Game ${ game . id } ended and has been persisted to the database.`
137+ ) ;
138+ } catch ( error ) {
139+ logger . error ( `Failed to persist game end for ${ game . id } :` , error ) ;
140+ }
141+ }
142+
100143 public addGameToTimer ( gameId : string ) {
101144 this . activeGames . add ( gameId ) ;
102145 logger . info ( `Game ${ gameId } added to master timer.` ) ;
@@ -153,7 +196,7 @@ export class GameService {
153196 players : dbGame . players . map ( ( p ) => ( {
154197 userId : p . userId ,
155198 color : p . color ,
156- name : p . user . name ,
199+ name : p . user . name ! ,
157200 } ) ) ,
158201 chat : dbGame . chat as unknown as ChatMessage [ ] ,
159202 winnerId : dbGame . winnerId || undefined ,
@@ -306,6 +349,89 @@ export class GameService {
306349 return gameData ;
307350 }
308351
352+ async startBotGame ( playerId : string ) : Promise < Game > {
353+ const humanPlayer = await prisma . user . findUnique ( {
354+ where : {
355+ id : playerId ,
356+ } ,
357+ } ) ;
358+
359+ if ( ! humanPlayer ) throw new Error ( "Player not found" ) ;
360+
361+ let botUser = await prisma . user . findUnique ( {
362+ where : {
363+ username : "ChessBot" ,
364+ } ,
365+ } ) ;
366+ if ( ! botUser ) {
367+ botUser = await prisma . user . create ( {
368+ data : {
369+ username : "ChessBot" ,
370+ name : "Computer" ,
371+ email : `bot@${ Date . now ( ) } .chess` ,
372+ provider : AuthProvider . GUEST ,
373+ elo : 1400 ,
374+ } ,
375+ } ) ;
376+ }
377+
378+ const room = await prisma . room . create ( {
379+ data : {
380+ type : RoomType . PRIVATE ,
381+ status : RoomStatus . ACTIVE ,
382+ players : [
383+ { id : humanPlayer . id , color : "white" } ,
384+ { id : botUser . id , color : "black" } ,
385+ ] ,
386+ } ,
387+ } ) ;
388+
389+ const chess = new Chess ( ) ;
390+ const timeControl : TimeControl = { initial : 600 , increment : 0 } ;
391+
392+ const game = await prisma . game . create ( {
393+ data : {
394+ roomId : room . id ,
395+ fen : chess . fen ( ) ,
396+ status : GameStatus . ACTIVE ,
397+ timers : { white : timeControl . initial , black : timeControl . initial } ,
398+ timeControl : timeControl as unknown as InputJsonValue ,
399+ players : {
400+ create : [
401+ { userId : humanPlayer . id , color : "white" } ,
402+ { userId : botUser . id , color : "black" } ,
403+ ] ,
404+ } ,
405+ } ,
406+ } ) ;
407+
408+ const gameData : Game = {
409+ id : game . id ,
410+ roomId : game . roomId ,
411+ fen : game . fen ,
412+ moveHistory : [ ] ,
413+ timers : game . timers as { white : number ; black : number } ,
414+ timeControl : game . timeControl as unknown as TimeControl ,
415+ status : game . status as GameStatus ,
416+ players : [
417+ {
418+ userId : humanPlayer . id ,
419+ color : "white" ,
420+ name : humanPlayer . name || "Player" ,
421+ } ,
422+ { userId : botUser . id , color : "black" , name : "Computer" } ,
423+ ] ,
424+ chat : [ ] ,
425+ winnerId : undefined ,
426+ createdAt : game . createdAt ,
427+ } ;
428+
429+ await redis . setJSON ( `game:${ game . id } ` , gameData ) ;
430+ this . addGameToTimer ( game . id ) ;
431+
432+ return gameData ;
433+ }
434+
309435 async makeMove (
310436 gameId : string ,
311437 playerId : string ,
@@ -377,36 +503,68 @@ export class GameService {
377503 }
378504
379505 if ( game . status !== GameStatus . ACTIVE ) {
380- await prisma . $transaction ( async ( tx ) => {
381- await tx . game . update ( {
382- where : {
383- id : gameId ,
384- } ,
385- data : {
386- fen : game . fen ,
387- moveHistory : game . moveHistory as unknown as InputJsonValue [ ] ,
388- status : game . status ,
389- winnerId : game . winnerId ,
390- chat : game . chat as unknown as InputJsonValue [ ] ,
391- timers : game . timers as unknown as InputJsonValue ,
392- timeControl : game . timeControl as unknown as InputJsonValue ,
393- } ,
394- } ) ;
395-
396- await tx . room . update ( {
397- where : { id : game . roomId } ,
398- data : { status : RoomStatus . CLOSED } ,
399- } ) ;
400- } ) ,
401- {
402- maxWait : 10000 ,
403- timeout : 20000 ,
404- } ;
506+ await this . _persistGameEnd ( game ) ;
405507 }
406508
407509 await redis . setJSON ( `game:${ gameId } ` , game ) ;
408510
409511 this . ws . broadcastToGame ( game ) ;
512+
513+ const botPlayer = game . players . find ( ( p ) => p . name === "Computer" ) ;
514+ if (
515+ botPlayer &&
516+ game . status === GameStatus . ACTIVE &&
517+ chess . turn ( ) === botPlayer . color [ 0 ]
518+ ) {
519+ const thinkingTime = Math . floor ( Math . random ( ) * 8000 ) + 2000 ;
520+ setTimeout ( ( ) => {
521+ this . makeBotMove ( gameId ) ;
522+ } , thinkingTime ) ;
523+ }
524+ }
525+
526+ async makeBotMove ( gameId : string ) : Promise < void > {
527+ const gameData = await redis . get ( `game:${ gameId } ` ) ;
528+ if ( ! gameData ) return ;
529+
530+ const game = JSON . parse ( gameData ) as Game ;
531+ if ( game . status !== GameStatus . ACTIVE ) return ;
532+
533+ const chess = new Chess ( game . fen ) ;
534+ const botPlayer = game . players . find ( ( p ) => p . name === "Computer" ) ;
535+
536+ if ( ! botPlayer || chess . turn ( ) !== botPlayer . color [ 0 ] ) {
537+ return ;
538+ }
539+
540+ const botMove = findBestMove ( chess , 1 ) ;
541+
542+ if ( botMove ) {
543+ const botMoveResult = chess . move ( botMove . san ) ;
544+
545+ if ( botMoveResult ) {
546+ game . fen = chess . fen ( ) ;
547+ game . moveHistory . push ( {
548+ from : botMove . from ,
549+ to : botMove . to ,
550+ san : botMove . san ,
551+ } ) ;
552+
553+ if ( game . timeControl . increment > 0 ) {
554+ const botColor = botPlayer . color as "white" | "black" ;
555+ game . timers [ botColor ] += game . timeControl . increment ;
556+ }
557+
558+ if ( chess . isCheckmate ( ) || chess . isDraw ( ) ) {
559+ game . status = chess . isDraw ( ) ? GameStatus . DRAW : GameStatus . COMPLETED ;
560+ if ( chess . isCheckmate ( ) ) game . winnerId = botPlayer . userId ;
561+ await this . _persistGameEnd ( game ) ;
562+ }
563+
564+ await redis . setJSON ( `game:${ gameId } ` , game ) ;
565+ this . ws . broadcastToGame ( game ) ;
566+ }
567+ }
410568 }
411569
412570 async getLegalMoves (
0 commit comments