Skip to content

Commit

Permalink
Add option to append emoji of current player (#436)
Browse files Browse the repository at this point in the history
  • Loading branch information
utarwyn committed Aug 4, 2023
1 parent 243c6ac commit f8815a7
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 45 deletions.
1 change: 1 addition & 0 deletions config/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
"gameBoardDisableButtons": false,
"gameBoardEmbed": false,
"gameBoardEmojies": [],
"gameBoardPlayerEmoji": false,
"gameBoardReactions": false
}
19 changes: 18 additions & 1 deletion src/__tests__/GameBoard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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', () => {
Expand Down
34 changes: 22 additions & 12 deletions src/__tests__/GameBoardBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, '__');
Expand Down
6 changes: 5 additions & 1 deletion src/__tests__/GameBoardButtonBuilder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
59 changes: 40 additions & 19 deletions src/bot/builder/GameBoardBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -53,7 +58,7 @@ export default class GameBoardBuilder {
*/
constructor() {
this.title = '';
this.state = '';
this.stateKey = '';
this.boardSize = 0;
this.boardData = [];
}
Expand Down Expand Up @@ -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;
}

Expand All @@ -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;
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -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);
}
}
15 changes: 6 additions & 9 deletions src/bot/builder/GameBoardButtonBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand All @@ -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))
Expand Down
13 changes: 10 additions & 3 deletions src/bot/entity/GameBoard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/config/ConfigProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions src/config/GameConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down

0 comments on commit f8815a7

Please sign in to comment.