diff --git a/config/locales/ar.json b/config/locales/ar.json index 3d01d020..a7b6a0ff 100644 --- a/config/locales/ar.json +++ b/config/locales/ar.json @@ -23,6 +23,8 @@ "end": "لم يفز أي أحد في هذه الجولة, هل تريدون لعب مرة اخرى؟", "win": ":tada: فاز {player} في هذه الجولة", "expire": ":x: **انتهت** اللعبة ... بسبب عدم تفاعل", + "in-progress": "you cannot start another game, please wait for current one to end.", "waiting-ai": ":robot: الروبوت يلعب, انتضر رجاءً...", "ai": "الروبوت" } +} diff --git a/config/locales/de.json b/config/locales/de.json index 344b0828..daddba2a 100644 --- a/config/locales/de.json +++ b/config/locales/de.json @@ -23,6 +23,7 @@ "end": "Kein Sieger, unentschieden! Nochmal?", "win": ":tada: {player} hat gewonnen!", "expire": ":x: Spiel ist **abgelaufen**... da war wohl keiner mehr da.", + "in-progress": "you cannot start another game, please wait for current one to end.", "waiting-ai": ":robot: KI ist am Zug, bitte warten...", "ai": "KI" } diff --git a/config/locales/en.json b/config/locales/en.json index 5525460d..30763a8c 100644 --- a/config/locales/en.json +++ b/config/locales/en.json @@ -23,6 +23,7 @@ "end": "No one won the game, it's a tie! Let's try again?", "win": ":tada: {player} has won the game!", "expire": ":x: Game has **expired**... maybe because of one user inactivity.", + "in-progress": "you cannot start another game, please wait for current one to end.", "waiting-ai": ":robot: AI is playing, please wait...", "ai": "the AI" } diff --git a/config/locales/es.json b/config/locales/es.json index 3357ea0b..54a0991f 100644 --- a/config/locales/es.json +++ b/config/locales/es.json @@ -23,6 +23,7 @@ "end": "Nadie ganó el juego, ¡es un empate! ¿Intentemoslo de nuevo?", "win": ":tada: {player} ha ganado el juego!", "expire": ":x: El juego ha **caducado**... quizás debido a la inactividad de un usuario.", + "in-progress": "you cannot start another game, please wait for current one to end.", "waiting-ai": ":robot: La IA está jugando, por favor espere...", "ai": "La IA" } diff --git a/config/locales/fr.json b/config/locales/fr.json index 90e864bf..4aa6c020 100644 --- a/config/locales/fr.json +++ b/config/locales/fr.json @@ -23,6 +23,7 @@ "end": "Personne n'a gagné la partie, c'est une égalité ! On réessaie ?", "win": ":tada: {player} a gagné la partie !", "expire": ":x: La partie vient **d'expirer**... peut-être à cause de l'inactivité d'un joueur.", + "in-progress": "vous ne pouvez pas démarrer de partie, attendez que celle en cours se termine.", "waiting-ai": ":robot: L'IA joue, merci de patienter...", "ai": "l'IA" } diff --git a/config/locales/id.json b/config/locales/id.json index aaf2bac9..9fcd03a7 100644 --- a/config/locales/id.json +++ b/config/locales/id.json @@ -23,6 +23,7 @@ "end": "Tidak ada yang menang, Ini seri! Coba lagi?", "win": ":tada: {player} Memenangkan permainan", "expire": ":x: Game telah **Kadaluarsa**... Mungkin salah satu pemain tidak aktif", + "in-progress": "you cannot start another game, please wait for current one to end.", "waiting-ai": ":robot: AI Sedang bermain, tunggu sebentar...", "ai": "sang AI" } diff --git a/config/locales/it.json b/config/locales/it.json index 37223270..34f54711 100644 --- a/config/locales/it.json +++ b/config/locales/it.json @@ -23,6 +23,7 @@ "end": "Che peccato, non ha vinto nessuno! Vuoi rigiocare?", "win": ":tada: {player} ha vinto!", "expire": ":x: Il tempo è **scaduto**... forse perché i giocatori non sono più online.", + "in-progress": "you cannot start another game, please wait for current one to end.", "waiting-ai": ":robot: L'IA sta' giocando, aspetta il tuo turno...", "ai": "l'IA" } diff --git a/config/locales/nl.json b/config/locales/nl.json index b28d20a7..2da9a446 100644 --- a/config/locales/nl.json +++ b/config/locales/nl.json @@ -23,6 +23,7 @@ "end": "Gelijk spel! Niemand heeft gewonnen! Nog een keer?", "win": ":tada: {player} heeft gewonnen!", "expire": ":x: Het spel is **verlopen**... Misschien is iemand niet actief?", + "in-progress": "you cannot start another game, please wait for current one to end.", "waiting-ai": ":robot: De AI is aan het spelen, eventjes geduld...", "ai": "AI" } diff --git a/config/locales/pl.json b/config/locales/pl.json index bb936bc1..cc6c826c 100644 --- a/config/locales/pl.json +++ b/config/locales/pl.json @@ -23,6 +23,7 @@ "end": "Remis! Nikt nie wygrał. Może spróbujcie jeszcze raz?", "win": ":tada: {player} wygrał grę w kółko i krzyżyk!", "expire": ":x: Gra **wygasła**... Może przez nieaktywność któregoś z graczy?", + "in-progress": "you cannot start another game, please wait for current one to end.", "waiting-ai": ":robot: Trwa ruch Sztucznej Inteligencji, proszę czekać...", "ai": "Sztuczna Inteligencja" } diff --git a/config/locales/pt-br.json b/config/locales/pt-br.json index 175870a7..e5e7a495 100644 --- a/config/locales/pt-br.json +++ b/config/locales/pt-br.json @@ -23,6 +23,7 @@ "end": "Ninguém ganhou o jogo, empate! Quer tentar novamente?", "win": ":tada: {player} venceu a partida!", "expire": ":x: O jogo foi **expirado**... Talvez por causa de inatividade de um usuário.", + "in-progress": "you cannot start another game, please wait for current one to end.", "waiting-ai": ":robot: AI está jogando, espere um momento...", "ai": "a AI" } diff --git a/config/locales/ru.json b/config/locales/ru.json index efc997e4..e40bf5fa 100644 --- a/config/locales/ru.json +++ b/config/locales/ru.json @@ -23,6 +23,7 @@ "end": "Ничья! Может быть снова попробуем?)", "win": ":tada:{player} Выиграл игру!", "expire": ":x: Игра была досрочно завершена... Может потому что никто не активил?", + "in-progress": "you cannot start another game, please wait for current one to end.", "waiting-ai": ":robot: Компьютер думает, подождите...", "ai": "Кампудахтр" } diff --git a/config/locales/tr.json b/config/locales/tr.json index 05caef3c..b32f9f86 100644 --- a/config/locales/tr.json +++ b/config/locales/tr.json @@ -23,6 +23,7 @@ "end": "Maçı kimse kazanamadı, berabere kaldı!", "win": ":tada: {player} oyunu kazandı!", "expire": ":x: Oyunun süresi **doldu**... Bir kullanıcının hareketsizliğinden olabilir", + "in-progress": "you cannot start another game, please wait for current one to end.", "waiting-ai": ":robot: Yapay Zeka oynuyor, lütfen bekle...", "ai": "Yapay Zeka" } diff --git a/config/locales/vi.json b/config/locales/vi.json index 1c9146bc..db3fe1ce 100644 --- a/config/locales/vi.json +++ b/config/locales/vi.json @@ -23,6 +23,7 @@ "end": "Không ai thắng màn thách đấu này. Thử lại chứ?", "win": ":tada: {player} đã thắng ván đấu!", "expire": ":x: Ván đấu đã **hết giờ**... Bởi vì một người đã không tương tác.", + "in-progress": "you cannot start another game, please wait for current one to end.", "waiting-ai": ":robot: Máy đang chơi, chờ chút nha...", "ai": "Máy" } diff --git a/src/__tests__/GameStateManager.test.ts b/src/__tests__/GameStateManager.test.ts index 71886039..f1bc91ea 100644 --- a/src/__tests__/GameStateManager.test.ts +++ b/src/__tests__/GameStateManager.test.ts @@ -5,6 +5,7 @@ import MessagingTunnel from '@bot/messaging/MessagingTunnel'; import GameStateManager from '@bot/state/GameStateManager'; import GameStateValidator from '@bot/state/GameStateValidator'; import TicTacToeBot from '@bot/TicTacToeBot'; +import AI from '@tictactoe/AI'; import Entity from '@tictactoe/Entity'; import { GuildMember } from 'discord.js'; @@ -46,11 +47,21 @@ describe('GameStateManager', () => { const invited = {}; await manager.requestDuel(tunnel, invited); expect(spyValidate).toHaveBeenCalledTimes(1); + expect(spyValidate).toHaveBeenCalledWith(tunnel); + }); + + it('should check if a new game is possible', async () => { + jest.spyOn(validator, 'isInteractionValid').mockReturnValue(true); + const spyValidate = jest.spyOn(validator, 'isNewGamePossible'); + const invited = {}; + await manager.requestDuel(tunnel, invited); + expect(spyValidate).toHaveBeenCalledTimes(1); expect(spyValidate).toHaveBeenCalledWith(tunnel, invited); }); it('should create a duel request and send it into the messaging tunnel', async () => { jest.spyOn(validator, 'isInteractionValid').mockReturnValue(true); + jest.spyOn(validator, 'isNewGamePossible').mockReturnValue(true); const spyReplyWith = jest.spyOn(tunnel, 'replyWith'); await manager.requestDuel(tunnel, {}); expect(duelRequest).toHaveBeenCalledTimes(1); @@ -59,6 +70,7 @@ describe('GameStateManager', () => { it('should setup user cooldown if enabled in configuration', async () => { jest.spyOn(validator, 'isInteractionValid').mockReturnValue(true); + jest.spyOn(validator, 'isNewGamePossible').mockReturnValue(true); // by default, no cooldown await manager.requestDuel(tunnel, {}); @@ -77,19 +89,46 @@ describe('GameStateManager', () => { const spyValidate = jest.spyOn(validator, 'isInteractionValid'); await manager.createGame(tunnel); expect(spyValidate).toHaveBeenCalledTimes(1); - expect(spyValidate).toHaveBeenCalledWith(tunnel, undefined); + expect(spyValidate).toHaveBeenCalledWith(tunnel); + }); + + it('should check if a new game is possible', async () => { + jest.spyOn(validator, 'isInteractionValid').mockReturnValue(true); + const spyValidate = jest.spyOn(validator, 'isNewGamePossible'); + const invited = {}; + await manager.createGame(tunnel, invited); + expect(spyValidate).toHaveBeenCalledTimes(1); + expect(spyValidate).toHaveBeenCalledWith(tunnel, invited); }); it('should create a game board and send it into the messaging tunnel', async () => { jest.spyOn(validator, 'isInteractionValid').mockReturnValue(true); + jest.spyOn(validator, 'isNewGamePossible').mockReturnValue(true); const spyReplyWith = jest.spyOn(tunnel, 'replyWith'); - await manager.createGame(tunnel); + const invited = {}; + await manager.createGame(tunnel, invited); expect(manager.gameboards).toHaveLength(1); expect(gameBoard).toHaveBeenCalledTimes(1); + expect(gameBoard).toHaveBeenCalledWith(manager, tunnel, invited, expect.anything()); expect(spyReplyWith).toHaveBeenCalledTimes(1); }); + + it('should create a game board with AI if no invited member', async () => { + jest.spyOn(validator, 'isInteractionValid').mockReturnValue(true); + jest.spyOn(validator, 'isNewGamePossible').mockReturnValue(true); + + await manager.createGame(tunnel); + + expect(gameBoard).toHaveBeenCalledTimes(1); + expect(gameBoard).toHaveBeenCalledWith( + manager, + tunnel, + expect.any(AI), + expect.anything() + ); + }); }); describe('Method: endGame', () => { diff --git a/src/__tests__/GameStateValidator.test.ts b/src/__tests__/GameStateValidator.test.ts index 27d4161a..bd5401d1 100644 --- a/src/__tests__/GameStateValidator.test.ts +++ b/src/__tests__/GameStateValidator.test.ts @@ -136,7 +136,7 @@ describe('GameStateValidator', () => { } manager.bot.configuration.simultaneousGames = simultaneousGames; const invited = sameInvited ? tunnel.author : undefined; - expect(validator.isInteractionValid(tunnel, invited)).toBe(expected); + expect(validator.isNewGamePossible(tunnel, invited)).toBe(expected); } ); }); diff --git a/src/bot/command/GameCommand.ts b/src/bot/command/GameCommand.ts index 5b5cec23..6ee9dfee 100644 --- a/src/bot/command/GameCommand.ts +++ b/src/bot/command/GameCommand.ts @@ -100,7 +100,9 @@ export default class GameCommand { inviter.user.id !== invited.user.id && invited.permissionsIn(tunnel.channel).has('VIEW_CHANNEL') ) { - await this.manager.requestDuel(tunnel, invited); + if (!(await this.manager.requestDuel(tunnel, invited))) { + await tunnel.replyWith({ content: localize.__('game.in-progress') }, true); + } } else { await tunnel.replyWith({ content: localize.__('duel.unknown-user') }, true); } @@ -108,7 +110,9 @@ export default class GameCommand { await tunnel.replyWith({ content: localize.__('duel.no-bot') }, true); } } else { - await this.manager.createGame(tunnel); + if (!(await this.manager.createGame(tunnel))) { + await tunnel.replyWith({ content: localize.__('game.in-progress') }, true); + } } } } diff --git a/src/bot/entity/DuelRequest.ts b/src/bot/entity/DuelRequest.ts index 1bf6a378..d890ec77 100644 --- a/src/bot/entity/DuelRequest.ts +++ b/src/bot/entity/DuelRequest.ts @@ -182,7 +182,7 @@ export default class DuelRequest { private async challengeAnswered(accepted: boolean): Promise { if (accepted) { await this.tunnel.end(); - return this.manager.createGame(this.tunnel, this.invited); + await this.manager.createGame(this.tunnel, this.invited); } else { return this.tunnel.end({ allowedMentions: { parse: [] }, diff --git a/src/bot/state/GameStateManager.ts b/src/bot/state/GameStateManager.ts index afc501e8..b534fed2 100644 --- a/src/bot/state/GameStateManager.ts +++ b/src/bot/state/GameStateManager.ts @@ -49,9 +49,14 @@ export default class GameStateManager { * * @param tunnel messaging tunnel that initiated the request * @param invited member invited to be part of the duel + * @returns true if duel request has been handled, false otherwise */ - public async requestDuel(tunnel: MessagingTunnel, invited: GuildMember): Promise { - if (this.validator.isInteractionValid(tunnel, invited)) { + public async requestDuel(tunnel: MessagingTunnel, invited: GuildMember): Promise { + if (this.validator.isInteractionValid(tunnel)) { + if (!this.validator.isNewGamePossible(tunnel, invited)) { + return false; + } + const duel = new DuelRequest( this, tunnel, @@ -70,6 +75,8 @@ export default class GameStateManager { this.memberCooldownEndTimes.set(tunnel.author.id, Date.now() + cooldown * 1000); } } + + return true; } /** @@ -77,9 +84,14 @@ export default class GameStateManager { * * @param tunnel messaging tunnel that initiated the game creation * @param invited member invited to be part of the game, undefined means the AI + * @returns true if game request has been handled, false otherwise */ - public async createGame(tunnel: MessagingTunnel, invited?: GuildMember): Promise { - if (this.validator.isInteractionValid(tunnel, invited)) { + public async createGame(tunnel: MessagingTunnel, invited?: GuildMember): Promise { + if (this.validator.isInteractionValid(tunnel)) { + if (!this.validator.isNewGamePossible(tunnel, invited)) { + return false; + } + const gameboard = new GameBoard( this, tunnel, @@ -94,6 +106,8 @@ export default class GameStateManager { const message = await tunnel.replyWith(gameboard.content); await gameboard.attachTo(message); } + + return true; } /** diff --git a/src/bot/state/GameStateValidator.ts b/src/bot/state/GameStateValidator.ts index 7a1c6c90..4a2db479 100644 --- a/src/bot/state/GameStateValidator.ts +++ b/src/bot/state/GameStateValidator.ts @@ -55,13 +55,21 @@ export default class GameStateValidator { * Checks if an interaction through a messaging tunnel is valid or not. * * @param tunnel messaging tunnel object - * @param invited invited guild member, can be undefined * @returns true if the interaction is valid, false otherwise */ - public isInteractionValid(tunnel: MessagingTunnel, invited?: GuildMember): boolean { + public isInteractionValid(tunnel: MessagingTunnel): boolean { + return this.isMessagingAllowed(tunnel) && this.isMemberAllowed(tunnel.author); + } + + /** + * Checks if creating a new game is possible based on channel state and author. + * + * @param tunnel messaging tunnel object + * @param invited invited guild member, can be undefined + * @returns true if a game can be created, false otherwise + */ + public isNewGamePossible(tunnel: MessagingTunnel, invited?: GuildMember): boolean { return ( - this.isMessagingAllowed(tunnel) && - this.isMemberAllowed(tunnel.author) && // Check if one of both entites is already playing !this.manager.gameboards.some(gameboard => [tunnel.author, invited].some(