From 4b1e7cb676fa6772328299c10ea14dcaa491e1fe Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Thu, 2 Apr 2020 22:22:56 +0300 Subject: [PATCH] Add hero adventures task --- src/Action/ClickButtonAction.ts | 14 +++++ src/Action/CompleteTaskAction.ts | 17 ++++++ src/Action/GrabHeroAttributesAction.ts | 35 +++++++++++++ src/Action/SendOnAdventureAction.ts | 71 ++++++++++++++++++++++++++ src/Action/UpgradeBuildingAction.ts | 16 ++---- src/Dashboard.ts | 2 +- src/Errors.ts | 20 +++++++- src/GameState.ts | 1 - src/Scheduler.ts | 69 ++++++++++++++++++------- src/Storage/GameState.ts | 34 ++++++++++++ src/Storage/TaskQueue.ts | 9 ++++ src/Task/SendOnAdventureTask.ts | 41 +++++++++++++++ src/Task/TaskController.ts | 1 + src/Task/UpgradeBuildingTask.ts | 6 +++ src/utils.ts | 4 ++ 15 files changed, 307 insertions(+), 33 deletions(-) create mode 100644 src/Action/ClickButtonAction.ts create mode 100644 src/Action/CompleteTaskAction.ts create mode 100644 src/Action/GrabHeroAttributesAction.ts create mode 100644 src/Action/SendOnAdventureAction.ts delete mode 100644 src/GameState.ts create mode 100644 src/Storage/GameState.ts create mode 100644 src/Task/SendOnAdventureTask.ts diff --git a/src/Action/ClickButtonAction.ts b/src/Action/ClickButtonAction.ts new file mode 100644 index 0000000..974f6bd --- /dev/null +++ b/src/Action/ClickButtonAction.ts @@ -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 { + const el = jQuery(args.selector); + if (el.length === 1) { + console.log('CLICK BUTTON', el); + el.trigger('click'); + } + } +} diff --git a/src/Action/CompleteTaskAction.ts b/src/Action/CompleteTaskAction.ts new file mode 100644 index 0000000..59a0dd5 --- /dev/null +++ b/src/Action/CompleteTaskAction.ts @@ -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 { + this.scheduler.completeTask(task.id); + } +} diff --git a/src/Action/GrabHeroAttributesAction.ts b/src/Action/GrabHeroAttributesAction.ts new file mode 100644 index 0000000..9ebca22 --- /dev/null +++ b/src/Action/GrabHeroAttributesAction.ts @@ -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 { + 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 }); + } +} diff --git a/src/Action/SendOnAdventureAction.ts b/src/Action/SendOnAdventureAction.ts new file mode 100644 index 0000000..d3b8351 --- /dev/null +++ b/src/Action/SendOnAdventureAction.ts @@ -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 { + 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 { + const adventures: Array = []; + + 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; + } +} diff --git a/src/Action/UpgradeBuildingAction.ts b/src/Action/UpgradeBuildingAction.ts index 70540c5..89b2b1a 100644 --- a/src/Action/UpgradeBuildingAction.ts +++ b/src/Action/UpgradeBuildingAction.ts @@ -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 { 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'); } } diff --git a/src/Dashboard.ts b/src/Dashboard.ts index 99d79bc..348c624 100644 --- a/src/Dashboard.ts +++ b/src/Dashboard.ts @@ -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'; diff --git a/src/Errors.ts b/src/Errors.ts index 832d9b4..0373ad7 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -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); } } diff --git a/src/GameState.ts b/src/GameState.ts deleted file mode 100644 index 65205ac..0000000 --- a/src/GameState.ts +++ /dev/null @@ -1 +0,0 @@ -export default class GameState {} diff --git a/src/Scheduler.ts b/src/Scheduler.ts index f5e470c..8921060 100644 --- a/src/Scheduler.ts +++ b/src/Scheduler.ts @@ -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) diff --git a/src/Storage/GameState.ts b/src/Storage/GameState.ts new file mode 100644 index 0000000..f06c588 --- /dev/null +++ b/src/Storage/GameState.ts @@ -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); + } +} diff --git a/src/Storage/TaskQueue.ts b/src/Storage/TaskQueue.ts index 72411d5..b02cc2c 100644 --- a/src/Storage/TaskQueue.ts +++ b/src/Storage/TaskQueue.ts @@ -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); diff --git a/src/Task/SendOnAdventureTask.ts b/src/Task/SendOnAdventureTask.ts new file mode 100644 index 0000000..3c821c8 --- /dev/null +++ b/src/Task/SendOnAdventureTask.ts @@ -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), + ]); + } +} diff --git a/src/Task/TaskController.ts b/src/Task/TaskController.ts index 55a8dd3..dbb7fe3 100644 --- a/src/Task/TaskController.ts +++ b/src/Task/TaskController.ts @@ -1,5 +1,6 @@ import { Task } from '../Storage/TaskQueue'; export default abstract class TaskController { + abstract name(): string; abstract run(task: Task); } diff --git a/src/Task/UpgradeBuildingTask.ts b/src/Task/UpgradeBuildingTask.ts index 513defc..d9fcd25 100644 --- a/src/Task/UpgradeBuildingTask.ts +++ b/src/Task/UpgradeBuildingTask.ts @@ -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), ]); } } diff --git a/src/utils.ts b/src/utils.ts index 420d89a..2b4764e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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( '