Fix insert build tasks, so order is matter
This commit is contained in:
		| @@ -1,13 +1,14 @@ | ||||
| import { ActionController, registerAction } from './ActionController'; | ||||
| import { Args } from '../Common'; | ||||
| import { Task } from '../Storage/TaskQueue'; | ||||
| import { BuildingQueueFullError } from '../Errors'; | ||||
| import { BuildingQueueFullError, GrabError } from '../Errors'; | ||||
| import { grabActiveVillageId, grabBuildingQueueInfo } from '../Page/VillageBlock'; | ||||
| import { BuildingQueueInfo } from '../Game'; | ||||
|  | ||||
| @registerAction | ||||
| export class CheckBuildingRemainingTimeAction extends ActionController { | ||||
|     async run(args: Args, task: Task): Promise<any> { | ||||
|         const info = grabBuildingQueueInfo(); | ||||
|         const info = this.grabBuildingQueueInfoOrDefault(); | ||||
|         if (info.seconds > 0) { | ||||
|             throw new BuildingQueueFullError( | ||||
|                 task.id, | ||||
| @@ -17,4 +18,15 @@ export class CheckBuildingRemainingTimeAction extends ActionController { | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private grabBuildingQueueInfoOrDefault() { | ||||
|         try { | ||||
|             return grabBuildingQueueInfo(); | ||||
|         } catch (e) { | ||||
|             if (e instanceof GrabError) { | ||||
|                 return new BuildingQueueInfo(0); | ||||
|             } | ||||
|             throw e; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { clickUpgradeButton } from '../Page/BuildingPage'; | ||||
| import { grabResourceDeposits } from '../Page/SlotBlock'; | ||||
| import { UpgradeBuildingTask } from '../Task/UpgradeBuildingTask'; | ||||
| import { ResourceDeposit } from '../Game'; | ||||
| import { aroundMinutes } from '../utils'; | ||||
|  | ||||
| @registerAction | ||||
| export class UpgradeResourceToLevel extends ActionController { | ||||
| @@ -35,19 +36,16 @@ export class UpgradeResourceToLevel extends ActionController { | ||||
|                     task.args.buildId === dep.buildId | ||||
|             ); | ||||
|  | ||||
|         const available = deposits | ||||
|             .sort((x, y) => x.level - y.level) | ||||
|             .filter(dep => dep.ready) | ||||
|             .filter(isDepositTaskNotInQueue); | ||||
|         const notUpgraded = deposits.sort((x, y) => x.level - y.level).filter(isDepositTaskNotInQueue); | ||||
|  | ||||
|         if (available.length === 0) { | ||||
|             throw new TryLaterError(task.id, 10 * 60, 'No available deposits'); | ||||
|         if (notUpgraded.length === 0) { | ||||
|             throw new TryLaterError(task.id, aroundMinutes(10), 'No available deposits'); | ||||
|         } | ||||
|  | ||||
|         const targetDep = available[0]; | ||||
|         for (let dep of notUpgraded) { | ||||
|             this.scheduler.scheduleTask(UpgradeBuildingTask.name, { villageId, buildId: dep.buildId }); | ||||
|         } | ||||
|  | ||||
|         this.scheduler.scheduleTask(UpgradeBuildingTask.name, { villageId, buildId: targetDep.buildId }); | ||||
|  | ||||
|         throw new TryLaterError(task.id, 20 * 60, 'Sleep for next round'); | ||||
|         throw new TryLaterError(task.id, aroundMinutes(10), 'Sleep for next round'); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,8 +1,11 @@ | ||||
| import { ResourcesInterface } from './Game'; | ||||
|  | ||||
| export interface Args { | ||||
|     villageId?: number; | ||||
|     buildId?: number; | ||||
|     categoryId?: number; | ||||
|     buildTypeId?: number; | ||||
|     resources?: ResourcesInterface; | ||||
|     [name: string]: any; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -23,7 +23,14 @@ export function numberToResourceType(typeAsNumber: number): ResourceType { | ||||
|  | ||||
| export type ResourceList = Array<{ num: number; type: ResourceType; value: number }>; | ||||
|  | ||||
| export class Resources { | ||||
| export interface ResourcesInterface { | ||||
|     lumber: number; | ||||
|     clay: number; | ||||
|     iron: number; | ||||
|     crop: number; | ||||
| } | ||||
|  | ||||
| export class Resources implements ResourcesInterface { | ||||
|     readonly lumber: number; | ||||
|     readonly clay: number; | ||||
|     readonly iron: number; | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { Scheduler } from '../Scheduler'; | ||||
| import { TrainTroopTask } from '../Task/TrainTroopTask'; | ||||
| import { grabActiveVillageId } from './VillageBlock'; | ||||
| import { ConsoleLogger, Logger } from '../Logger'; | ||||
| import { createBuildButton, createUpgradeButton } from './BuildingPage'; | ||||
| import { createBuildButton, createUpgradeButton, grabContractResources } from './BuildingPage'; | ||||
| import { BuildBuildingTask } from '../Task/BuildBuildingTask'; | ||||
|  | ||||
| const QUARTERS_ID = 19; | ||||
| @@ -38,14 +38,16 @@ export class BuildPage { | ||||
|         const buildId = this.buildId; | ||||
|         const categoryId = this.categoryId; | ||||
|         const villageId = grabActiveVillageId(); | ||||
|         this.scheduler.scheduleTask(BuildBuildingTask.name, { villageId, buildId, categoryId, buildTypeId }); | ||||
|         const resources = grabContractResources(); | ||||
|         this.scheduler.scheduleTask(BuildBuildingTask.name, { villageId, buildId, categoryId, buildTypeId, resources }); | ||||
|         notify(`Building ${buildId} scheduled`); | ||||
|     } | ||||
|  | ||||
|     private onScheduleUpgradeBuilding() { | ||||
|         const buildId = this.buildId; | ||||
|         const villageId = grabActiveVillageId(); | ||||
|         this.scheduler.scheduleTask(UpgradeBuildingTask.name, { villageId, buildId }); | ||||
|         const resources = grabContractResources(); | ||||
|         this.scheduler.scheduleTask(UpgradeBuildingTask.name, { villageId, buildId, resources }); | ||||
|         notify(`Upgrading ${buildId} scheduled`); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { GrabError } from '../Errors'; | ||||
| import { getNumber, trimPrefix, uniqId } from '../utils'; | ||||
| import { Resources } from '../Game'; | ||||
|  | ||||
| export function clickBuildButton(typeId: number) { | ||||
|     const section = jQuery(`#contract_building${typeId}`); | ||||
| @@ -27,6 +28,11 @@ export function createBuildButton(onClickHandler: (buildTypeId: number) => void) | ||||
|     }); | ||||
| } | ||||
|  | ||||
| export function hasUpgradeButton(): boolean { | ||||
|     const btn = jQuery('.upgradeButtonsContainer .section1 button.green.build'); | ||||
|     return btn.length === 1; | ||||
| } | ||||
|  | ||||
| export function clickUpgradeButton() { | ||||
|     const btn = jQuery('.upgradeButtonsContainer .section1 button.green.build'); | ||||
|     if (btn.length !== 1) { | ||||
| @@ -45,3 +51,17 @@ export function createUpgradeButton(onClickHandler: () => void) { | ||||
|         onClickHandler(); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| export function grabContractResources(): Resources { | ||||
|     const $els = jQuery('#contract .resource'); | ||||
|     if ($els.length === 0) { | ||||
|         throw new GrabError('No resource contract element'); | ||||
|     } | ||||
|     const grab = n => | ||||
|         getNumber( | ||||
|             jQuery($els.get(n)) | ||||
|                 .find('.value') | ||||
|                 .text() | ||||
|         ); | ||||
|     return new Resources(grab(0), grab(1), grab(2), grab(3)); | ||||
| } | ||||
|   | ||||
| @@ -47,7 +47,7 @@ function grabVillageInfo($el): Village { | ||||
| export function grabResourcesPerformance(): Resources { | ||||
|     const $el = jQuery('#production'); | ||||
|     if ($el.length !== 1) { | ||||
|         throw new GrabError(); | ||||
|         throw new GrabError('No production element'); | ||||
|     } | ||||
|  | ||||
|     const $nums = $el.find('td.num'); | ||||
| @@ -63,7 +63,7 @@ export function grabResourcesPerformance(): Resources { | ||||
| export function grabBuildingQueueInfo(): BuildingQueueInfo { | ||||
|     const timer = jQuery('.buildDuration .timer'); | ||||
|     if (timer.length !== 1) { | ||||
|         throw new GrabError(); | ||||
|         throw new GrabError('No building queue timer element'); | ||||
|     } | ||||
|  | ||||
|     const remainingSeconds = getNumber(timer.attr('value')); | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { timestamp } from './utils'; | ||||
| import { UpgradeBuildingTask } from './Task/UpgradeBuildingTask'; | ||||
| import { TaskId, TaskList, TaskQueue } from './Storage/TaskQueue'; | ||||
| import { Task, TaskId, TaskList, TaskQueue } from './Storage/TaskQueue'; | ||||
| import { Args, Command } from './Common'; | ||||
| import { SendOnAdventureTask } from './Task/SendOnAdventureTask'; | ||||
| import { BalanceHeroResourcesTask } from './Task/BalanceHeroResourcesTask'; | ||||
| @@ -50,8 +50,18 @@ export class Scheduler { | ||||
|  | ||||
|     scheduleTask(name: string, args: Args): void { | ||||
|         this.logger.log('PUSH TASK', name, args); | ||||
|         const villageId = args.villageId; | ||||
|         if (isBuildingTask(name)) { | ||||
|             this.taskQueue.insertAfterLast( | ||||
|                 t => isBuildingTask(t.name) && sameVillage(villageId, t.args), | ||||
|                 name, | ||||
|                 args, | ||||
|                 timestamp() | ||||
|             ); | ||||
|         } else { | ||||
|             this.taskQueue.push(name, args, timestamp()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     completeTask(id: TaskId) { | ||||
|         this.taskQueue.complete(id); | ||||
| @@ -69,11 +79,7 @@ export class Scheduler { | ||||
|  | ||||
|     postponeBuildingsInVillage(villageId: number, seconds: number) { | ||||
|         this.taskQueue.modify( | ||||
|             t => t.name === BuildBuildingTask.name && t.args.villageId === villageId, | ||||
|             t => t.withTime(timestamp() + seconds) | ||||
|         ); | ||||
|         this.taskQueue.modify( | ||||
|             t => t.name === UpgradeBuildingTask.name && t.args.villageId === villageId, | ||||
|             t => isBuildingTask(t.name) && sameVillage(villageId, t.args), | ||||
|             t => t.withTime(timestamp() + seconds) | ||||
|         ); | ||||
|     } | ||||
| @@ -86,3 +92,11 @@ export class Scheduler { | ||||
|         this.actionQueue.clear(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function isBuildingTask(taskName: string) { | ||||
|     return taskName === BuildBuildingTask.name || taskName === UpgradeBuildingTask.name; | ||||
| } | ||||
|  | ||||
| function sameVillage(villageId: number | undefined, args: Args) { | ||||
|     return villageId !== undefined && args.villageId === villageId; | ||||
| } | ||||
|   | ||||
| @@ -2,6 +2,8 @@ import { StateGrabber } from './StateGrabber'; | ||||
| import { grabActiveVillageId, grabBuildingQueueInfo, grabResourcesPerformance } from '../Page/VillageBlock'; | ||||
| import { VillageState } from './VillageState'; | ||||
| import { parseLocation } from '../utils'; | ||||
| import { GrabError } from '../Errors'; | ||||
| import { BuildingQueueInfo } from '../Game'; | ||||
|  | ||||
| export class VillageOverviewPageGrabber extends StateGrabber { | ||||
|     grab(): void { | ||||
| @@ -13,6 +15,17 @@ export class VillageOverviewPageGrabber extends StateGrabber { | ||||
|         const villageId = grabActiveVillageId(); | ||||
|         const state = new VillageState(villageId); | ||||
|         state.storeResourcesPerformance(grabResourcesPerformance()); | ||||
|         state.storeBuildingQueueInfo(grabBuildingQueueInfo()); | ||||
|         state.storeBuildingQueueInfo(this.grabBuildingQueueInfoOrDefault()); | ||||
|     } | ||||
|  | ||||
|     private grabBuildingQueueInfoOrDefault() { | ||||
|         try { | ||||
|             return grabBuildingQueueInfo(); | ||||
|         } catch (e) { | ||||
|             if (e instanceof GrabError) { | ||||
|                 return new BuildingQueueInfo(0); | ||||
|             } | ||||
|             throw e; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -50,6 +50,15 @@ export class TaskQueue { | ||||
|         return task; | ||||
|     } | ||||
|  | ||||
|     insertAfterLast(predicate: (t: Task) => boolean, name: string, args: Args, ts: number): Task { | ||||
|         const id = uniqTaskId(); | ||||
|         const task = new Task(id, ts, name, args); | ||||
|         this.logger.log('INSERT AFTER TASK', id, ts, name, args); | ||||
|         const items = insertTaskAfter(task, this.getItems(), predicate); | ||||
|         this.flushItems(items); | ||||
|         return task; | ||||
|     } | ||||
|  | ||||
|     get(ts: number): Task | undefined { | ||||
|         const readyItems = this.getItems().filter(t => t.ts <= ts); | ||||
|         if (readyItems.length === 0) { | ||||
| @@ -133,3 +142,27 @@ export class TaskQueue { | ||||
|         this.storage.set(QUEUE_NAME, normalized); | ||||
|     } | ||||
| } | ||||
|  | ||||
| function insertTaskAfter(task: Task, tasks: TaskList, predicate: (t: Task) => boolean): TaskList { | ||||
|     const queuedTaskIndex = findLastIndex(tasks, predicate); | ||||
|     if (queuedTaskIndex === undefined) { | ||||
|         tasks.push(task); | ||||
|         return tasks; | ||||
|     } | ||||
|     const queuedTask = tasks[queuedTaskIndex]; | ||||
|     let insertedTask = task.withTime(Math.max(task.ts, queuedTask.ts + 1)); | ||||
|     tasks.splice(queuedTaskIndex, 0, insertedTask); | ||||
|     return tasks; | ||||
| } | ||||
|  | ||||
| function findLastIndex(tasks: TaskList, predicate: (t: Task) => boolean): number | undefined { | ||||
|     const count = tasks.length; | ||||
|     const indexInReversed = tasks | ||||
|         .slice() | ||||
|         .reverse() | ||||
|         .findIndex(predicate); | ||||
|     if (indexInReversed < 0) { | ||||
|         return undefined; | ||||
|     } | ||||
|     return count - 1 - indexInReversed; | ||||
| } | ||||
|   | ||||
| @@ -22,6 +22,12 @@ export async function sleepLong() { | ||||
|     return await sleep(ms); | ||||
| } | ||||
|  | ||||
| export function aroundMinutes(minutes: number) { | ||||
|     const seconds = minutes * 60; | ||||
|     const delta = Math.floor(seconds * 0.9); | ||||
|     return seconds - delta + Math.floor(Math.random() * 2 * delta); | ||||
| } | ||||
|  | ||||
| export async function waitForLoad() { | ||||
|     return new Promise(resolve => jQuery(resolve)); | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user