From f8815a73d84a3ab18b68a3d5c7bd5ee47c2f114d Mon Sep 17 00:00:00 2001 From: utarwyn Date: Fri, 4 Aug 2023 22:14:26 +0200 Subject: [PATCH] Add option to append emoji of current player (#436) --- config/config.example.json | 1 + src/__tests__/GameBoard.test.ts | 19 ++++++- src/__tests__/GameBoardBuilder.test.ts | 34 +++++++---- src/__tests__/GameBoardButtonBuilder.test.ts | 6 +- src/bot/builder/GameBoardBuilder.ts | 59 +++++++++++++------- src/bot/builder/GameBoardButtonBuilder.ts | 15 ++--- src/bot/entity/GameBoard.ts | 13 ++++- src/config/ConfigProvider.ts | 1 + src/config/GameConfig.ts | 4 ++ 9 files changed, 107 insertions(+), 45 deletions(-) diff --git a/config/config.example.json b/config/config.example.json index d20cec3b..cff375ec 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -17,5 +17,6 @@ "gameBoardDisableButtons": false, "gameBoardEmbed": false, "gameBoardEmojies": [], + "gameBoardPlayerEmoji": false, "gameBoardReactions": false } diff --git a/src/__tests__/GameBoard.test.ts b/src/__tests__/GameBoard.test.ts index 09e2bd06..88976cc1 100644 --- a/src/__tests__/GameBoard.test.ts +++ b/src/__tests__/GameBoard.test.ts @@ -59,6 +59,7 @@ describe('GameBoard', () => { withEmbed: jest.fn().mockReturnThis(), withEmojies: jest.fn().mockReturnThis(), withEndingMessage: jest.fn().mockReturnThis(), + withLoadingMessage: jest.fn().mockReturnThis(), withEntityPlaying: jest.fn().mockReturnThis(), withExpireMessage: jest.fn().mockReturnThis(), withTitle: jest.fn().mockReturnThis() @@ -88,11 +89,27 @@ describe('GameBoard', () => { } ); + it('should set loading message if reactions are not loaded', () => { + gameBoard.content; + expect(mockedBuilder.withLoadingMessage).toHaveBeenCalledTimes(1); + }); + it('should set entity playing if reactions are loaded', () => { gameBoard['reactionsLoaded'] = true; gameBoard.content; expect(mockedBuilder.withEntityPlaying).toHaveBeenCalledTimes(1); - expect(mockedBuilder.withEntityPlaying).toHaveBeenCalledWith(tunnel.author); + expect(mockedBuilder.withEntityPlaying).toHaveBeenCalledWith(tunnel.author, undefined); + }); + + it('should set entity playing with an emoji', () => { + configuration.gameBoardPlayerEmoji = true; + gameBoard['reactionsLoaded'] = true; + gameBoard.content; + expect(mockedBuilder.withEntityPlaying).toHaveBeenCalledTimes(1); + expect(mockedBuilder.withEntityPlaying).toHaveBeenCalledWith( + tunnel.author, + Player.First + ); }); it('should add an ending message if the game is finished', () => { diff --git a/src/__tests__/GameBoardBuilder.test.ts b/src/__tests__/GameBoardBuilder.test.ts index 1d343f29..b2136d01 100644 --- a/src/__tests__/GameBoardBuilder.test.ts +++ b/src/__tests__/GameBoardBuilder.test.ts @@ -59,25 +59,35 @@ describe('GameBoardBuilder', () => { }); it('should add an empty line between board and state if both defined', () => { - builder.withBoard(1, [Player.None]).withEntityPlaying(); + builder.withBoard(1, [Player.None]).withEntityPlaying(new AI()); expect(builder.toMessageOptions().content).toContain('\n'); }); - it.each` - entity | state | stateParams - ${undefined} | ${'game.load'} | ${[]} - ${new AI()} | ${'game.waiting-ai'} | ${[]} - ${{ toString: () => 'fake' }} | ${'game.action'} | ${[{ player: 'fake' }]} - `('should set state based on playing entity $entity', ({ entity, state, stateParams }) => { - const spyLocalize = jest.spyOn(localize, '__'); - builder.withEntityPlaying(entity); - expect(builder.toMessageOptions()).toEqual(expect.objectContaining({ content: state })); - expect(spyLocalize).toHaveBeenCalledWith(state, ...stateParams); + it('should set state with game loading message', () => { + builder.withLoadingMessage(); + expect(builder.toMessageOptions()).toEqual( + expect.objectContaining({ content: 'game.load' }) + ); }); + it.each` + entity | emojiIndex | state | stateParams + ${new AI()} | ${undefined} | ${'game.waiting-ai'} | ${[undefined]} + ${{ toString: () => 'fake' }} | ${undefined} | ${'game.action'} | ${[{ player: 'fake' }]} + ${{ toString: () => 'fake' }} | ${1} | ${'game.action'} | ${[{ player: 'fake 🇽' }]} + `( + 'should set state based on playing entity $entity and emoji index $emojiIndex', + ({ entity, emojiIndex, state, stateParams }) => { + const spyLocalize = jest.spyOn(localize, '__'); + builder.withEntityPlaying(entity, emojiIndex); + expect(builder.toMessageOptions()).toEqual(expect.objectContaining({ content: state })); + expect(spyLocalize).toHaveBeenCalledWith(state, ...stateParams); + } + ); + it.each` entity | state | stateParams - ${undefined} | ${'game.end'} | ${[]} + ${undefined} | ${'game.end'} | ${[undefined]} ${{ toString: () => 'fake' }} | ${'game.win'} | ${[{ player: 'fake' }]} `('should set state based on winning entity $entity', ({ entity, state, stateParams }) => { const spyLocalize = jest.spyOn(localize, '__'); diff --git a/src/__tests__/GameBoardButtonBuilder.test.ts b/src/__tests__/GameBoardButtonBuilder.test.ts index c0f2bd1c..86daac2f 100644 --- a/src/__tests__/GameBoardButtonBuilder.test.ts +++ b/src/__tests__/GameBoardButtonBuilder.test.ts @@ -59,9 +59,13 @@ describe('GameBoardButtonBuilder', () => { expect((options.components![1].components[1] as MessageButton).emoji?.name).toBe('square'); }); + it('should do nothing when a loading message is added', () => { + const options = builder.withLoadingMessage().toMessageOptions(); + expect(options.content).toBe(''); + }); + it.each` entity | state - ${undefined} | ${''} ${new AI()} | ${':robot: AI is playing, please wait...'} ${{ toString: () => 'fake' }} | ${'fake, select your move:'} `('should set state based if playing entity is $entity', ({ entity, state }) => { diff --git a/src/bot/builder/GameBoardBuilder.ts b/src/bot/builder/GameBoardBuilder.ts index 6e70f32f..c47c3cf4 100644 --- a/src/bot/builder/GameBoardBuilder.ts +++ b/src/bot/builder/GameBoardBuilder.ts @@ -28,10 +28,15 @@ export default class GameBoardBuilder { */ protected title: string; /** - * Stores game current state. + * Stores localization key of current game state. * @protected */ - protected state: string; + protected stateKey: string; + /** + * Stores entity whiches is concerned in the state message. + * @protected + */ + protected stateEntity?: { name: string; emojiIndex?: number }; /** * Stores game board size. * @protected @@ -53,7 +58,7 @@ export default class GameBoardBuilder { */ constructor() { this.title = ''; - this.state = ''; + this.stateKey = ''; this.boardSize = 0; this.boardData = []; } @@ -100,20 +105,26 @@ export default class GameBoardBuilder { return this; } + /** + * Writes that the game is loading. + * + * @returns same instance + */ + public withLoadingMessage(): this { + this.stateKey = 'game.load'; + return this; + } + /** * Writes that an entity is playing. * - * @param entity entity whiches is playing. If undefined: display loading message + * @param entity entity whiches is playing. + * @param emojiIndex index of the emoji to display next to entity name * @returns same instance */ - public withEntityPlaying(entity?: Entity): GameBoardBuilder { - if (entity instanceof AI) { - this.state = localize.__('game.waiting-ai'); - } else if (!entity) { - this.state = localize.__('game.load'); - } else { - this.state = localize.__('game.action', { player: entity.toString() }); - } + public withEntityPlaying(entity: Entity, emojiIndex?: number): GameBoardBuilder { + this.stateEntity = { name: entity.toString(), emojiIndex: emojiIndex }; + this.stateKey = entity instanceof AI ? 'game.waiting-ai' : 'game.action'; return this; } @@ -125,9 +136,10 @@ export default class GameBoardBuilder { */ public withEndingMessage(winner?: Entity): GameBoardBuilder { if (winner) { - this.state = localize.__('game.win', { player: winner.toString() }); + this.stateKey = 'game.win'; + this.stateEntity = { name: winner.toString() }; } else { - this.state = localize.__('game.end'); + this.stateKey = 'game.end'; } return this; } @@ -138,7 +150,7 @@ export default class GameBoardBuilder { * @returns same instance */ public withExpireMessage(): GameBoardBuilder { - this.state = localize.__('game.expire'); + this.stateKey = 'game.expire'; return this; } @@ -169,15 +181,24 @@ export default class GameBoardBuilder { } } - // Generate final string - const state = this.state && board ? '\n' + this.state : this.state; + const state = this.generateState(); + const stateWithBoard = `${board}${board && state ? '\n' : ''}${state}`; + return { allowedMentions: { parse: ['users'] }, embeds: this.embedColor - ? [{ title: this.title, description: board + state, color: this.embedColor }] + ? [{ title: this.title, description: stateWithBoard, color: this.embedColor }] : [], - content: !this.embedColor ? this.title + board + state : undefined, + content: !this.embedColor ? this.title + stateWithBoard : undefined, components: [] }; } + + protected generateState(): string { + let player = this.stateEntity?.name; + if (this.stateEntity?.emojiIndex !== undefined) { + player += ` ${this.emojies[this.stateEntity.emojiIndex]}`; + } + return localize.__(this.stateKey, player ? { player } : undefined); + } } diff --git a/src/bot/builder/GameBoardButtonBuilder.ts b/src/bot/builder/GameBoardButtonBuilder.ts index 88f08e59..f4d63013 100644 --- a/src/bot/builder/GameBoardButtonBuilder.ts +++ b/src/bot/builder/GameBoardButtonBuilder.ts @@ -61,13 +61,9 @@ export default class GameBoardButtonBuilder extends GameBoardBuilder { * @inheritdoc * @override */ - override withEntityPlaying(entity?: Entity): GameBoardBuilder { - // Do not display state if game is loading - if (entity) { - return super.withEntityPlaying(entity); - } else { - return this; - } + override withLoadingMessage(): this { + // there is no need to display loading message + return this; } /** @@ -94,11 +90,12 @@ export default class GameBoardButtonBuilder extends GameBoardBuilder { * @override */ override toMessageOptions(): MessageOptions { + const state = this.generateState(); return { embeds: this.embedColor - ? [{ title: this.title, description: this.state, color: this.embedColor }] + ? [{ title: this.title, description: state, color: this.embedColor }] : [], - content: !this.embedColor ? this.title + this.state : undefined, + content: !this.embedColor ? this.title + state : undefined, components: [...Array(this.boardSize).keys()].map(row => new MessageActionRow().addComponents( [...Array(this.boardSize).keys()].map(col => this.createButton(row, col)) diff --git a/src/bot/entity/GameBoard.ts b/src/bot/entity/GameBoard.ts index e737298d..546c1c5f 100644 --- a/src/bot/entity/GameBoard.ts +++ b/src/bot/entity/GameBoard.ts @@ -96,10 +96,17 @@ export default class GameBoard { builder .withTitle(this.entities[0], this.entities[1]) - .withBoard(this.game.boardSize, this.game.board) - .withEntityPlaying( - this.reactionsLoaded ? this.getEntity(this.game.currentPlayer) : undefined + .withBoard(this.game.boardSize, this.game.board); + + const currentEntity = this.getEntity(this.game.currentPlayer); + if (this.reactionsLoaded && currentEntity != null) { + builder.withEntityPlaying( + currentEntity, + this.configuration.gameBoardPlayerEmoji ? this.game.currentPlayer : undefined ); + } else { + builder.withLoadingMessage(); + } if (this.expired) { builder.withExpireMessage(); diff --git a/src/config/ConfigProvider.ts b/src/config/ConfigProvider.ts index 9cbfb1af..00481893 100644 --- a/src/config/ConfigProvider.ts +++ b/src/config/ConfigProvider.ts @@ -31,6 +31,7 @@ export default class ConfigProvider implements Config { public gameBoardDisableButtons = false; public gameBoardEmbed = false; public gameBoardEmojies = []; + public gameBoardPlayerEmoji = false; public gameBoardReactions = false; [key: string]: any; diff --git a/src/config/GameConfig.ts b/src/config/GameConfig.ts index f406a8c6..4ed620b8 100644 --- a/src/config/GameConfig.ts +++ b/src/config/GameConfig.ts @@ -35,6 +35,10 @@ export default interface GameConfig { * List of emojies used to identify players. */ gameBoardEmojies?: string[]; + /** + * Should display current player's emoji next to its name. + */ + gameBoardPlayerEmoji?: boolean; /** * Interact with game board using reactions instead of buttons. */