Add task scheduler

This commit is contained in:
Anton Vakhrushev 2020-03-29 10:49:15 +03:00
commit 5c69dd6a90
21 changed files with 6881 additions and 0 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
NODE_IMAGE=node:12.16-alpine

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.idea/
.nyc_output/
dist/
coverage/
node_modules/
test-results/
var/
.npmrc

22
Makefile Normal file
View File

@ -0,0 +1,22 @@
all: format build
restart-server:
docker-compose restart
build:
tools/npm run-script build:dev
build-min:
tools/npm run-script build
format:
tools/npm run-script format
test:
tools/npm run test
coverage:
tools/npm run coverage
publish:
tools/npm publish --access public

64
README.md Normal file
View File

@ -0,0 +1,64 @@
# Электронная гадалка
[![CircleCI](https://circleci.com/gh/anwinged/predictor/tree/master.svg?style=svg)](https://circleci.com/gh/anwinged/predictor/tree/master)
[Демоверсия][demo]
Алгоритм, который противостоит человеку, и на основе ходов пытается предсказать
следующих ход человека.
Игрок загадывает один из двух вариантов, а робот пытается его угадать.
Если программе удалось угадать, то игрок теряет очко.
Если программа не смогла предсказать выбор человека, то игрок зарабатывает очко.
Алгоритм реализован на основе [описания][algorithm]. В процессе реализации алгоритм слегка изменился.
В отличие от описания, здесь можно дополнительно указать количество вариантов.
С двумя вариантами будет игра "Чет - нечет", а с тремя - "Камень, ножницы, бумага".
Интересно то, что программу сложно обыграть. Игрок пытается обставить робота, но все равно
делает свои ходы не случайно. Именно эта "неслучайность" позволяет быстро приспосабливаться и эффективно
противостоять игроку.
Более подробно о гадалке и алгоритме можно прочитать на сайте [ltwood][about].
## Использование
```javascript
import Predictor from "predictor";
// Создание гадалки
const predictor = new Predictor(config);
// Передача значения, которое выбрал пользователь,
// и получение предсказание для этого значения
const prediction = predictor.pass(1);
// Получение текущего счета
const score = predictor.score;
// Получение количества сделанных ходов
const sc = predictor.stepCount();
```
### Конфигурация
- `config.base` - количество вариантов хода. Число большее 2.
Чем больше вариантов, тем больше ходов алгоритм тратит на приспособление к ходам человека.
- `config.daemons` - настройка списка демонов.
- `config.daemons.human` - количество анализируемых ходов человека,
- `config.daemons.robot` - количество анализируемых ходов робота,
- `config.daemons.epsilon` - вес вознаграждения правильного ответа.
- `config.supervisor_epsilon` - вес вознаграждения демона за правильный ответ.
## Сборка
tools/build-docker
tools/npm run build
## Тестирование
tools/npm run test
[algorithm]: https://sites.google.com/site/ltwood/projects/heshby/algorithm
[demo]: https://vakhrushev.me/articles/2019-05-01-predictor/
[about]: https://sites.google.com/site/ltwood/projects/heshby

11
docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
version: '3.6'
services:
server:
image: abiosoft/caddy:1.0.3-no-stats
volumes:
- ./server/Caddyfile:/etc/Caddyfile:ro
- ./dist:/srv:ro
ports:
- 49876:49876
restart: unless-stopped

6454
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

40
package.json Normal file
View File

@ -0,0 +1,40 @@
{
"name": "@anwinged/predictor",
"version": "0.1.0",
"description": "",
"author": "Anton Vakhrushev",
"license": "MIT",
"main": "dist/predictor.js",
"repository": {
"type": "git",
"url": "https://github.com/anwinged/predictor.git"
},
"scripts": {
"test": "mocha",
"test:junit-report": "mocha --reporter mocha-junit-reporter --reporter-options mochaFile=./test-results/results.xml",
"coverage": "nyc mocha",
"build:dev": "webpack",
"build": "webpack --env.production",
"format": "prettier --tab-width=4 --single-quote --trailing-comma es5 --write '{src,tests}/**/*.{ts,js}'",
"format-check": "prettier --tab-width=4 --single-quote --trailing-comma es5 --check '{src,tests}/**/*.{ts,js}'",
"format-wp": "prettier --tab-width=4 --single-quote --trailing-comma es5 --write 'webpack.config.js'",
"format-md": "prettier --write './*.md'"
},
"devDependencies": {
"@types/chai": "^4.2.11",
"@types/mocha": "^7.0.2",
"@types/node": "^13.9.4",
"@types/url-parse": "^1.4.3",
"chai": "^4.2.0",
"mocha": "^7.1.1",
"mocha-junit-reporter": "^1.23.3",
"nyc": "^15.0.0",
"prettier": "^1.19.1",
"ts-loader": "^6.2.2",
"ts-node": "^8.8.1",
"typescript": "^3.8.3",
"url-parse": "^1.4.7",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11"
}
}

4
server/Caddyfile Normal file
View File

@ -0,0 +1,4 @@
0.0.0.0:49876 {
root /srv
tls off
}

3
src/Action/Action.ts Normal file
View File

@ -0,0 +1,3 @@
export default abstract class Action {
abstract async run();
}

View File

@ -0,0 +1,7 @@
import Action from './Action';
export default class GoToMainAction extends Action {
async run(): Promise<any> {
return Promise.resolve(null);
}
}

30
src/ModeDetector.ts Normal file
View File

@ -0,0 +1,30 @@
import * as URLParse from 'url-parse';
const SESSION_KEY = 'travian_automation_mode';
const SESSION_VALUE = 'command_execution';
export default class ModeDetector {
isAuto(): boolean {
return this.isAutoByLocation() || this.isAutoBySession();
}
setAuto(): void {
sessionStorage.setItem(SESSION_KEY, SESSION_VALUE);
}
private isAutoByLocation(): boolean {
const p = new URLParse(window.location.href, true);
console.log('PARSED LOCATION', p);
if (p.query['auto-management'] !== undefined) {
console.log('AUTO MANAGEMENT ON');
return true;
}
return false;
}
private isAutoBySession(): boolean {
const k = sessionStorage.getItem(SESSION_KEY);
return k === SESSION_VALUE;
}
}

47
src/Queue.ts Normal file
View File

@ -0,0 +1,47 @@
export class QueueItem {
readonly name: string;
readonly args;
constructor(name: string, args: { [name: string]: any }) {
this.name = name;
this.args = args;
}
}
export class Queue {
private readonly name;
constructor(name: string) {
this.name = name;
}
pop() {
const serialized = localStorage.getItem(this.name);
if (serialized === null) {
return null;
}
const items = JSON.parse(serialized) as Array<QueueItem>;
if (items.length === 0) {
return null;
}
const first = items.shift();
localStorage.setItem(this.name, JSON.stringify(items));
if (first === undefined) {
return null;
}
return new QueueItem(first.name || '', first.args || {});
}
push(item: QueueItem): void {
const serialized = localStorage.getItem(this.name);
const items = serialized
? (JSON.parse(serialized) as Array<QueueItem>)
: [];
const first = items.push(item);
localStorage.setItem(this.name, JSON.stringify(items));
}
}

54
src/Scheduler.ts Normal file
View File

@ -0,0 +1,54 @@
import { sleep } from './utils';
import { Queue } from './Queue';
import GoToMainAction from './Action/GoToMainAction';
const ACTION_QUEUE = 'action_queue';
const TASK_QUEUE = 'task_queue';
export default class Scheduler {
taskQueue: Queue;
actionQueue: Queue;
constructor() {
this.taskQueue = new Queue(TASK_QUEUE);
this.actionQueue = new Queue(ACTION_QUEUE);
}
async run() {
while (true) {
const action = this.popAction();
console.log('POP ACTION', action);
if (action !== null) {
await action.run();
} else {
const task = this.popTask();
console.log('POP TASK', task);
if (task !== null) {
// do task
}
}
const waitTime = Math.random() * 5000;
console.log('WAIT', waitTime);
await sleep(waitTime);
}
}
private popTask() {
const item = this.taskQueue.pop();
if (item === null) {
return null;
}
return null;
}
private popAction() {
const item = this.actionQueue.pop();
if (item === null) {
return null;
}
if (item.name === 'go_to_main') {
return new GoToMainAction();
}
return null;
}
}

12
src/index.ts Normal file
View File

@ -0,0 +1,12 @@
import ModeDetector from './ModeDetector';
import Scheduler from './Scheduler';
const md = new ModeDetector();
if (md.isAuto()) {
md.setAuto();
console.log('AUTO MANAGEMENT ON');
const scheduler = new Scheduler();
scheduler.run();
} else {
console.log('NORMAL MODE');
}

5
src/utils.ts Normal file
View File

@ -0,0 +1,5 @@
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export { sleep };

21
tools/node Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash
set -eu
source .env
TTY=
if [ -t 1 ] ; then
TTY=--tty
fi
docker run \
--rm \
--interactive \
${TTY} \
--init \
--user "$(id -u):$(id -g)" \
--volume "$PWD:/app" \
--workdir /app \
${NODE_IMAGE} \
node "$@"

27
tools/npm Executable file
View File

@ -0,0 +1,27 @@
#!/bin/bash
set -eu
source .env
HOST_CACHE_DIR=$PWD/var/docker-cache/.npm
CONTAINER_CACHE_DIR=/tmp/.npm
mkdir -p ${HOST_CACHE_DIR}
TTY=
if [ -t 1 ] ; then
TTY=--tty
fi
docker run \
--rm \
--interactive \
${TTY} \
--init \
--user "$UID:$(id -g)" \
--volume "$PWD:/app" \
--env npm_config_cache="${CONTAINER_CACHE_DIR}" \
--workdir /app \
${NODE_IMAGE} \
npm "$@"

16
tools/tsc Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
set -eu
source .env
docker run \
--rm \
--interactive \
--tty \
--init \
--user "$(id -u):$(id -g)" \
--volume "$PWD:/app" \
--workdir /app \
${NODE_IMAGE} \
./node_modules/.bin/tsc "$@"

15
travian.user.js Normal file
View File

@ -0,0 +1,15 @@
// ==UserScript==
// @name New Userscript
// @namespace http://tampermonkey.net/
// @version 0.1
// @description try to take over the world!
// @author You
// @match http://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
// Your code here...
})();

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"outDir": "./dist",
"lib": ["es6", "dom"],
"allowJs": true,
"target": "es5",
"module": "commonjs",
"strictNullChecks": true,
"types": ["node", "url-parse", "mocha", "chai"]
},
"include": [
"./src/**/*"
]
}

26
webpack.config.js Normal file
View File

@ -0,0 +1,26 @@
const path = require('path');
module.exports = (env = {}) => ({
mode: env.production ? 'production' : 'development',
entry: path.resolve(__dirname, 'src/index.ts'),
output: {
filename: env.production ? 'travian.min.js' : 'travian.js',
path: path.resolve(__dirname, 'dist'),
library: 'travian',
libraryTarget: 'umd',
umdNamedDefine: true,
},
resolve: {
extensions: ['.ts', '.js'],
},
module: {
rules: [
{
test: /\.ts$/,
use: {
loader: 'ts-loader',
},
},
],
},
});