Add hero adventures task

This commit is contained in:
Anton Vakhrushev 2020-04-02 22:22:56 +03:00
parent 711c8a414b
commit 4b1e7cb676
15 changed files with 307 additions and 33 deletions

View File

@ -0,0 +1,14 @@
import ActionController from './ActionController';
import { Args } from '../Common';
import { Task } from '../Storage/TaskQueue';
export default class ClickButtonAction extends ActionController {
static NAME = 'click_button';
async run(args: Args, task: Task): Promise<any> {
const el = jQuery(args.selector);
if (el.length === 1) {
console.log('CLICK BUTTON', el);
el.trigger('click');
}
}
}

View File

@ -0,0 +1,17 @@
import ActionController from './ActionController';
import { Args } from '../Common';
import { Task } from '../Storage/TaskQueue';
import Scheduler from '../Scheduler';
export default class CompleteTaskAction extends ActionController {
static NAME = 'complete_task';
private scheduler: Scheduler;
constructor(scheduler: Scheduler) {
super();
this.scheduler = scheduler;
}
async run(args: Args, task: Task): Promise<any> {
this.scheduler.completeTask(task.id);
}
}

View File

@ -0,0 +1,35 @@
import ActionController from './ActionController';
import { Args } from '../Common';
import { Task } from '../Storage/TaskQueue';
import { ActionError } from '../Errors';
import { GameState } from '../Storage/GameState';
export default class GrabHeroAttributesAction extends ActionController {
static NAME = 'grab_hero_attributes';
private state: GameState;
constructor(state: GameState) {
super();
this.state = state;
}
async run(args: Args, task: Task): Promise<any> {
const healthElement = jQuery(
'#attributes .attribute.health .powervalue .value'
);
if (healthElement.length !== 1) {
throw new ActionError(task.id, 'Health dom element not found');
}
const text = healthElement.text();
let normalized = text.replace(/[^0-9]/g, '');
const value = Number(normalized);
if (isNaN(value)) {
throw new ActionError(
task.id,
`Health value "${text}" (${normalized}) couldn't be converted to number`
);
}
this.state.set('hero', { health: value });
}
}

View File

@ -0,0 +1,71 @@
import ActionController from './ActionController';
import { Args } from '../Common';
import { Task } from '../Storage/TaskQueue';
import { GameState } from '../Storage/GameState';
import { trimPrefix } from '../utils';
import { AbortTaskError } from '../Errors';
const HARD = 0;
const NORMAL = 3;
interface Adventure {
el: HTMLElement;
level: number;
}
export default class SendOnAdventureAction extends ActionController {
static NAME = 'send_on_adventure';
private state: GameState;
constructor(state: GameState) {
super();
this.state = state;
}
async run(args: Args, task: Task): Promise<any> {
const adventures = this.findAdventures();
console.log('ADVENTURES', adventures);
adventures.sort((x, y) => x.level - y.level);
const easiest = adventures.shift();
const hero = this.state.get('hero') || {};
console.log('EASIEST', easiest);
console.log('HERO', hero);
if (easiest && hero.health) {
if (easiest.level === NORMAL && hero.health >= 30) {
return jQuery(easiest.el)
.find('td.goTo .gotoAdventure')
.trigger('click');
}
if (easiest.level === HARD && hero.health >= 50) {
return jQuery(easiest.el)
.find('td.goTo .gotoAdventure')
.trigger('click');
}
}
throw new AbortTaskError(task.id, 'No suitable adventure');
}
private findAdventures(): Array<Adventure> {
const adventures: Array<Adventure> = [];
jQuery('tr[id^=adventure]').each((index, el) => {
const imgClass =
jQuery(el)
.find('.difficulty img')
.attr('class')
?.toString() || '';
const level = Number(trimPrefix(imgClass, 'adventureDifficulty'));
if (!isNaN(level)) {
adventures.push({ el, level: level });
}
});
return adventures;
}
}

View File

@ -6,23 +6,15 @@ import { Task } from '../Storage/TaskQueue';
export default class UpgradeBuildingAction extends ActionController {
static NAME = 'upgrade_building';
private scheduler: Scheduler;
constructor(scheduler: Scheduler) {
super();
this.scheduler = scheduler;
}
async run(args: Args, task: Task): Promise<any> {
const btn = jQuery(
'.upgradeButtonsContainer .section1 button.green.build'
);
if (btn.length === 1) {
this.scheduler.completeTask(task.id);
btn.trigger('click');
} else {
if (btn.length !== 1) {
throw new TryLaterError(5 * 60, 'No upgrade button, try later');
}
return null;
btn.trigger('click');
}
}

View File

@ -1,5 +1,5 @@
import * as URLParse from 'url-parse';
import { markPage, uniqId, waitForLoad } from './utils';
import { markPage, trimPrefix, uniqId, waitForLoad } from './utils';
import Scheduler from './Scheduler';
import UpgradeBuildingTask from './Task/UpgradeBuildingTask';
import { Command } from './Common';

View File

@ -1,5 +1,23 @@
import { TaskId } from './Storage/TaskQueue';
export class ActionError extends Error {
readonly id: TaskId;
constructor(id: TaskId, msg: string = '') {
super(msg);
this.id = id;
Object.setPrototypeOf(this, ActionError.prototype);
}
}
export class AbortTaskError extends Error {
readonly id: TaskId;
constructor(id: TaskId, msg: string = '') {
super(msg);
this.id = id;
Object.setPrototypeOf(this, AbortTaskError.prototype);
}
}
export class TryLaterError extends Error {
readonly seconds: number;
readonly id: TaskId;
@ -18,6 +36,6 @@ export class BuildingQueueFullError extends Error {
super(msg);
this.id = id;
this.seconds = seconds;
Object.setPrototypeOf(this, TryLaterError.prototype);
Object.setPrototypeOf(this, BuildingQueueFullError.prototype);
}
}

View File

@ -1 +0,0 @@
export default class GameState {}

View File

@ -1,7 +1,11 @@
import { markPage, sleepShort, timestamp } from './utils';
import UpgradeBuildingTask from './Task/UpgradeBuildingTask';
import UpgradeBuildingAction from './Action/UpgradeBuildingAction';
import { BuildingQueueFullError, TryLaterError } from './Errors';
import {
AbortTaskError,
BuildingQueueFullError,
TryLaterError,
} from './Errors';
import { TaskQueue, TaskList, Task, TaskId } from './Storage/TaskQueue';
import ActionQueue from './Storage/ActionQueue';
import { Args, Command } from './Common';
@ -10,16 +14,24 @@ import ActionController from './Action/ActionController';
import TaskController from './Task/TaskController';
import GoToPageAction from './Action/GoToPageAction';
import CheckBuildingRemainingTimeAction from './Action/CheckBuildingRemainingTimeAction';
import SendOnAdventureTask from './Task/SendOnAdventureTask';
import GrabHeroAttributesAction from './Action/GrabHeroAttributesAction';
import { GameState } from './Storage/GameState';
import CompleteTaskAction from './Action/CompleteTaskAction';
import SendOnAdventureAction from './Action/SendOnAdventureAction';
import ClickButtonAction from './Action/ClickButtonAction';
export default class Scheduler {
private readonly version: string;
private taskQueue: TaskQueue;
private actionQueue: ActionQueue;
private gameState: GameState;
constructor(version: string) {
this.version = version;
this.taskQueue = new TaskQueue();
this.actionQueue = new ActionQueue();
this.gameState = new GameState();
}
async run() {
@ -27,7 +39,10 @@ export default class Scheduler {
markPage('Executor', this.version);
this.renderTaskQueue();
setInterval(() => this.renderTaskQueue(), 5000);
setInterval(() => this.renderTaskQueue(), 5 * 1000);
this.scheduleHeroAdventure();
setInterval(() => this.scheduleHeroAdventure(), 3600 * 1000);
while (true) {
await this.doLoopStep();
@ -39,6 +54,15 @@ export default class Scheduler {
new TaskQueueRenderer().render(this.taskQueue.seeItems());
}
private scheduleHeroAdventure() {
if (!this.taskQueue.hasNamed(SendOnAdventureTask.NAME)) {
this.taskQueue.push(
new Command(SendOnAdventureTask.NAME, {}),
timestamp()
);
}
}
private async doLoopStep() {
await sleepShort();
const currentTs = timestamp();
@ -51,7 +75,7 @@ export default class Scheduler {
return;
}
const actionCommand = this.popActionCommand();
const actionCommand = this.actionQueue.pop();
this.log('CURRENT TASK', taskCommand);
this.log('CURRENT ACTION', actionCommand);
@ -67,7 +91,7 @@ export default class Scheduler {
private async processTaskCommand(task: Task) {
const taskController = this.createTaskControllerByName(task.cmd.name);
this.log('PROCESS TASK CONTROLLER', taskController, task);
this.log('PROCESS TASK', task.cmd.name, task, taskController);
if (taskController) {
taskController.run(task);
}
@ -75,7 +99,7 @@ export default class Scheduler {
private async processActionCommand(cmd: Command, task: Task) {
const actionController = this.createActionControllerByName(cmd.name);
this.log('PROCESS ACTION CONTROLLER', cmd.name, actionController);
this.log('PROCESS ACTION', cmd.name, actionController);
if (actionController) {
await this.runAction(actionController, cmd.args, task);
}
@ -104,31 +128,37 @@ export default class Scheduler {
switch (taskName) {
case UpgradeBuildingTask.NAME:
return new UpgradeBuildingTask(this);
case SendOnAdventureTask.NAME:
return new SendOnAdventureTask(this);
}
this.logError('TASK NOT FOUND', taskName);
return undefined;
}
private popActionCommand(): Command | undefined {
const actionItem = this.actionQueue.pop();
if (actionItem === undefined) {
return undefined;
}
return actionItem;
}
private createActionControllerByName(
actonName: string
): ActionController | undefined {
if (actonName === UpgradeBuildingAction.NAME) {
return new UpgradeBuildingAction(this);
}
if (actonName === GoToPageAction.NAME) {
return new GoToPageAction();
}
if (actonName === ClickButtonAction.NAME) {
return new ClickButtonAction();
}
if (actonName === CompleteTaskAction.NAME) {
return new CompleteTaskAction(this);
}
if (actonName === UpgradeBuildingAction.NAME) {
return new UpgradeBuildingAction();
}
if (actonName === CheckBuildingRemainingTimeAction.NAME) {
return new CheckBuildingRemainingTimeAction();
}
if (actonName === GrabHeroAttributesAction.NAME) {
return new GrabHeroAttributesAction(this.gameState);
}
if (actonName === SendOnAdventureAction.NAME) {
return new SendOnAdventureAction(this.gameState);
}
this.logError('ACTION NOT FOUND', actonName);
return undefined;
}
@ -138,14 +168,17 @@ export default class Scheduler {
await action.run(args, task);
} catch (e) {
console.warn('ACTION ABORTED', e.message);
this.actionQueue.clear();
if (e instanceof AbortTaskError) {
console.warn('ABORT TASK', e.id);
this.completeTask(e.id);
}
if (e instanceof TryLaterError) {
console.warn('TRY', task.id, 'AFTER', e.seconds);
this.actionQueue.clear();
this.taskQueue.postpone(task.id, timestamp() + e.seconds);
}
if (e instanceof BuildingQueueFullError) {
console.warn('BUILDING QUEUE FULL, TRY ALL AFTER', e.seconds);
this.actionQueue.clear();
this.taskQueue.modify(
t => t.cmd.name === UpgradeBuildingTask.NAME,
t => t.withTime(timestamp() + e.seconds)

34
src/Storage/GameState.ts Normal file
View File

@ -0,0 +1,34 @@
const NAMESPACE = 'game_state:v1';
function join(x: string, y: string) {
return x.replace(/[:]+$/g, '') + ':' + y.replace(/^[:]+/g, '');
}
export class GameState {
get(key: string): any {
this.log('GET', key);
try {
const serialized = localStorage.getItem(join(NAMESPACE, key));
return JSON.parse(serialized || 'null');
} catch (e) {
if (e instanceof SyntaxError) {
return null;
}
throw e;
}
}
has(key: string): boolean {
return localStorage.getItem(join(NAMESPACE, key)) === null;
}
set(key: string, value: any) {
let serialized = JSON.stringify(value);
this.log('SET', key, serialized);
localStorage.setItem(join(NAMESPACE, key), serialized);
}
private log(...args) {
console.log('GAME STATE:', ...args);
}
}

View File

@ -49,6 +49,15 @@ export class TaskQueue {
return readyItems[0];
}
has(predicate: (t: Task) => boolean): boolean {
const [matched, _] = this.split(predicate);
return matched.length > 0;
}
hasNamed(name: string): boolean {
return this.has(t => t.cmd.name === name);
}
modify(predicate: (t: Task) => boolean, modifier: (t: Task) => Task) {
const [matched, other] = this.split(predicate);
const modified = matched.map(modifier);

View File

@ -0,0 +1,41 @@
import Scheduler from '../Scheduler';
import { Args, Command } from '../Common';
import { Task } from '../Storage/TaskQueue';
import TaskController from './TaskController';
import GoToPageAction from '../Action/GoToPageAction';
import GrabHeroAttributesAction from '../Action/GrabHeroAttributesAction';
import CompleteTaskAction from '../Action/CompleteTaskAction';
import SendOnAdventureAction from '../Action/SendOnAdventureAction';
import ClickButtonAction from '../Action/ClickButtonAction';
export default class SendOnAdventureTask extends TaskController {
static NAME = 'send_on_adventure';
private scheduler: Scheduler;
constructor(scheduler: Scheduler) {
super();
this.scheduler = scheduler;
}
name(): string {
return SendOnAdventureTask.NAME;
}
run(task: Task) {
const args: Args = { ...task.cmd.args, taskId: task.id };
this.scheduler.scheduleActions([
new Command(GoToPageAction.NAME, { ...args, path: 'hero.php' }),
new Command(GrabHeroAttributesAction.NAME, args),
new Command(GoToPageAction.NAME, {
...args,
path: '/hero.php?t=3',
}),
new Command(SendOnAdventureAction.NAME, args),
new Command(ClickButtonAction.NAME, {
...args,
selector: '.adventureSendButton button',
}),
new Command(CompleteTaskAction.NAME, args),
]);
}
}

View File

@ -1,5 +1,6 @@
import { Task } from '../Storage/TaskQueue';
export default abstract class TaskController {
abstract name(): string;
abstract run(task: Task);
}

View File

@ -5,6 +5,7 @@ import { Task } from '../Storage/TaskQueue';
import TaskController from './TaskController';
import GoToPageAction from '../Action/GoToPageAction';
import CheckBuildingRemainingTimeAction from '../Action/CheckBuildingRemainingTimeAction';
import CompleteTaskAction from '../Action/CompleteTaskAction';
export default class UpgradeBuildingTask extends TaskController {
static NAME = 'upgrade_building';
@ -15,6 +16,10 @@ export default class UpgradeBuildingTask extends TaskController {
this.scheduler = scheduler;
}
name(): string {
return UpgradeBuildingTask.NAME;
}
run(task: Task) {
console.log('RUN', UpgradeBuildingTask.NAME, 'with', task);
const args: Args = { ...task.cmd.args, taskId: task.id };
@ -26,6 +31,7 @@ export default class UpgradeBuildingTask extends TaskController {
path: '/build.php?id=' + args.id,
}),
new Command(UpgradeBuildingAction.NAME, args),
new Command(CompleteTaskAction.NAME, args),
]);
}
}

View File

@ -30,6 +30,10 @@ export function timestamp(): number {
return Math.floor(Date.now() / 1000);
}
export function trimPrefix(text: string, prefix: string): string {
return text.startsWith(prefix) ? text.substr(prefix.length) : text;
}
export function markPage(text: string, version: string) {
jQuery('body').append(
'<div style="' +