Commit 8978f8bf authored by evgen moruzhenko's avatar evgen moruzhenko

ref

parent ad9780fc
export BOT_TOKEN=ODY5NTY4MDE5NjIwMjk4Nzky.YQAGUw.kW8iDvISPRXZmD60GjoVpRRALZg
export BOT_COMMAND_PREFIX=.
......@@ -6,10 +6,10 @@
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"start": "source ./.env && nest start",
"start:dev": "source ./.env && nest start --watch",
"start:debug": "source ./.env && nest start --debug --watch",
"start:prod": "source ./.env && node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix"
},
"keywords": [],
......
export const BOT_TOKEN = 'ODY5NTY4MDE5NjIwMjk4Nzky.YQAGUw.kW8iDvISPRXZmD60GjoVpRRALZg'
export const BOT_COMMAND_PREFIX = '/';
export const BOT_TOKEN = process.env.BOT_TOKEN || ''; //'ODY5NTY4MDE5NjIwMjk4Nzky.YQAGUw.kW8iDvISPRXZmD60GjoVpRRALZg'
export const BOT_COMMAND_PREFIX = process.env.BOT_COMMAND_PREFIX || '/';
import { Message } from 'discord.js';
enum places {
_Place = '\u2423',
xPlace = '\u2715',
oPlace = '0',
}
export class GameMessage {
static winningMessage = (player: string): string => `Player ${player} has won!`;
static drawMessage = (): string => `Game ended in a draw!`;
static currentPlayerTurn = (player: string): string => `It's ${player}'s turn`;
}
class Game {
private screen: Message;
private winningConditions = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
private initialGameState: string[] = [
places._Place, places._Place, places._Place,
places._Place, places._Place, places._Place,
places._Place, places._Place, places._Place
];
public gameActive = true;
public currentPlayer = places.xPlace;
public gameState: string[];
private handleResultValidation() {
let roundWon = false;
for (let i = 0; i < this.winningConditions.length; i++) {
const winCondition = this.winningConditions[i];
if (
this.gameState[winCondition[0]] === places._Place ||
this.gameState[winCondition[1]] === places._Place ||
this.gameState[winCondition[2]] === places._Place
) {
continue;
}
if (
this.gameState[winCondition[0]] === this.gameState[winCondition[1]] &&
this.gameState[winCondition[1]] === this.gameState[winCondition[2]]
) {
roundWon = true;
break
}
}
if (roundWon) {
this.screen.channel.send(GameMessage.winningMessage(this.currentPlayer));
this.gameActive = false;
return;
}
let roundDraw = !this.gameState.includes(places._Place);
if (roundDraw) {
this.screen.channel.send(GameMessage.drawMessage());
this.gameActive = false;
return;
}
this.handlePlayerChange();
}
private handlePlayerChange() {
this.currentPlayer = this.currentPlayer === places.xPlace ? places.oPlace : places.xPlace;
this.screen.channel.send(GameMessage.currentPlayerTurn(this.currentPlayer));
if (this.currentPlayer === places.oPlace) this.doAction();
}
private handleCellPlayed(cellIndex: number) {
this.gameState[cellIndex] = this.currentPlayer;
}
private doAction() {
let place: number;
do {
place = Math.floor(Math.random() * (9 - 1) + 1);
} while (this.gameState[place - 1] !== places._Place);
this.handleCellSelected(place);
}
public handleRestartGame() {
this.gameActive = true;
this.currentPlayer = places.xPlace;
this.gameState = [...this.initialGameState];
this.screen.channel.send(GameMessage.currentPlayerTurn(this.currentPlayer));
}
public handleCellSelected(cellNumber: number) {
const cellIndex = cellNumber - 1;
if (this.gameState[cellIndex] !== places._Place || !this.gameActive) {
this.screen.reply('No-no-no! This place is placed!');
return;
}
this.handleCellPlayed(cellIndex);
this.handleResultValidation();
}
public prepareStateArrayToOutput(): string {
const field = [];
for (let i = 0; i < this.gameState.length; i += 1) {
if (i == 2 || i == 5 || i == 8) {
field.push(this.gameState.slice(i - 2, i + 1).join('|'));
}
}
return field.join('\n');
}
public setScreen(screen: Message) {
this.screen = screen;
}
}
export default new Game();
......@@ -2,7 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { Once, ClientProvider, Client, OnCommand } from 'discord-nestjs';
import { Message, MessageEmbed } from 'discord.js';
import game, { GameMessage } from './bot.game';
import game from './tic-tac-toe/game';
@Injectable()
export class BotGateway {
......@@ -18,24 +18,29 @@ export class BotGateway {
@OnCommand({ name: 'start' })
async onStart(message: Message): Promise<void> {
game.setScreen(message);
game.handleRestartGame();
const language: string = message.toString().split(' ')[1];
const firstPlayer: string = message.toString().split(' ')[2];
const result = game.restart(language, firstPlayer);
if (result.error) return;
const reply = new MessageEmbed().addFields({
name: GameMessage.currentPlayerTurn(game.currentPlayer),
value: game.prepareStateArrayToOutput() },
);
name: result.message,
value: result.field,
});
await message.channel.send(reply);
}
@OnCommand({ name: 'set' })
async onSetCommand(message: Message): Promise<void> {
const position: number = Number(message.toString().split(' ')[1]);
game.setScreen(message);
game.handleCellSelected(position);
const result = game.run(position);
if (result.error) return;
const reply = new MessageEmbed().addFields({
name: GameMessage.currentPlayerTurn(game.currentPlayer),
value: game.prepareStateArrayToOutput()
name: result.message,
value: result.field,
});
await message.channel.send(reply);
......
import { Module } from '@nestjs/common';
import { DiscordModule } from 'discord-nestjs';
import { BotGateway } from './bot.gateway';
import { BOT_TOKEN, BOT_COMMAND_PREFIX} from '../app.config';
import { DiscordConfigService } from '../configs/discord.config';
@Module({
imports: [
DiscordModule.forRoot({
token: BOT_TOKEN,
commandPrefix: BOT_COMMAND_PREFIX,
DiscordModule.forRootAsync({
useClass: DiscordConfigService,
}),
],
providers: [BotGateway],
......
import { IResult } from './Game.interfaces';
import { Shapes, Player, GameStatus } from './Game.enums';
import { Localization, translate, MessageList } from '../localization';
export class Game {
private winningConditions = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6],
];
public active: boolean;
public shape: string;
public state: string[];
private language: string;
private player: string;
private turnRange: number[];
private initialGameState: string[] = Array(9).fill(Shapes.none);
private normalizePosition(position: number): number {
return position - 1;
}
private validateResult(): GameStatus {
for (let i = 0; i < this.winningConditions.length; i++) {
const winCondition = this.winningConditions[i];
if (!(
this.state[winCondition[0]] === Shapes.none ||
this.state[winCondition[1]] === Shapes.none ||
this.state[winCondition[2]] === Shapes.none
)) {
if (
this.state[winCondition[0]] === this.state[winCondition[1]] &&
this.state[winCondition[1]] === this.state[winCondition[2]]
) {
return GameStatus.win;
}
}
}
if (!this.state.includes(Shapes.none)) {
return GameStatus.draw;
}
return GameStatus.next;
}
private togglePlayer() {
this.player = this.player === Player.user ? Player.bot : Player.user;
}
private doTurn(cell: number) {
this.state[cell] = this.player === Player.user ? Shapes.x : Shapes.o;
this.turnRange = this.turnRange.filter((item) => item !== cell);
}
private aiTurn(): number {
let place: number = this.turnRange[Math.random() * this.turnRange.length | 0]+1;
return place;
}
public getPlayer() {
return this.player;
}
public restart(language: string = 'en', firstPlayer: string = 'user'): IResult {
this.language = language;
this.turnRange = Array.from({ length: 9 }, (_, index) => index);
this.active = true;
this.player = firstPlayer;
this.state = [...this.initialGameState];
if (this.player === Player.bot) {
return this.run(this.aiTurn());
}
return {
message: translate(MessageList.playerTurn, this.player, Localization[this.language]),
field: this.renderGameState(),
};
}
private isCellValid(cell: number) {
return (cell >= 0 && cell < 9);
}
private isCellEmpty(cell: number) {
return this.turnRange.includes(cell);
}
private renderGameState(): string {
const field = [];
for (let i = 0; i < this.state.length; i += 1) {
if (i == 2 || i == 5 || i == 8) {
field.push(this.state.slice(i - 2, i + 1).join('|'));
}
}
return field.join('\n');
}
public run(position: number): IResult {
/* normalize position */
position = this.normalizePosition(position);
/* check game is active */
if (!this.active) {
return {
message: translate(MessageList.gameOver, this.player, Localization[this.language]),
field: this.renderGameState(),
};
}
/* check position is correct range */
if (!this.isCellValid(position)) {
return {
message: translate(MessageList.wrongCell, this.player, Localization[this.language]),
field: this.renderGameState(),
}
}
/* check position is in available rage of turns */
if (!this.isCellEmpty(position)) {
if (this.player === Player.bot) {
return this.run(this.aiTurn());
}
return {
message: translate(MessageList.notEmptyCell, this.player, Localization[this.language]),
field: this.renderGameState(),
};
}
/* do turn of user or bot */
this.doTurn(position);
/* check status of game/chack result of turn */
const status = this.validateResult();
if (status !== GameStatus.next) {
this.active = false;
return {
message: translate(MessageList[status], this.player, Localization[this.language]),
field: this.renderGameState(),
};
}
/* if game coninous then toggle player */
this.togglePlayer();
/*
if current player is user:
send to him a field to view
*/
if (this.player === Player.user) {
return {
message: translate(MessageList.playerTurn, this.player, Localization[this.language]),
field: this.renderGameState(),
};
}
/*
if current player is bot:
get cell position from bot ai
and do turn
*/
return this.run(this.aiTurn());
}
}
export enum Shapes {
none = '\u2423',
x = '\u2715',
o = '0',
}
export enum Player {
user = 'user',
bot = 'bot',
}
export enum GameStatus {
win = 'win',
draw = 'draw',
next = 'next',
}
export interface IResult {
message: string,
field?: string,
error?: string,
}
import { Game } from './Game.class';
export default new Game();
export enum MessageList {
win = 'win',
draw = 'draw',
playerTurn = 'playerTurn',
gameOver = 'gameOver',
notEmptyCell = 'notEmptyCell',
wrongCell = 'wrongCell',
}
export interface IMessageList {
win: string,
draw: string,
playerTurn: string,
gameOver: string,
notEmptyCell: string,
wrongCell: string,
}
export interface IMessage {
[key: string]: IMessageList,
}
import { IMessage, IMessageList } from './Localization.interfaces';
import { MessageList } from './Localization.enums';
export { MessageList } from './Localization.enums';
const normalize = (message: string) => {
return message.charAt(0).toUpperCase() + message.slice(1);
}
export const translate = (message: MessageList, player: string, language: IMessageList = Localization.en): string => {
return `**${normalize(language[message].replace('player', player))}**`;
};
export const Localization: IMessage = {
en: {
win: 'player has won!',
draw: 'This game is draw!',
playerTurn: 'player\'s turn...',
gameOver: 'Game Over!',
notEmptyCell: 'This cell isn\'t empty!',
wrongCell: 'Wrong cell!',
},
ru: {
win: 'player вин!',
draw: 'Ну такое...',
playerTurn: 'Ход player\'а',
gameOver :'Гейм овер!',
notEmptyCell: 'Сюда низзя!',
wrongCell: 'Чего?!. Куда?!.',
},
}
import { Injectable } from '@nestjs/common';
import { DiscordModuleOption, DiscordOptionsFactory, } from 'discord-nestjs';
if (!process.env.BOT_TOKEN) {
console.log('\x1b[31;1mNo Token in ENV found!\x1b[0m');
process.exit(255);
}
@Injectable()
export class DiscordConfigService implements DiscordOptionsFactory {
createDiscordOptions(): DiscordModuleOption {
return {
token: process.env.BOT_TOKEN || '',
commandPrefix: process.env.BOT_COMMAND_PREFIX || '!',
};
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment