Add hero adventures task
This commit is contained in:
parent
711c8a414b
commit
4b1e7cb676
14
src/Action/ClickButtonAction.ts
Normal file
14
src/Action/ClickButtonAction.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
17
src/Action/CompleteTaskAction.ts
Normal file
17
src/Action/CompleteTaskAction.ts
Normal 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);
|
||||
}
|
||||
}
|
35
src/Action/GrabHeroAttributesAction.ts
Normal file
35
src/Action/GrabHeroAttributesAction.ts
Normal 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 });
|
||||
}
|
||||
}
|
71
src/Action/SendOnAdventureAction.ts
Normal file
71
src/Action/SendOnAdventureAction.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
export default class GameState {}
|
@ -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
34
src/Storage/GameState.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
41
src/Task/SendOnAdventureTask.ts
Normal file
41
src/Task/SendOnAdventureTask.ts
Normal 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),
|
||||
]);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import { Task } from '../Storage/TaskQueue';
|
||||
|
||||
export default abstract class TaskController {
|
||||
abstract name(): string;
|
||||
abstract run(task: Task);
|
||||
}
|
||||
|
@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
@ -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="' +
|
||||
|
Loading…
Reference in New Issue
Block a user