From 8bea617f5b28bec7ffb6e54dacdec3a601f12fdf Mon Sep 17 00:00:00 2001 From: Anton Vakhrushev Date: Sun, 24 May 2020 17:23:13 +0300 Subject: [PATCH] Rebuild village production queue system --- src/Action/CompleteTaskAction.ts | 2 +- src/Container.ts | 57 ++++- src/ControlPanel.ts | 23 +- src/Core/Contract.ts | 10 + src/Core/ProductionQueue.ts | 17 ++ src/Executor.ts | 44 ++-- src/Grabber/BuildingContractGrabber.ts | 7 +- src/Grabber/ForgePageGrabber.ts | 21 +- src/Grabber/Grabber.ts | 8 +- src/Grabber/GrabberManager.ts | 31 ++- src/Grabber/GuildHallPageGrabber.ts | 7 +- src/Grabber/MarketPageGrabber.ts | 7 +- src/Grabber/VillageOverviewPageGrabber.ts | 6 +- src/Grabber/VillageResourceGrabber.ts | 9 +- src/Page/BuildingPageController.ts | 31 ++- src/Queue/DataStorageTaskProvider.ts | 5 +- src/Queue/TaskProvider.ts | 15 +- src/Queue/TaskQueue.ts | 16 +- src/Scheduler.ts | 263 +++++++--------------- src/Storage/VillageStorage.ts | 62 ++++- src/Task/RunVillageProductionTask.ts | 11 + src/Task/UpdateResourceContracts.ts | 7 +- src/VillageController.ts | 93 ++++++++ src/VillageControllerFactory.ts | 21 ++ src/VillageRepository.ts | 18 ++ src/VillageState.ts | 65 +++--- 26 files changed, 520 insertions(+), 336 deletions(-) create mode 100644 src/Core/Contract.ts create mode 100644 src/Task/RunVillageProductionTask.ts create mode 100644 src/VillageController.ts create mode 100644 src/VillageControllerFactory.ts diff --git a/src/Action/CompleteTaskAction.ts b/src/Action/CompleteTaskAction.ts index 44451f0..fecaaa2 100644 --- a/src/Action/CompleteTaskAction.ts +++ b/src/Action/CompleteTaskAction.ts @@ -5,6 +5,6 @@ import { Task } from '../Queue/TaskProvider'; @registerAction export class CompleteTaskAction extends ActionController { async run(args: Args, task: Task): Promise { - this.scheduler.removeTask(task.id); + this.scheduler.completeTask(task.id); } } diff --git a/src/Container.ts b/src/Container.ts index 8a683dd..06de00e 100644 --- a/src/Container.ts +++ b/src/Container.ts @@ -10,6 +10,8 @@ import { StatisticsStorage } from './Storage/StatisticsStorage'; import { VillageRepository } from './VillageRepository'; import { VillageStateRepository } from './VillageState'; import { LogStorage } from './Storage/LogStorage'; +import { VillageControllerFactory } from './VillageControllerFactory'; +import { GrabberManager } from './Grabber/GrabberManager'; export class Container { private readonly version: string; @@ -40,16 +42,35 @@ export class Container { return this._statistics; } + private _villageControllerFactory: VillageControllerFactory | undefined; + + get villageControllerFactory(): VillageControllerFactory { + this._villageControllerFactory = + this._villageControllerFactory || + (() => { + return new VillageControllerFactory(this.villageRepository); + })(); + return this._villageControllerFactory; + } + private _scheduler: Scheduler | undefined; get scheduler(): Scheduler { this._scheduler = this._scheduler || (() => { - const taskProvider = DataStorageTaskProvider.create(); - const taskQueue = new TaskQueue(taskProvider, new ConsoleLogger(TaskQueue.name)); + const taskQueue = new TaskQueue( + DataStorageTaskProvider.create('tasks:v1'), + new ConsoleLogger(TaskQueue.name) + ); const actionQueue = new ActionQueue(); - return new Scheduler(taskQueue, actionQueue, this.villageRepository, new ConsoleLogger(Scheduler.name)); + return new Scheduler( + taskQueue, + actionQueue, + this.villageRepository, + this.villageControllerFactory, + new ConsoleLogger(Scheduler.name) + ); })(); return this._scheduler; } @@ -60,11 +81,22 @@ export class Container { this._villageStateRepository = this._villageStateRepository || (() => { - return new VillageStateRepository(this.villageRepository, this.scheduler); + return new VillageStateRepository(this.villageRepository, this.villageControllerFactory); })(); return this._villageStateRepository; } + private _grabberManager: GrabberManager | undefined; + + get grabberManager(): GrabberManager { + this._grabberManager = + this._grabberManager || + (() => { + return new GrabberManager(this.villageControllerFactory); + })(); + return this._grabberManager; + } + private _executor: Executor | undefined; get executor(): Executor { @@ -75,7 +107,15 @@ export class Container { new ConsoleLogger(Executor.name), new StorageLogger(new LogStorage(), LogLevel.warning), ]); - return new Executor(this.version, this.scheduler, this.villageStateRepository, this.statistics, logger); + return new Executor( + this.version, + this.scheduler, + this.villageStateRepository, + this.villageControllerFactory, + this.grabberManager, + this.statistics, + logger + ); })(); return this._executor; } @@ -86,7 +126,12 @@ export class Container { this._controlPanel = this._controlPanel || (() => { - return new ControlPanel(this.version, this.scheduler, this.villageStateRepository); + return new ControlPanel( + this.version, + this.scheduler, + this.villageStateRepository, + this.villageControllerFactory + ); })(); return this._controlPanel; } diff --git a/src/ControlPanel.ts b/src/ControlPanel.ts index ea921bd..93347bb 100644 --- a/src/ControlPanel.ts +++ b/src/ControlPanel.ts @@ -22,6 +22,7 @@ import { VillageState, VillageStateRepository } from './VillageState'; import { Task } from './Queue/TaskProvider'; import { Action } from './Queue/ActionQueue'; import { createStore } from './DashboardView/Store'; +import { VillageControllerFactory } from './VillageControllerFactory'; Vue.use(Vuex); @@ -53,11 +54,18 @@ export class ControlPanel { private readonly scheduler: Scheduler; private readonly villageStateRepository: VillageStateRepository; private readonly logger: Logger; + private villageControllerFactory: VillageControllerFactory; - constructor(version: string, scheduler: Scheduler, villageStateRepository: VillageStateRepository) { + constructor( + version: string, + scheduler: Scheduler, + villageStateRepository: VillageStateRepository, + villageControllerFactory: VillageControllerFactory + ) { this.version = version; this.scheduler = scheduler; this.villageStateRepository = villageStateRepository; + this.villageControllerFactory = villageControllerFactory; this.logger = new ConsoleLogger(this.constructor.name); } @@ -128,9 +136,10 @@ export class ControlPanel { DataStorage.onChange(() => state.refresh()); const getBuildingsInQueue = () => - this.scheduler - .getTaskItems() - .filter(t => t.name === UpgradeBuildingTask.name && t.args.villageId === villageId) + this.villageControllerFactory + .create(villageId) + .getTasks() + .filter(t => t.name === UpgradeBuildingTask.name) .map(t => t.args.buildId || 0); if (p.pathname === '/dorf1.php') { @@ -151,7 +160,11 @@ export class ControlPanel { } if (isBuildingPage()) { - const buildPage = new BuildingPageController(this.scheduler, getBuildingPageAttributes()); + const buildPage = new BuildingPageController( + this.scheduler, + getBuildingPageAttributes(), + this.villageControllerFactory.create(villageId) + ); buildPage.run(); } diff --git a/src/Core/Contract.ts b/src/Core/Contract.ts new file mode 100644 index 0000000..eb5c80d --- /dev/null +++ b/src/Core/Contract.ts @@ -0,0 +1,10 @@ +export enum ContractType { + UpgradeBuilding, + ImproveTrooper, +} + +export interface ContractAttributes { + type: ContractType; + buildId?: number; + unitId?: number; +} diff --git a/src/Core/ProductionQueue.ts b/src/Core/ProductionQueue.ts index f003199..6352a6b 100644 --- a/src/Core/ProductionQueue.ts +++ b/src/Core/ProductionQueue.ts @@ -1,3 +1,5 @@ +import { getProductionQueue } from '../Task/TaskMap'; + export enum ProductionQueue { Building = 'building', TrainUnit = 'train_unit', @@ -27,3 +29,18 @@ export function translateProductionQueue(queue: ProductionQueue): string { return 'Празднование'; } } + +export interface TaskNamePredicate { + (name: string): boolean; +} + +/** + * List on non intersected task queue predicates. + */ +export const TASK_TYPE_PREDICATES: Array = ProductionQueueTypes.map(queue => { + return (taskName: string) => getProductionQueue(taskName) === queue; +}); + +export function isProductionTask(taskName: string): boolean { + return TASK_TYPE_PREDICATES.reduce((memo, predicate) => memo || predicate(taskName), false); +} diff --git a/src/Executor.ts b/src/Executor.ts index e0201ce..923cd52 100644 --- a/src/Executor.ts +++ b/src/Executor.ts @@ -11,6 +11,7 @@ import { Action } from './Queue/ActionQueue'; import { Task } from './Queue/TaskProvider'; import { createTaskHandler } from './Task/TaskMap'; import { VillageStateRepository } from './VillageState'; +import { VillageControllerFactory } from './VillageControllerFactory'; export interface ExecutionSettings { pauseTs: number; @@ -20,7 +21,8 @@ export class Executor { private readonly version: string; private readonly scheduler: Scheduler; private readonly villageStateRepository: VillageStateRepository; - private grabbers: GrabberManager; + private villageControllerFactory: VillageControllerFactory; + private grabberManager: GrabberManager; private statistics: Statistics; private executionState: ExecutionStorage; private logger: Logger; @@ -29,13 +31,16 @@ export class Executor { version: string, scheduler: Scheduler, villageStateRepository: VillageStateRepository, + villageControllerFactory: VillageControllerFactory, + grabberManager: GrabberManager, statistics: Statistics, logger: Logger ) { this.version = version; this.scheduler = scheduler; this.villageStateRepository = villageStateRepository; - this.grabbers = new GrabberManager(scheduler); + this.villageControllerFactory = villageControllerFactory; + this.grabberManager = grabberManager; this.statistics = statistics; this.executionState = new ExecutionStorage(); this.logger = logger; @@ -49,6 +54,7 @@ export class Executor { const sleep = createExecutionLoopSleeper(); + // noinspection InfiniteLoopJS while (true) { await sleep(); if (!this.isPaused()) { @@ -76,25 +82,22 @@ export class Executor { } private async doTaskProcessingStep() { + this.runGrabbers(); + const currentTs = timestamp(); - const task = this.scheduler.nextTask(currentTs); + const { task, action } = this.scheduler.nextTask(currentTs); // текущего таска нет, очищаем очередь действий по таску if (!task) { this.logger.info('NO ACTIVE TASK'); - this.scheduler.clearActions(); return; } - const actionCommand = this.scheduler.nextAction(); - - this.logger.info('CURRENT JOB', 'TASK', task, 'ACTION', actionCommand); - - this.runGrabbers(); + this.logger.info('CURRENT JOB', 'TASK', task, 'ACTION', action); try { - if (actionCommand) { - return await this.processActionCommand(actionCommand, task); + if (task && action) { + return await this.processActionCommand(action, task); } if (task) { @@ -105,27 +108,24 @@ export class Executor { } } - private async processActionCommand(cmd: Action, task: Task) { - const actionHandler = createActionHandler(cmd.name, this.scheduler, this.villageStateRepository); - this.logger.info('PROCESS ACTION', cmd.name, actionHandler); - if (cmd.args.taskId !== task.id) { - throw new ActionError(`Action task id ${cmd.args.taskId} not equal current task id ${task.id}`); - } + private async processActionCommand(action: Action, task: Task) { + const actionHandler = createActionHandler(action.name, this.scheduler, this.villageStateRepository); + this.logger.info('Process action', action.name, actionHandler); if (actionHandler) { this.statistics.incrementAction(timestamp()); - await actionHandler.run(cmd.args, task); + await actionHandler.run(action.args, task); } else { - this.logger.warn('ACTION NOT FOUND', cmd.name); + this.logger.error('Action not found', action.name); } } private async processTaskCommand(task: Task) { const taskHandler = createTaskHandler(task.name, this.scheduler); - this.logger.info('PROCESS TASK', task.name, task, taskHandler); + this.logger.info('Process task', task.name, task, taskHandler); if (taskHandler) { await taskHandler.run(task); } else { - this.logger.warn('TASK NOT FOUND', task.name); + this.logger.error('Task handler not created', task.name); this.scheduler.removeTask(task.id); } } @@ -174,7 +174,7 @@ export class Executor { private runGrabbers() { try { this.logger.info('Rug grabbers'); - this.grabbers.grab(); + this.grabberManager.grab(); } catch (e) { this.logger.warn('Grabbers fails with', e.message); } diff --git a/src/Grabber/BuildingContractGrabber.ts b/src/Grabber/BuildingContractGrabber.ts index 1ef1bab..8228c52 100644 --- a/src/Grabber/BuildingContractGrabber.ts +++ b/src/Grabber/BuildingContractGrabber.ts @@ -1,8 +1,7 @@ import { Grabber } from './Grabber'; -import { grabActiveVillageId } from '../Page/VillageBlock'; import { getBuildingPageAttributes, isBuildingPage } from '../Page/PageDetectors'; import { grabContractResources, hasContractResources } from '../Page/BuildingPage/BuildingPage'; -import { ContractType } from '../Scheduler'; +import { ContractType } from '../Core/Contract'; export class BuildingContractGrabber extends Grabber { grab(): void { @@ -19,12 +18,10 @@ export class BuildingContractGrabber extends Grabber { return; } - const villageId = grabActiveVillageId(); const contract = grabContractResources(); - this.scheduler.updateResources(contract, { + this.controller.updateResources(contract, { type: ContractType.UpgradeBuilding, - villageId, buildId: building.buildId, }); } diff --git a/src/Grabber/ForgePageGrabber.ts b/src/Grabber/ForgePageGrabber.ts index c2c3895..131db7c 100644 --- a/src/Grabber/ForgePageGrabber.ts +++ b/src/Grabber/ForgePageGrabber.ts @@ -1,9 +1,7 @@ import { Grabber } from './Grabber'; -import { grabActiveVillageId } from '../Page/VillageBlock'; import { getBuildingPageAttributes, isForgePage } from '../Page/PageDetectors'; -import { ContractType } from '../Scheduler'; +import { ContractType } from '../Core/Contract'; import { grabImprovementContracts, grabRemainingSeconds } from '../Page/BuildingPage/ForgePage'; -import { VillageStorage } from '../Storage/VillageStorage'; import { ProductionQueue } from '../Core/ProductionQueue'; import { timestamp } from '../utils'; @@ -13,29 +11,26 @@ export class ForgePageGrabber extends Grabber { return; } - const villageId = grabActiveVillageId(); - - this.grabContracts(villageId); - this.grabTimer(villageId); + this.grabContracts(); + this.grabTimer(); } - private grabContracts(villageId: number): void { + private grabContracts(): void { const { buildId } = getBuildingPageAttributes(); const contracts = grabImprovementContracts(); for (let { resources, unitId } of contracts) { - this.scheduler.updateResources(resources, { + this.controller.updateResources(resources, { type: ContractType.ImproveTrooper, - villageId, buildId, unitId, }); } } - private grabTimer(villageId: number): void { - const state = new VillageStorage(villageId); + private grabTimer(): void { + const storage = this.controller.getStorage(); const seconds = grabRemainingSeconds(); - state.storeQueueTaskEnding(ProductionQueue.UpgradeUnit, seconds ? seconds + timestamp() : 0); + storage.storeQueueTaskEnding(ProductionQueue.UpgradeUnit, seconds ? seconds + timestamp() : 0); } } diff --git a/src/Grabber/Grabber.ts b/src/Grabber/Grabber.ts index 269be09..db0adc1 100644 --- a/src/Grabber/Grabber.ts +++ b/src/Grabber/Grabber.ts @@ -1,10 +1,10 @@ -import { Scheduler } from '../Scheduler'; +import { VillageController } from '../VillageController'; export abstract class Grabber { - protected scheduler: Scheduler; + protected controller: VillageController; - constructor(scheduler: Scheduler) { - this.scheduler = scheduler; + constructor(controller: VillageController) { + this.controller = controller; } abstract grab(): void; diff --git a/src/Grabber/GrabberManager.ts b/src/Grabber/GrabberManager.ts index b91761d..0b9632d 100644 --- a/src/Grabber/GrabberManager.ts +++ b/src/Grabber/GrabberManager.ts @@ -3,28 +3,35 @@ import { VillageResourceGrabber } from './VillageResourceGrabber'; import { VillageOverviewPageGrabber } from './VillageOverviewPageGrabber'; import { HeroPageGrabber } from './HeroPageGrabber'; import { MarketPageGrabber } from './MarketPageGrabber'; -import { Scheduler } from '../Scheduler'; import { BuildingContractGrabber } from './BuildingContractGrabber'; import { ForgePageGrabber } from './ForgePageGrabber'; import { GuildHallPageGrabber } from './GuildHallPageGrabber'; +import { VillageControllerFactory } from '../VillageControllerFactory'; export class GrabberManager { - private readonly grabbers: Array = []; + private factory: VillageControllerFactory; - constructor(scheduler: Scheduler) { - this.grabbers = []; - this.grabbers.push(new VillageResourceGrabber(scheduler)); - this.grabbers.push(new VillageOverviewPageGrabber(scheduler)); - this.grabbers.push(new HeroPageGrabber(scheduler)); - this.grabbers.push(new MarketPageGrabber(scheduler)); - this.grabbers.push(new BuildingContractGrabber(scheduler)); - this.grabbers.push(new ForgePageGrabber(scheduler)); - this.grabbers.push(new GuildHallPageGrabber(scheduler)); + constructor(factory: VillageControllerFactory) { + this.factory = factory; } grab() { - for (let grabber of this.grabbers) { + const grabbers = this.createGrabbers(); + for (let grabber of grabbers) { grabber.grab(); } } + + private createGrabbers(): Array { + const controller = this.factory.getActive(); + const grabbers: Array = []; + grabbers.push(new VillageResourceGrabber(controller)); + grabbers.push(new VillageOverviewPageGrabber(controller)); + grabbers.push(new HeroPageGrabber(controller)); + grabbers.push(new MarketPageGrabber(controller)); + grabbers.push(new BuildingContractGrabber(controller)); + grabbers.push(new ForgePageGrabber(controller)); + grabbers.push(new GuildHallPageGrabber(controller)); + return grabbers; + } } diff --git a/src/Grabber/GuildHallPageGrabber.ts b/src/Grabber/GuildHallPageGrabber.ts index 740f53d..e11fa05 100644 --- a/src/Grabber/GuildHallPageGrabber.ts +++ b/src/Grabber/GuildHallPageGrabber.ts @@ -1,6 +1,4 @@ import { Grabber } from './Grabber'; -import { grabActiveVillageId } from '../Page/VillageBlock'; -import { VillageStorage } from '../Storage/VillageStorage'; import { isGuildHallPage } from '../Page/PageDetectors'; import { grabRemainingSeconds } from '../Page/BuildingPage/GuildHallPage'; import { ProductionQueue } from '../Core/ProductionQueue'; @@ -12,9 +10,8 @@ export class GuildHallPageGrabber extends Grabber { return; } - const villageId = grabActiveVillageId(); - const state = new VillageStorage(villageId); const seconds = grabRemainingSeconds(); - state.storeQueueTaskEnding(ProductionQueue.Celebration, seconds ? seconds + timestamp() : 0); + const storage = this.controller.getStorage(); + storage.storeQueueTaskEnding(ProductionQueue.Celebration, seconds ? seconds + timestamp() : 0); } } diff --git a/src/Grabber/MarketPageGrabber.ts b/src/Grabber/MarketPageGrabber.ts index c67dd3c..973cab5 100644 --- a/src/Grabber/MarketPageGrabber.ts +++ b/src/Grabber/MarketPageGrabber.ts @@ -1,6 +1,4 @@ import { Grabber } from './Grabber'; -import { grabActiveVillageId } from '../Page/VillageBlock'; -import { VillageStorage } from '../Storage/VillageStorage'; import { isMarketSendResourcesPage } from '../Page/PageDetectors'; import { grabIncomingMerchants } from '../Page/BuildingPage/MarketPage'; @@ -10,8 +8,7 @@ export class MarketPageGrabber extends Grabber { return; } - const villageId = grabActiveVillageId(); - const state = new VillageStorage(villageId); - state.storeIncomingMerchants(grabIncomingMerchants()); + const storage = this.controller.getStorage(); + storage.storeIncomingMerchants(grabIncomingMerchants()); } } diff --git a/src/Grabber/VillageOverviewPageGrabber.ts b/src/Grabber/VillageOverviewPageGrabber.ts index 50b370a..d9d18ed 100644 --- a/src/Grabber/VillageOverviewPageGrabber.ts +++ b/src/Grabber/VillageOverviewPageGrabber.ts @@ -1,6 +1,5 @@ import { Grabber } from './Grabber'; -import { grabActiveVillageId, grabBuildingQueueInfo, grabResourcesPerformance } from '../Page/VillageBlock'; -import { VillageStorage } from '../Storage/VillageStorage'; +import { grabBuildingQueueInfo, grabResourcesPerformance } from '../Page/VillageBlock'; import { parseLocation, timestamp } from '../utils'; import { GrabError } from '../Errors'; import { BuildingQueueInfo } from '../Game'; @@ -13,8 +12,7 @@ export class VillageOverviewPageGrabber extends Grabber { return; } - const villageId = grabActiveVillageId(); - const storage = new VillageStorage(villageId); + const storage = this.controller.getStorage(); storage.storeResourcesPerformance(grabResourcesPerformance()); storage.storeBuildingQueueInfo(this.grabBuildingQueueInfoOrDefault()); diff --git a/src/Grabber/VillageResourceGrabber.ts b/src/Grabber/VillageResourceGrabber.ts index 8d12736..5fa6f81 100644 --- a/src/Grabber/VillageResourceGrabber.ts +++ b/src/Grabber/VillageResourceGrabber.ts @@ -1,13 +1,10 @@ import { Grabber } from './Grabber'; -import { grabActiveVillageId } from '../Page/VillageBlock'; import { grabVillageResources, grabVillageResourceStorage } from '../Page/ResourcesBlock'; -import { VillageStorage } from '../Storage/VillageStorage'; export class VillageResourceGrabber extends Grabber { grab(): void { - const villageId = grabActiveVillageId(); - const state = new VillageStorage(villageId); - state.storeResources(grabVillageResources()); - state.storeResourceStorage(grabVillageResourceStorage()); + const storage = this.controller.getStorage(); + storage.storeResources(grabVillageResources()); + storage.storeResourceStorage(grabVillageResourceStorage()); } } diff --git a/src/Page/BuildingPageController.ts b/src/Page/BuildingPageController.ts index b9c84ca..30b7d88 100644 --- a/src/Page/BuildingPageController.ts +++ b/src/Page/BuildingPageController.ts @@ -1,4 +1,4 @@ -import { notify, split } from '../utils'; +import { notify } from '../utils'; import { UpgradeBuildingTask } from '../Task/UpgradeBuildingTask'; import { Scheduler } from '../Scheduler'; import { TrainTroopTask } from '../Task/TrainTroopTask'; @@ -17,15 +17,18 @@ import { createResearchButtons } from './BuildingPage/ForgePage'; import { ForgeImprovementTask } from '../Task/ForgeImprovementTask'; import { createCelebrationButtons } from './BuildingPage/GuildHallPage'; import { CelebrationTask } from '../Task/CelebrationTask'; +import { VillageController } from '../VillageController'; export class BuildingPageController { private scheduler: Scheduler; private readonly attributes: BuildingPageAttributes; + private villageController: VillageController; private readonly logger: Logger; - constructor(scheduler: Scheduler, attributes: BuildingPageAttributes) { + constructor(scheduler: Scheduler, attributes: BuildingPageAttributes, villageController: VillageController) { this.scheduler = scheduler; this.attributes = attributes; + this.villageController = villageController; this.logger = new ConsoleLogger(this.constructor.name); } @@ -56,7 +59,7 @@ export class BuildingPageController { } if (isMarketSendResourcesPage()) { - createSendResourcesButton((res, crd) => this.onSendResources(res, crd)); + createSendResourcesButton((res, crd) => this.onSendResources(crd)); } if (isForgePage()) { @@ -64,7 +67,7 @@ export class BuildingPageController { } if (isGuildHallPage()) { - createCelebrationButtons((res, idx) => this.onCelebration(res, idx)); + createCelebrationButtons(res => this.onCelebration(res)); } } @@ -72,14 +75,20 @@ export class BuildingPageController { const buildId = this.attributes.buildId; const categoryId = this.attributes.categoryId; const villageId = grabActiveVillageId(); - this.scheduler.scheduleTask(BuildBuildingTask.name, { villageId, buildId, categoryId, buildTypeId, resources }); + this.villageController.addTask(BuildBuildingTask.name, { + villageId, + buildId, + categoryId, + buildTypeId, + resources, + }); notify(`Building ${buildId} scheduled`); } private onScheduleUpgradeBuilding(resources: Resources) { const buildId = this.attributes.buildId; const villageId = grabActiveVillageId(); - this.scheduler.scheduleTask(UpgradeBuildingTask.name, { villageId, buildId, resources }); + this.villageController.addTask(UpgradeBuildingTask.name, { villageId, buildId, resources }); notify(`Upgrading ${buildId} scheduled`); } @@ -94,11 +103,11 @@ export class BuildingPageController { troopResources: resources, resources: resources.scale(trainCount), }; - this.scheduler.scheduleTask(TrainTroopTask.name, args); + this.villageController.addTask(TrainTroopTask.name, args); notify(`Training ${trainCount} troopers scheduled`); } - private onSendResources(resources: Resources, coordinates: Coordinates) { + private onSendResources(coordinates: Coordinates) { const villageId = grabActiveVillageId(); const targetVillage = grabVillageList().find(v => v.crd.eq(coordinates)); this.scheduler.scheduleTask(SendResourcesTask.name, { @@ -114,7 +123,7 @@ export class BuildingPageController { private onResearch(resources: Resources, unitId: number) { const villageId = grabActiveVillageId(); - this.scheduler.scheduleTask(ForgeImprovementTask.name, { + this.villageController.addTask(ForgeImprovementTask.name, { villageId, buildTypeId: this.attributes.buildTypeId, buildId: this.attributes.buildId, @@ -124,9 +133,9 @@ export class BuildingPageController { notify(`Researching ${unitId} scheduled`); } - private onCelebration(resources: Resources, idx: number) { + private onCelebration(resources: Resources) { const villageId = grabActiveVillageId(); - this.scheduler.scheduleTask(CelebrationTask.name, { + this.villageController.addTask(CelebrationTask.name, { villageId, buildTypeId: this.attributes.buildTypeId, buildId: this.attributes.buildId, diff --git a/src/Queue/DataStorageTaskProvider.ts b/src/Queue/DataStorageTaskProvider.ts index 1a2e0e3..e27d95a 100644 --- a/src/Queue/DataStorageTaskProvider.ts +++ b/src/Queue/DataStorageTaskProvider.ts @@ -1,7 +1,6 @@ import { DataStorage } from '../DataStorage'; import { Task, TaskList, TaskProvider, uniqTaskId } from './TaskProvider'; -const NAMESPACE = 'tasks:v1'; const QUEUE_NAME = 'queue'; export class DataStorageTaskProvider implements TaskProvider { @@ -11,8 +10,8 @@ export class DataStorageTaskProvider implements TaskProvider { this.storage = storage; } - static create() { - return new DataStorageTaskProvider(new DataStorage(NAMESPACE)); + static create(namespace: string) { + return new DataStorageTaskProvider(new DataStorage(namespace)); } getTasks(): TaskList { diff --git a/src/Queue/TaskProvider.ts b/src/Queue/TaskProvider.ts index eb39b14..b5aaecd 100644 --- a/src/Queue/TaskProvider.ts +++ b/src/Queue/TaskProvider.ts @@ -1,10 +1,11 @@ import { Args } from './Args'; import { uniqId } from '../utils'; +import { ResourcesInterface } from '../Core/Resources'; export type TaskId = string; let idSequence = 1; -let lastTimestamp: number | null = null; +let lastTimestamp: number | undefined = undefined; export function uniqTaskId(): TaskId { const ts = Math.floor(Date.now() / 1000); @@ -38,3 +39,15 @@ export interface TaskProvider { getTasks(): TaskList; setTasks(tasks: TaskList): void; } + +export interface TaskTransformer { + (task: Task): Task; +} + +export function withTime(ts: number): TaskTransformer { + return (task: Task) => new Task(task.id, ts, task.name, task.args); +} + +export function withResources(resources: ResourcesInterface): TaskTransformer { + return (task: Task) => new Task(task.id, task.ts, task.name, { ...task.args, resources }); +} diff --git a/src/Queue/TaskQueue.ts b/src/Queue/TaskQueue.ts index 0ef198c..bf00a58 100644 --- a/src/Queue/TaskQueue.ts +++ b/src/Queue/TaskQueue.ts @@ -11,14 +11,10 @@ export class TaskQueue { this.logger = logger; } - push(name: string, args: Args, ts: number): Task { - const id = uniqTaskId(); - const task = new Task(id, ts, name, args); - this.logger.info('PUSH TASK', id, ts, name, args); + add(task: Task) { let items = this.getItems(); items.push(task); this.flushItems(items); - return task; } get(ts: number): Task | undefined { @@ -29,6 +25,11 @@ export class TaskQueue { return readyItems[0]; } + findById(taskId: TaskId): Task | undefined { + const [matched, _] = this.split(t => t.id === taskId); + return matched.shift(); + } + has(predicate: (t: Task) => boolean): boolean { const [matched, _] = this.split(predicate); return matched.length > 0; @@ -53,11 +54,6 @@ export class TaskQueue { return this.getItems(); } - private shiftTask(id: TaskId): [Task | undefined, TaskList] { - const [a, b] = this.split(t => t.id === id); - return [a.shift(), b]; - } - private split(predicate: (t: Task) => boolean): [TaskList, TaskList] { const matched: TaskList = []; const other: TaskList = []; diff --git a/src/Scheduler.ts b/src/Scheduler.ts index 4192057..efb977b 100644 --- a/src/Scheduler.ts +++ b/src/Scheduler.ts @@ -1,5 +1,4 @@ import { timestamp } from './utils'; -import { UpgradeBuildingTask } from './Task/UpgradeBuildingTask'; import { TaskQueue } from './Queue/TaskQueue'; import { SendOnAdventureTask } from './Task/SendOnAdventureTask'; import { BalanceHeroResourcesTask } from './Task/BalanceHeroResourcesTask'; @@ -7,49 +6,49 @@ import { Logger } from './Logger'; import { GrabVillageState } from './Task/GrabVillageState'; import { Action, ActionQueue, ImmutableActionList } from './Queue/ActionQueue'; import { UpdateResourceContracts } from './Task/UpdateResourceContracts'; -import { Resources, ResourcesInterface } from './Core/Resources'; import { SendResourcesTask } from './Task/SendResourcesTask'; import { Args } from './Queue/Args'; -import { ImmutableTaskList, Task, TaskId } from './Queue/TaskProvider'; -import { ForgeImprovementTask } from './Task/ForgeImprovementTask'; -import { getProductionQueue } from './Task/TaskMap'; +import { ImmutableTaskList, Task, TaskId, uniqTaskId, withTime } from './Queue/TaskProvider'; import { MARKET_ID } from './Core/Buildings'; import { VillageRepositoryInterface } from './VillageRepository'; -import { ProductionQueue, ProductionQueueTypes } from './Core/ProductionQueue'; +import { isProductionTask } from './Core/ProductionQueue'; +import { VillageControllerFactory } from './VillageControllerFactory'; +import { RunVillageProductionTask } from './Task/RunVillageProductionTask'; -export enum ContractType { - UpgradeBuilding, - ImproveTrooper, -} - -interface ContractAttributes { - type: ContractType; - villageId?: number; - buildId?: number; - unitId?: number; +export interface NextExecution { + task?: Task; + action?: Action; } export class Scheduler { private taskQueue: TaskQueue; private actionQueue: ActionQueue; private villageRepository: VillageRepositoryInterface; + private villageControllerFactory: VillageControllerFactory; private logger: Logger; constructor( taskQueue: TaskQueue, actionQueue: ActionQueue, villageRepository: VillageRepositoryInterface, + villageControllerFactory: VillageControllerFactory, logger: Logger ) { this.taskQueue = taskQueue; this.actionQueue = actionQueue; this.villageRepository = villageRepository; + this.villageControllerFactory = villageControllerFactory; this.logger = logger; // this.taskQueue.push(GrabVillageState.name, {}, timestamp()); // this.taskQueue.push(UpdateResourceContracts.name, {}, timestamp()); // this.taskQueue.push(BalanceHeroResourcesTask.name, {}, timestamp()); + const villages = this.villageRepository.all(); + for (let village of villages) { + this.createUniqTaskTimer(5 * 60, RunVillageProductionTask.name, { villageId: village.id }); + } + this.createUniqTaskTimer(5 * 60, GrabVillageState.name); this.createUniqTaskTimer(10 * 60, BalanceHeroResourcesTask.name); this.createUniqTaskTimer(20 * 60, UpdateResourceContracts.name); @@ -69,25 +68,67 @@ export class Scheduler { return this.actionQueue.seeItems(); } - nextTask(ts: number) { - return this.taskQueue.get(ts); + nextTask(ts: number): NextExecution { + const task = this.taskQueue.get(ts); + + // Task not found - next task not ready or queue is empty + if (!task) { + this.clearActions(); + return {}; + } + + const action = this.actionQueue.pop(); + + // Action not found - task is new + if (!action) { + return { task: this.replaceTask(task) }; + } + + // Task in action not equals current task it - rerun task + if (action.args.taskId !== task.id) { + this.clearActions(); + return { task: this.replaceTask(task) }; + } + + return { task, action }; } - nextAction() { - return this.actionQueue.pop(); + private replaceTask(task: Task): Task | undefined { + if (task.name === RunVillageProductionTask.name && task.args.villageId) { + const villageId = task.args.villageId; + const controller = this.villageControllerFactory.create(villageId); + const villageTask = controller.getReadyProductionTask(); + if (villageTask) { + this.removeTask(task.id); + const newTask = new Task(villageTask.id, 0, villageTask.name, { + ...villageTask.args, + villageId: controller.villageId, + }); + this.taskQueue.add(newTask); + return newTask; + } + } + return task; } scheduleTask(name: string, args: Args, ts?: number | undefined): void { - this.logger.info('PUSH TASK', name, args, ts); - let insertedTs = calculateInsertTime(this.taskQueue.seeItems(), name, args, ts); - this.taskQueue.push(name, args, insertedTs); - if (args.villageId) { - this.reorderVillageTasks(args.villageId); + if (isProductionTask(name) && args.villageId) { + const controller = this.villageControllerFactory.create(args.villageId); + controller.addTask(name, args); + } else { + this.logger.info('Schedule task', name, args, ts); + this.taskQueue.add(new Task(uniqTaskId(), ts || timestamp(), name, args)); } } scheduleUniqTask(name: string, args: Args, ts?: number | undefined): void { - let alreadyHasTask = this.taskQueue.has(t => t.name === name); + let alreadyHasTask; + if (args.villageId) { + alreadyHasTask = this.taskQueue.has(t => t.name === name && t.args.villageId === args.villageId); + } else { + alreadyHasTask = this.taskQueue.has(t => t.name === name); + } + if (!alreadyHasTask) { this.scheduleTask(name, args, ts); } @@ -98,55 +139,30 @@ export class Scheduler { this.actionQueue.clear(); } + completeTask(taskId: TaskId) { + const task = this.taskQueue.findById(taskId); + const villageId = task ? task.args.villageId : undefined; + if (villageId) { + const controller = this.villageControllerFactory.create(villageId); + controller.removeTask(taskId); + } + this.removeTask(taskId); + } + postponeTask(taskId: TaskId, seconds: number) { const task = this.taskQueue.seeItems().find(t => t.id === taskId); if (!task) { return; } - const villageId = task.args.villageId; - const modifyTime = (t: Task) => withTime(t, timestamp() + seconds); - - let predicateUsed = false; - - for (let taskTypePred of TASK_TYPE_PREDICATES) { - if (taskTypePred(task.name) && villageId) { - this.taskQueue.modify(t => sameVillage(villageId, t.args) && taskTypePred(t.name), modifyTime); - predicateUsed = true; - } - } - - if (!predicateUsed) { + if (isProductionTask(task.name) && task.args.villageId) { + const controller = this.villageControllerFactory.create(task.args.villageId); + controller.postponeTask(taskId, seconds); + this.removeTask(taskId); + } else { + const modifyTime = withTime(timestamp() + seconds); this.taskQueue.modify(t => t.id === taskId, modifyTime); } - - if (villageId) { - this.reorderVillageTasks(villageId); - } - } - - updateResources(resources: Resources, attr: ContractAttributes): void { - if (attr.type === ContractType.UpgradeBuilding && attr.villageId && attr.buildId) { - const count = this.taskQueue.modify( - t => - t.name === UpgradeBuildingTask.name && - t.args.villageId === attr.villageId && - t.args.buildId === attr.buildId, - t => withResources(t, resources) - ); - this.logger.info('Update', count, 'upgrade contracts', attr, resources); - } - if (attr.type === ContractType.ImproveTrooper && attr.villageId && attr.buildId && attr.unitId) { - const count = this.taskQueue.modify( - t => - t.name === ForgeImprovementTask.name && - t.args.villageId === attr.villageId && - t.args.buildId === attr.buildId && - t.args.unitId === attr.unitId, - t => withResources(t, resources) - ); - this.logger.info('Update', count, 'improve contracts', attr, resources); - } } scheduleActions(actions: Array): void { @@ -157,31 +173,6 @@ export class Scheduler { this.actionQueue.clear(); } - getVillageRequiredResources(villageId: number): Resources { - const tasks = this.taskQueue - .seeItems() - .filter(t => sameVillage(villageId, t.args) && t.args.resources && t.name !== SendResourcesTask.name); - const first = tasks.shift(); - if (first && first.args.resources) { - return Resources.fromObject(first.args.resources); - } - return Resources.zero(); - } - - getTotalVillageRequiredResources(villageId: number): Resources { - const tasks = this.taskQueue - .seeItems() - .filter(t => sameVillage(villageId, t.args) && t.args.resources && t.name !== SendResourcesTask.name); - return tasks.reduce((acc, t) => acc.add(t.args.resources!), Resources.zero()); - } - - getResourceShipmentVillageIds(villageId: number): Array { - const tasks = this.taskQueue - .seeItems() - .filter(t => t.name === SendResourcesTask.name && t.args.villageId === villageId && t.args.targetVillageId); - return tasks.map(t => t.args.targetVillageId!); - } - dropResourceTransferTasks(fromVillageId: number, toVillageId: number): void { this.taskQueue.remove( t => @@ -205,92 +196,4 @@ export class Scheduler { tabId: 5, }); } - - getProductionQueueTasks(villageId: number, queue: ProductionQueue): ReadonlyArray { - return this.taskQueue - .seeItems() - .filter(t => t.args.villageId === villageId && getProductionQueue(t.name) === queue); - } - - private reorderVillageTasks(villageId: number) { - const tasks = this.taskQueue.seeItems(); - - for (let i = 0; i < TASK_TYPE_PREDICATES.length; ++i) { - const taskTypePred = TASK_TYPE_PREDICATES[i]; - const lowTaskTypePredicates = TASK_TYPE_PREDICATES.slice(i + 1); - const lastTaskTs = lastTaskTime(tasks, t => taskTypePred(t.name) && sameVillage(villageId, t.args)); - if (lastTaskTs) { - for (let pred of lowTaskTypePredicates) { - this.taskQueue.modify( - t => pred(t.name) && sameVillage(villageId, t.args), - t => withTime(t, lastTaskTs + 1) - ); - } - } - } - } -} - -interface TaskNamePredicate { - (name: string): boolean; -} - -/** - * List on non intersected task queue predicates. - */ -const TASK_TYPE_PREDICATES: Array = ProductionQueueTypes.map(queue => { - return (taskName: string) => getProductionQueue(taskName) === queue; -}); - -function sameVillage(villageId: number | undefined, args: Args) { - return villageId !== undefined && args.villageId === villageId; -} - -function withTime(task: Task, ts: number): Task { - return new Task(task.id, ts, task.name, task.args); -} - -function withResources(task: Task, resources: ResourcesInterface): Task { - return new Task(task.id, task.ts, task.name, { ...task.args, resources }); -} - -function lastTaskTime(tasks: ImmutableTaskList, predicate: (t: Task) => boolean): number | undefined { - const queuedTaskIndex = findLastIndex(tasks, predicate); - if (queuedTaskIndex === undefined) { - return undefined; - } - return tasks[queuedTaskIndex].ts; -} - -function findLastIndex(tasks: ImmutableTaskList, 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; -} - -/** - * Calculates insert time for new task based on task queue. - */ -function calculateInsertTime(tasks: ImmutableTaskList, name: string, args: Args, ts: number | undefined): number { - const villageId = args.villageId; - let insertedTs = ts; - - if (villageId && !insertedTs) { - for (let taskTypePred of TASK_TYPE_PREDICATES) { - const sameVillageAndTypePred = (t: Task) => - taskTypePred(name) && taskTypePred(t.name) && sameVillage(villageId, t.args); - insertedTs = lastTaskTime(tasks, sameVillageAndTypePred); - if (insertedTs) { - insertedTs += 1; - } - } - } - - return insertedTs || timestamp(); } diff --git a/src/Storage/VillageStorage.ts b/src/Storage/VillageStorage.ts index 13f494a..0a9e4ed 100644 --- a/src/Storage/VillageStorage.ts +++ b/src/Storage/VillageStorage.ts @@ -6,14 +6,16 @@ import { IncomingMerchant } from '../Core/Market'; import { VillageSettings, VillageSettingsDefaults } from '../Core/Village'; import { ProductionQueue } from '../Core/ProductionQueue'; import { getNumber } from '../utils'; +import { Task, TaskList, uniqTaskId } from '../Queue/TaskProvider'; -const RESOURCES_KEY = 'res'; -const CAPACITY_KEY = 'cap'; -const PERFORMANCE_KEY = 'perf'; -const BUILDING_QUEUE_KEY = 'bq'; -const INCOMING_MERCHANTS_KEY = 'im'; +const RESOURCES_KEY = 'resources'; +const CAPACITY_KEY = 'capacity'; +const PERFORMANCE_KEY = 'performance'; +const BUILDING_QUEUE_INFO_KEY = 'building_queue_info'; +const INCOMING_MERCHANTS_KEY = 'incoming_merchants'; const SETTINGS_KEY = 'settings'; -const QUEUE_ENDING_TIME_KEY = 'qet'; +const QUEUE_ENDING_TIME_KEY = 'queue_ending_time'; +const TASK_LIST_KEY = 'tasks'; const ResourceOptions = { factory: () => new Resources(0, 0, 0, 0), @@ -52,11 +54,11 @@ export class VillageStorage { } storeBuildingQueueInfo(info: BuildingQueueInfo): void { - this.storage.set(BUILDING_QUEUE_KEY, info); + this.storage.set(BUILDING_QUEUE_INFO_KEY, info); } getBuildingQueueInfo(): BuildingQueueInfo { - let plain = this.storage.get(BUILDING_QUEUE_KEY); + let plain = this.storage.get(BUILDING_QUEUE_INFO_KEY); let res = new BuildingQueueInfo(0); return Object.assign(res, plain) as BuildingQueueInfo; } @@ -103,4 +105,48 @@ export class VillageStorage { const key = this.queueKey(queue); this.storage.set(key, ts); } + + getTasks(): Array { + return this.storage.getTypedList(TASK_LIST_KEY, { + factory: () => new Task(uniqTaskId(), 0, '', {}), + }); + } + + addTask(task: Task): void { + const tasks = this.getTasks(); + tasks.push(task); + this.storeTaskList(tasks); + } + + modifyTasks(predicate: (t: Task) => boolean, modifier: (t: Task) => Task): number { + const [matched, other] = this.split(predicate); + const modified = matched.map(modifier); + const modifiedCount = modified.length; + this.storeTaskList(modified.concat(other)); + return modifiedCount; + } + + removeTasks(predicate: (t: Task) => boolean): number { + const [_, other] = this.split(predicate); + const result = other.length; + this.storeTaskList(other); + return result; + } + + private split(predicate: (t: Task) => boolean): [TaskList, TaskList] { + const matched: TaskList = []; + const other: TaskList = []; + this.getTasks().forEach(t => { + if (predicate(t)) { + matched.push(t); + } else { + other.push(t); + } + }); + return [matched, other]; + } + + private storeTaskList(tasks: Array): void { + this.storage.set(TASK_LIST_KEY, tasks); + } } diff --git a/src/Task/RunVillageProductionTask.ts b/src/Task/RunVillageProductionTask.ts new file mode 100644 index 0000000..f548b10 --- /dev/null +++ b/src/Task/RunVillageProductionTask.ts @@ -0,0 +1,11 @@ +import { TaskController, ActionDefinition } from './TaskController'; +import { scanAllVillagesBundle } from './ActionBundles'; +import { Task } from '../Queue/TaskProvider'; +import { registerTask } from './TaskMap'; + +@registerTask() +export class RunVillageProductionTask extends TaskController { + defineActions(task: Task): Array { + return []; + } +} diff --git a/src/Task/UpdateResourceContracts.ts b/src/Task/UpdateResourceContracts.ts index 24c838a..5185f17 100644 --- a/src/Task/UpdateResourceContracts.ts +++ b/src/Task/UpdateResourceContracts.ts @@ -1,4 +1,4 @@ -import { TaskController, ActionDefinition } from './TaskController'; +import { ActionDefinition, TaskController } from './TaskController'; import { GoToPageAction } from '../Action/GoToPageAction'; import { UpgradeBuildingTask } from './UpgradeBuildingTask'; import { ImmutableTaskList, Task } from '../Queue/TaskProvider'; @@ -11,10 +11,9 @@ export class UpdateResourceContracts extends TaskController { defineActions(task: Task): Array { const tasks = this.scheduler.getTaskItems(); - const paths = [...this.walkUpgradeTasks(tasks), ...this.walkImprovementTask(tasks)]; - const uniq = uniqPaths(paths); + const paths = uniqPaths([...this.walkUpgradeTasks(tasks), ...this.walkImprovementTask(tasks)]); - return uniq.map(p => [GoToPageAction.name, { path: path(p.name, p.query) }]); + return paths.map(p => [GoToPageAction.name, { path: path(p.name, p.query) }]); } private walkUpgradeTasks(tasks: ImmutableTaskList): PathList { diff --git a/src/VillageController.ts b/src/VillageController.ts new file mode 100644 index 0000000..feab091 --- /dev/null +++ b/src/VillageController.ts @@ -0,0 +1,93 @@ +import { Task, TaskId, uniqTaskId, withResources, withTime } from './Queue/TaskProvider'; +import { VillageStorage } from './Storage/VillageStorage'; +import { Args } from './Queue/Args'; +import { isProductionTask, ProductionQueue, ProductionQueueTypes } from './Core/ProductionQueue'; +import { Resources } from './Core/Resources'; +import { UpgradeBuildingTask } from './Task/UpgradeBuildingTask'; +import { ForgeImprovementTask } from './Task/ForgeImprovementTask'; +import { ContractType, ContractAttributes } from './Core/Contract'; +import { timestamp } from './utils'; +import { getProductionQueue } from './Task/TaskMap'; + +export class VillageController { + private readonly _villageId: number; + private readonly _storage: VillageStorage; + + constructor(villageId: number, storage: VillageStorage) { + this._villageId = villageId; + this._storage = storage; + } + + get villageId() { + return this._villageId; + } + + getStorage(): VillageStorage { + return this._storage; + } + + addTask(name: string, args: Args) { + if (!isProductionTask(name)) { + throw new Error(`Task "${name}" is not production task`); + } + if (args.villageId !== this._villageId) { + throw new Error(`Task village id (${args.villageId}) not equal controller village id (${this._villageId}`); + } + const task = new Task(uniqTaskId(), 0, name, { villageId: this._villageId, ...args }); + this._storage.addTask(task); + } + + getTasks(): Array { + return this._storage.getTasks(); + } + + removeTask(taskId: TaskId) { + this._storage.removeTasks(t => t.id === taskId); + } + + getTasksInProductionQueue(queue: ProductionQueue): Array { + return this._storage.getTasks().filter(task => getProductionQueue(task.name) === queue); + } + + getReadyProductionTask(): Task | undefined { + let sortedTasks: Array = []; + for (let queue of ProductionQueueTypes) { + const tasks = this.getTasksInProductionQueue(queue); + sortedTasks = sortedTasks.concat(tasks); + } + return sortedTasks.shift(); + } + + postponeTask(taskId: TaskId, seconds: number) { + const modifyTime = withTime(timestamp() + seconds); + this._storage.modifyTasks(task => task.id === taskId, modifyTime); + } + + updateResources(resources: Resources, attr: ContractAttributes): void { + if (attr.type === ContractType.UpgradeBuilding && attr.buildId) { + const predicate = (t: Task) => t.name === UpgradeBuildingTask.name && t.args.buildId === attr.buildId; + this._storage.modifyTasks(predicate, withResources(resources)); + } + if (attr.type === ContractType.ImproveTrooper && attr.buildId && attr.unitId) { + const predicate = (t: Task) => + t.name === ForgeImprovementTask.name && + t.args.buildId === attr.buildId && + t.args.unitId === attr.unitId; + this._storage.modifyTasks(predicate, withResources(resources)); + } + } + + getVillageRequiredResources(): Resources { + const tasks = this._storage.getTasks().filter(t => t.args.resources); + const first = tasks.shift(); + if (first && first.args.resources) { + return Resources.fromObject(first.args.resources); + } + return Resources.zero(); + } + + getTotalVillageRequiredResources(): Resources { + const tasks = this._storage.getTasks().filter(t => t.args.resources); + return tasks.reduce((acc, t) => acc.add(t.args.resources!), Resources.zero()); + } +} diff --git a/src/VillageControllerFactory.ts b/src/VillageControllerFactory.ts new file mode 100644 index 0000000..da46493 --- /dev/null +++ b/src/VillageControllerFactory.ts @@ -0,0 +1,21 @@ +import { VillageController } from './VillageController'; +import { VillageStorage } from './Storage/VillageStorage'; +import { VillageRepository } from './VillageRepository'; + +export class VillageControllerFactory { + private villageRepository: VillageRepository; + + constructor(villageRepository: VillageRepository) { + this.villageRepository = villageRepository; + } + + create(villageId: number): VillageController { + const village = this.villageRepository.get(villageId); + return new VillageController(village.id, new VillageStorage(village.id)); + } + + getActive(): VillageController { + const village = this.villageRepository.getActive(); + return this.create(village.id); + } +} diff --git a/src/VillageRepository.ts b/src/VillageRepository.ts index da51258..f124d6c 100644 --- a/src/VillageRepository.ts +++ b/src/VillageRepository.ts @@ -1,12 +1,30 @@ import { Village } from './Core/Village'; import { grabVillageList } from './Page/VillageBlock'; +import { VillageNotFound } from './Errors'; export interface VillageRepositoryInterface { all(): Array; + getActive(): Village; } export class VillageRepository implements VillageRepositoryInterface { all(): Array { return grabVillageList(); } + + get(villageId: number): Village { + const village = this.all().find(vlg => vlg.id === villageId); + if (!village) { + throw new VillageNotFound('Active village not found'); + } + return village; + } + + getActive(): Village { + const village = this.all().find(vlg => vlg.active); + if (!village) { + throw new VillageNotFound('Active village not found'); + } + return village; + } } diff --git a/src/VillageState.ts b/src/VillageState.ts index b9235b1..a67587d 100644 --- a/src/VillageState.ts +++ b/src/VillageState.ts @@ -1,5 +1,4 @@ import { Village, VillageSettings } from './Core/Village'; -import { Scheduler } from './Scheduler'; import { Resources } from './Core/Resources'; import { VillageStorage } from './Storage/VillageStorage'; import { calcGatheringTimings, GatheringTime } from './Core/GatheringTimings'; @@ -8,6 +7,8 @@ import { VillageNotFound } from './Errors'; import { ProductionQueue, ProductionQueueTypes } from './Core/ProductionQueue'; import { Task } from './Queue/TaskProvider'; import { timestamp } from './utils'; +import { VillageControllerFactory } from './VillageControllerFactory'; +import { VillageController } from './VillageController'; interface VillageStorageState { resources: Resources; @@ -132,14 +133,13 @@ function taskResourceReducer(resources: Resources, task: Task) { } function createProductionQueueState( - villageId: number, queue: ProductionQueue, - storage: VillageStorage, - scheduler: Scheduler + controller: VillageController ): VillageProductionQueueState { + const storage = controller.getStorage(); const resources = storage.getResources(); const performance = storage.getResourcesPerformance(); - const tasks = scheduler.getProductionQueueTasks(villageId, queue); + const tasks = controller.getTasksInProductionQueue(queue); const firstTaskResources = tasks.slice(0, 1).reduce(taskResourceReducer, Resources.zero()); const allTaskResources = tasks.reduce(taskResourceReducer, Resources.zero()); @@ -156,33 +156,33 @@ function createProductionQueueState( }; } -function createAllProductionQueueStates(villageId: number, storage: VillageStorage, scheduler: Scheduler) { +function createAllProductionQueueStates(controller: VillageController) { let result: { [queue: string]: VillageProductionQueueState } = {}; for (let queue of ProductionQueueTypes) { - result[queue] = createProductionQueueState(villageId, queue, storage, scheduler); + result[queue] = createProductionQueueState(queue, controller); } return result; } -function calcFrontierResources(villageId: number, scheduler: Scheduler): Resources { +function calcFrontierResources(controller: VillageController): Resources { let result = Resources.zero(); for (let queue of ProductionQueueTypes) { - const tasks = scheduler.getProductionQueueTasks(villageId, queue); + const tasks = controller.getTasksInProductionQueue(queue); const firstTaskResources = tasks.slice(0, 1).reduce(taskResourceReducer, Resources.zero()); result = result.add(firstTaskResources); } return result; } -function createVillageOwnState(village: Village, scheduler: Scheduler): VillageOwnState { - const storage = new VillageStorage(village.id); +function createVillageOwnState(village: Village, controller: VillageController): VillageOwnState { + const storage = controller.getStorage(); const resources = storage.getResources(); const resourceStorage = storage.getResourceStorage(); const performance = storage.getResourcesPerformance(); const buildQueueInfo = storage.getBuildingQueueInfo(); - const requiredResources = scheduler.getVillageRequiredResources(village.id); - const frontierResources = calcFrontierResources(village.id, scheduler); - const totalRequiredResources = scheduler.getTotalVillageRequiredResources(village.id); + const requiredResources = controller.getVillageRequiredResources(); + const frontierResources = calcFrontierResources(controller); + const totalRequiredResources = controller.getTotalVillageRequiredResources(); return { id: village.id, @@ -196,24 +196,24 @@ function createVillageOwnState(village: Village, scheduler: Scheduler): VillageO buildRemainingSeconds: buildQueueInfo.seconds, incomingResources: calcIncomingResources(storage), settings: storage.getSettings(), - queues: createAllProductionQueueStates(village.id, storage, scheduler), + queues: createAllProductionQueueStates(controller), }; } -function createVillageOwnStates(villages: Array, scheduler: Scheduler): VillageOwnStateDictionary { +function createVillageOwnStates( + villages: Array, + villageControllerFactory: VillageControllerFactory +): VillageOwnStateDictionary { const result: VillageOwnStateDictionary = {}; for (let village of villages) { - result[village.id] = createVillageOwnState(village, scheduler); + const villageController = villageControllerFactory.create(village.id); + result[village.id] = createVillageOwnState(village, villageController); } return result; } -function createVillageState( - state: VillageOwnState, - ownStates: VillageOwnStateDictionary, - scheduler: Scheduler -): VillageState { - const villageIds = scheduler.getResourceShipmentVillageIds(state.id); +function createVillageState(state: VillageOwnState, ownStates: VillageOwnStateDictionary): VillageState { + const villageIds = Object.keys(ownStates).map(k => +k); const commitments = villageIds.reduce((memo, shipmentVillageId) => { const shipmentVillageState = ownStates[shipmentVillageId]; const shipmentVillageRequired = shipmentVillageState.required; @@ -224,26 +224,29 @@ function createVillageState( return { ...state, commitments, shipment: villageIds }; } -function getVillageStates(villages: Array, scheduler: Scheduler): Array { - const ownStates = createVillageOwnStates(villages, scheduler); - return villages.map(village => createVillageState(ownStates[village.id], ownStates, scheduler)); +function getVillageStates( + villages: Array, + villageControllerFactory: VillageControllerFactory +): Array { + const ownStates = createVillageOwnStates(villages, villageControllerFactory); + return villages.map(village => createVillageState(ownStates[village.id], ownStates)); } export class VillageStateRepository { private villageRepository: VillageRepositoryInterface; - private scheduler: Scheduler; + private villageControllerFactory: VillageControllerFactory; - constructor(villageRepository: VillageRepositoryInterface, scheduler: Scheduler) { + constructor(villageRepository: VillageRepositoryInterface, villageControllerFactory: VillageControllerFactory) { this.villageRepository = villageRepository; - this.scheduler = scheduler; + this.villageControllerFactory = villageControllerFactory; } getAllVillageStates(): Array { - return getVillageStates(this.villageRepository.all(), this.scheduler); + return getVillageStates(this.villageRepository.all(), this.villageControllerFactory); } getVillageState(villageId: number): VillageState { - const states = getVillageStates(this.villageRepository.all(), this.scheduler); + const states = getVillageStates(this.villageRepository.all(), this.villageControllerFactory); const needle = states.find(s => s.id === villageId); if (!needle) { throw new VillageNotFound(`Village ${villageId} not found`);