Skip to content

Commit

Permalink
init discord bot
Browse files Browse the repository at this point in the history
  • Loading branch information
ReDBrother committed Feb 23, 2022
0 parents commit ec72474
Show file tree
Hide file tree
Showing 21 changed files with 7,027 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[Makefile]
indent_style = tab

[*.js]
indent_style = space
indent_size = 2
25 changes: 25 additions & 0 deletions .eslintrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---

plugins:
# https://github.com/jest-community/eslint-plugin-jest
- jest

# https://eslint.org/docs/user-guide/configuring#specifying-environments
env:
node: true
es2020: true

extends:
- 'airbnb-base'
- 'plugin:jest/recommended'

parserOptions:
ecmaVersion: 11
sourceType: module

rules:
no-console: 0
import/extensions: 0 # FIXME: remove when rule will be adjusted for new nodejs version
no-param-reassign: 0
class-methods-use-this: 0
no-underscore-dangle: [2, { "allow": ["__filename", "__dirname"] }]
52 changes: 52 additions & 0 deletions .github/workflows/nodejs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Name of workflow
name: Node CI

# Trigger the workflow on push or pull request
on:
- push
- pull_request

jobs:
build:

# The type of machine to run the job on
runs-on: ubuntu-latest

strategy:
# Node versions list
matrix:
node-version: [14.x]

steps:
# Check-out repository under GitHub workspace
# https://github.com/actions/checkout
- uses: actions/checkout@v2
# Step's name
- name: Use Node.js ${{ matrix.node-version }}
# Configures the node version used on GitHub-hosted runners
# https://github.com/actions/setup-node
uses: actions/setup-node@v1
# The Node.js version to configure
with:
node-version: ${{ matrix.node-version }}
- name: npm install
# Install project
run: |
make install
# Add environment variables
env:
CI: true
- name: Run linter
# Run Linter
run: |
make lint
- name: Test & publish code coverage
# Publish code coverage on Code Climate
# https://github.com/paambaati/codeclimate-action
uses: paambaati/[email protected]
# Add Code Climate secret key
env:
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
with:
coverageCommand: make test-coverage
debug: true
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/node_modules/
*.log
.env
coverage
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node-options=--experimental-vm-modules
24 changes: 24 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
install: install-deps install-env

run:
bin/client.js

install-env:
cp env.template .env

install-deps:
npm ci

test:
npm test

test-coverage:
npm test -- --coverage --coverageProvider=v8

lint:
npx eslint --fix .

publish:
npm publish

.PHONY: test
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
##
text.
##

# Codebattle bot

[![Node CI](https://github.com/ReDBrother/codebattle_bot/workflows/Node%20CI/badge.svg)](https://github.com/hexlet-bbtob_elttabedoc/rehtorBDeR
[![Maintainability](https://api.codeclimate.com/v1/badges/dfc50c2d88cd46d069c1/maintainability)](https://codeclimate.com/github/ReDBrother/codebattle_bot/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/dfc50c2d88cd46d069c1/test_coverage)](https://codeclimate.com/github/ReDBrother/codebattle_bot/test_coverage)

## Setup

```sh
$ make install
```

## Run tests

```sh
$ make test
```
3 changes: 3 additions & 0 deletions bin/client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/node

require('../index.js');
44 changes: 44 additions & 0 deletions commands/debug.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const { SlashCommand } = require('slash-create');
const { selectors } = require('../lib/state.js');

module.exports = class DebugCommand extends SlashCommand {
constructor(creator) {
super(creator, {
name: 'debug',
description: 'Debug bot current state',
});
}

getPlayerDebugInfo({
id,
name,
is_bot: isBot,
github_id: githubId,
lang, creator,
rating,
}) {
return [
`id: ${id}`,
`name: ${name}`,
`is_bot: ${isBot}`,
`github_id: ${githubId}`,
`lang: ${lang}`,
`creator: ${creator}`,
`rating: ${rating}`,
];
}

getGameDebugInfo({ players, status }) {
const player1 = this.getPlayerDebugInfo(players[0]).join('\n\t');
const player2 = this.getPlayerDebugInfo(players[1]).join('\n\t');

return [`player1: ${player1.join('\n\t')}`, `player2: ${player2}`, `status: ${status}`];
}

async run(ctx) {
const games = selectors.getAllGames();
const subscribedGamesFields = games.map((game) => (
{ name: `Game #${game.id}`, value: this.getGameDebugInfo(game) }
));
}
};
15 changes: 15 additions & 0 deletions commands/disconnect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { SlashCommand } = require('slash-create');
const { socket } = require('../lib/socket.js');

module.exports = class BotDisconnectCommand extends SlashCommand {
constructor(creator) {
super(creator, {
name: 'disconnect',
description: 'disconnect socket connection to the codebattle server',
});
}

async run() {
socket.disconnect(() => {}, 0, 'Closing connection');
}
};
40 changes: 40 additions & 0 deletions commands/games.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const { SlashCommand } = require('slash-create');
const { socket } = require('../lib/socket.js');

module.exports = class GamesCommand extends SlashCommand {
constructor(creator) {
super(creator, {
name: 'games',
description: 'show list active games',
});
}

async run(ctx) {
const lobby = socket.channel('lobby');

lobby.onClose(() => {
ctx.send('Close connection to lobby');
});

lobby.join()
.receive('ok', ({ active_games: activeGames }) => {
ctx.send('Establish connection to lobby');
const activeGameIds = activeGames
.reduce((acc, { id, state }) => {
if (state === 'waiting_opponent') return acc;

return [...acc, id];
}, [])
.join(' ');
ctx.send(`list of active games [${activeGameIds}]`);
console.log(activeGames);
lobby.leave();
})
.receive('timeout', () => {
ctx.send('Networking issue...');
})
.receive('error', (err) => {
ctx.send(`there was an error with the connection! ${JSON.stringify(err)}`);
});
}
};
15 changes: 15 additions & 0 deletions commands/reconnect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { SlashCommand } = require('slash-create');
const { socket } = require('../lib/socket.js');

module.exports = class BotReconnectCommand extends SlashCommand {
constructor(creator) {
super(creator, {
name: 'reconnect',
description: 'reconnect socket connection to the codebattle server',
});
}

async run() {
socket.connect();
}
};
119 changes: 119 additions & 0 deletions commands/subscribe.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
const { SlashCommand, CommandOptionType } = require('slash-create');
const _ = require('lodash');
const { stripIndent } = require('common-tags');

const { socket, socketMessages } = require('../lib/socket.js');
const { dispatch, actions, selectors } = require('../lib/state.js');
const { sendAll, send } = require('../lib/utils.js');

module.exports = class GameSubscribeCommand extends SlashCommand {
constructor(creator) {
super(creator, {
name: 'subscribe',
description: 'Subscribe discord channel or your\'s dm to active game events',
options: [{
type: CommandOptionType.INTEGER,
name: 'game_id',
description: 'Id of active game you want to subscribe',
}],
});
}

hasConnection(game, { channelId }) {
return game.subscribers.some(({ id }) => id === channelId);
}

async run(ctx) {
const { game_id: gameId } = ctx.options;
const foundedGame = selectors.getActiveGame(gameId);

if (foundedGame && this.hasConnection(foundedGame, ctx)) {
ctx.send('Already connect to the game');
return;
}

if (foundedGame) {
dispatch(actions.addSubscriber({
gameId,
subscriber: ctx.id,
}));
ctx.send(`Establish connection with game #${gameId}. status: ${foundedGame.status}`);
return;
}

const gameChannel = socket.channel(`game:${gameId}`);
const params = { id: gameId, subscribers: [ctx] };

const onJoinSuccess = (gameInfo) => {
dispatch(actions.addGame({ ...params, ...gameInfo }));

gameChannel.onClose(() => {
const subscribers = selectors.getSubscribers(gameId);
dispatch(actions.removeGame(gameId));
sendAll(subscribers, `Game #${gameId} is not available now`);
});

gameChannel.on(socketMessages.userStartCheck, (resp) => {
const subscribers = selectors.getSubscribers(gameId);
const player = _.find(gameInfo.players, ({ id }) => id === resp.user_id);

sendAll(subscribers, `User #${player.name} started check solution`);
});

gameChannel.on(socketMessages.userCheckComplete, (resp) => {
const { subscribers, status } = selectors.getActiveGame(gameId);
const player = _.find(gameInfo.players, ({ id }) => id === resp.user_id);
const opponent = _.find(gameInfo.players, ({ id }) => id !== resp.user_id);
const isWinningCheck = status !== resp.status;

dispatch(actions.updateGame(gameId, { status: resp.status }));

subscribers.forEach((subscriber) => {
send(subscriber, stripIndent`
User #${player.name} complete check solution.
Result: ${resp.check_result.asserts.status}
`);

if (isWinningCheck) {
send(subscriber, stripIndent`
Game is over:
User #${player.name} win.
User #${opponent.name} lost.
`);
}
});

if (isWinningCheck) gameChannel.leave();
});

gameChannel.on(socketMessages.userGiveUp, (resp) => {
const subscribers = selectors.getSubscribers(gameId);
const player = _.find(gameInfo.players, ({ id }) => id === resp.user_id);

dispatch(actions.updateGame(gameId, { status: resp.status }));

sendAll(subscribers, stripIndent`
User ${player.name} gives up.
gameId: ${gameId}
`);
});
};

gameChannel
.join()
.receive('ok', (resp) => {
if (resp.status === 'game_over') {
ctx.send('Game is already over');
return;
}
ctx.send(`Establish connection with game #${gameId}. status: ${resp.status}`);
onJoinSuccess(resp);
})
.receive('timeout', () => {
ctx.send('Networking issue...');
})
.receive('error', (err) => {
ctx.send(`there was an error with the connection! ${JSON.stringify(err)}`);
});
}
};
Loading

0 comments on commit ec72474

Please sign in to comment.