Rebuild village production queue system

This commit is contained in:
Anton Vakhrushev 2020-05-24 17:23:13 +03:00
parent f3a1e67906
commit 8bea617f5b
26 changed files with 520 additions and 336 deletions

View File

@ -5,6 +5,6 @@ import { Task } from '../Queue/TaskProvider';
@registerAction @registerAction
export class CompleteTaskAction extends ActionController { export class CompleteTaskAction extends ActionController {
async run(args: Args, task: Task): Promise<any> { async run(args: Args, task: Task): Promise<any> {
this.scheduler.removeTask(task.id); this.scheduler.completeTask(task.id);
} }
} }

View File

@ -10,6 +10,8 @@ import { StatisticsStorage } from './Storage/StatisticsStorage';
import { VillageRepository } from './VillageRepository'; import { VillageRepository } from './VillageRepository';
import { VillageStateRepository } from './VillageState'; import { VillageStateRepository } from './VillageState';
import { LogStorage } from './Storage/LogStorage'; import { LogStorage } from './Storage/LogStorage';
import { VillageControllerFactory } from './VillageControllerFactory';
import { GrabberManager } from './Grabber/GrabberManager';
export class Container { export class Container {
private readonly version: string; private readonly version: string;
@ -40,16 +42,35 @@ export class Container {
return this._statistics; 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; private _scheduler: Scheduler | undefined;
get scheduler(): Scheduler { get scheduler(): Scheduler {
this._scheduler = this._scheduler =
this._scheduler || this._scheduler ||
(() => { (() => {
const taskProvider = DataStorageTaskProvider.create(); const taskQueue = new TaskQueue(
const taskQueue = new TaskQueue(taskProvider, new ConsoleLogger(TaskQueue.name)); DataStorageTaskProvider.create('tasks:v1'),
new ConsoleLogger(TaskQueue.name)
);
const actionQueue = new ActionQueue(); 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; return this._scheduler;
} }
@ -60,11 +81,22 @@ export class Container {
this._villageStateRepository = this._villageStateRepository =
this._villageStateRepository || this._villageStateRepository ||
(() => { (() => {
return new VillageStateRepository(this.villageRepository, this.scheduler); return new VillageStateRepository(this.villageRepository, this.villageControllerFactory);
})(); })();
return this._villageStateRepository; 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; private _executor: Executor | undefined;
get executor(): Executor { get executor(): Executor {
@ -75,7 +107,15 @@ export class Container {
new ConsoleLogger(Executor.name), new ConsoleLogger(Executor.name),
new StorageLogger(new LogStorage(), LogLevel.warning), 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; return this._executor;
} }
@ -86,7 +126,12 @@ export class Container {
this._controlPanel = this._controlPanel =
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; return this._controlPanel;
} }

View File

@ -22,6 +22,7 @@ import { VillageState, VillageStateRepository } from './VillageState';
import { Task } from './Queue/TaskProvider'; import { Task } from './Queue/TaskProvider';
import { Action } from './Queue/ActionQueue'; import { Action } from './Queue/ActionQueue';
import { createStore } from './DashboardView/Store'; import { createStore } from './DashboardView/Store';
import { VillageControllerFactory } from './VillageControllerFactory';
Vue.use(Vuex); Vue.use(Vuex);
@ -53,11 +54,18 @@ export class ControlPanel {
private readonly scheduler: Scheduler; private readonly scheduler: Scheduler;
private readonly villageStateRepository: VillageStateRepository; private readonly villageStateRepository: VillageStateRepository;
private readonly logger: Logger; 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.version = version;
this.scheduler = scheduler; this.scheduler = scheduler;
this.villageStateRepository = villageStateRepository; this.villageStateRepository = villageStateRepository;
this.villageControllerFactory = villageControllerFactory;
this.logger = new ConsoleLogger(this.constructor.name); this.logger = new ConsoleLogger(this.constructor.name);
} }
@ -128,9 +136,10 @@ export class ControlPanel {
DataStorage.onChange(() => state.refresh()); DataStorage.onChange(() => state.refresh());
const getBuildingsInQueue = () => const getBuildingsInQueue = () =>
this.scheduler this.villageControllerFactory
.getTaskItems() .create(villageId)
.filter(t => t.name === UpgradeBuildingTask.name && t.args.villageId === villageId) .getTasks()
.filter(t => t.name === UpgradeBuildingTask.name)
.map(t => t.args.buildId || 0); .map(t => t.args.buildId || 0);
if (p.pathname === '/dorf1.php') { if (p.pathname === '/dorf1.php') {
@ -151,7 +160,11 @@ export class ControlPanel {
} }
if (isBuildingPage()) { if (isBuildingPage()) {
const buildPage = new BuildingPageController(this.scheduler, getBuildingPageAttributes()); const buildPage = new BuildingPageController(
this.scheduler,
getBuildingPageAttributes(),
this.villageControllerFactory.create(villageId)
);
buildPage.run(); buildPage.run();
} }

10
src/Core/Contract.ts Normal file
View File

@ -0,0 +1,10 @@
export enum ContractType {
UpgradeBuilding,
ImproveTrooper,
}
export interface ContractAttributes {
type: ContractType;
buildId?: number;
unitId?: number;
}

View File

@ -1,3 +1,5 @@
import { getProductionQueue } from '../Task/TaskMap';
export enum ProductionQueue { export enum ProductionQueue {
Building = 'building', Building = 'building',
TrainUnit = 'train_unit', TrainUnit = 'train_unit',
@ -27,3 +29,18 @@ export function translateProductionQueue(queue: ProductionQueue): string {
return 'Празднование'; return 'Празднование';
} }
} }
export interface TaskNamePredicate {
(name: string): boolean;
}
/**
* List on non intersected task queue predicates.
*/
export const TASK_TYPE_PREDICATES: Array<TaskNamePredicate> = 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);
}

View File

@ -11,6 +11,7 @@ import { Action } from './Queue/ActionQueue';
import { Task } from './Queue/TaskProvider'; import { Task } from './Queue/TaskProvider';
import { createTaskHandler } from './Task/TaskMap'; import { createTaskHandler } from './Task/TaskMap';
import { VillageStateRepository } from './VillageState'; import { VillageStateRepository } from './VillageState';
import { VillageControllerFactory } from './VillageControllerFactory';
export interface ExecutionSettings { export interface ExecutionSettings {
pauseTs: number; pauseTs: number;
@ -20,7 +21,8 @@ export class Executor {
private readonly version: string; private readonly version: string;
private readonly scheduler: Scheduler; private readonly scheduler: Scheduler;
private readonly villageStateRepository: VillageStateRepository; private readonly villageStateRepository: VillageStateRepository;
private grabbers: GrabberManager; private villageControllerFactory: VillageControllerFactory;
private grabberManager: GrabberManager;
private statistics: Statistics; private statistics: Statistics;
private executionState: ExecutionStorage; private executionState: ExecutionStorage;
private logger: Logger; private logger: Logger;
@ -29,13 +31,16 @@ export class Executor {
version: string, version: string,
scheduler: Scheduler, scheduler: Scheduler,
villageStateRepository: VillageStateRepository, villageStateRepository: VillageStateRepository,
villageControllerFactory: VillageControllerFactory,
grabberManager: GrabberManager,
statistics: Statistics, statistics: Statistics,
logger: Logger logger: Logger
) { ) {
this.version = version; this.version = version;
this.scheduler = scheduler; this.scheduler = scheduler;
this.villageStateRepository = villageStateRepository; this.villageStateRepository = villageStateRepository;
this.grabbers = new GrabberManager(scheduler); this.villageControllerFactory = villageControllerFactory;
this.grabberManager = grabberManager;
this.statistics = statistics; this.statistics = statistics;
this.executionState = new ExecutionStorage(); this.executionState = new ExecutionStorage();
this.logger = logger; this.logger = logger;
@ -49,6 +54,7 @@ export class Executor {
const sleep = createExecutionLoopSleeper(); const sleep = createExecutionLoopSleeper();
// noinspection InfiniteLoopJS
while (true) { while (true) {
await sleep(); await sleep();
if (!this.isPaused()) { if (!this.isPaused()) {
@ -76,25 +82,22 @@ export class Executor {
} }
private async doTaskProcessingStep() { private async doTaskProcessingStep() {
this.runGrabbers();
const currentTs = timestamp(); const currentTs = timestamp();
const task = this.scheduler.nextTask(currentTs); const { task, action } = this.scheduler.nextTask(currentTs);
// текущего таска нет, очищаем очередь действий по таску // текущего таска нет, очищаем очередь действий по таску
if (!task) { if (!task) {
this.logger.info('NO ACTIVE TASK'); this.logger.info('NO ACTIVE TASK');
this.scheduler.clearActions();
return; return;
} }
const actionCommand = this.scheduler.nextAction(); this.logger.info('CURRENT JOB', 'TASK', task, 'ACTION', action);
this.logger.info('CURRENT JOB', 'TASK', task, 'ACTION', actionCommand);
this.runGrabbers();
try { try {
if (actionCommand) { if (task && action) {
return await this.processActionCommand(actionCommand, task); return await this.processActionCommand(action, task);
} }
if (task) { if (task) {
@ -105,27 +108,24 @@ export class Executor {
} }
} }
private async processActionCommand(cmd: Action, task: Task) { private async processActionCommand(action: Action, task: Task) {
const actionHandler = createActionHandler(cmd.name, this.scheduler, this.villageStateRepository); const actionHandler = createActionHandler(action.name, this.scheduler, this.villageStateRepository);
this.logger.info('PROCESS ACTION', cmd.name, actionHandler); this.logger.info('Process action', action.name, actionHandler);
if (cmd.args.taskId !== task.id) {
throw new ActionError(`Action task id ${cmd.args.taskId} not equal current task id ${task.id}`);
}
if (actionHandler) { if (actionHandler) {
this.statistics.incrementAction(timestamp()); this.statistics.incrementAction(timestamp());
await actionHandler.run(cmd.args, task); await actionHandler.run(action.args, task);
} else { } else {
this.logger.warn('ACTION NOT FOUND', cmd.name); this.logger.error('Action not found', action.name);
} }
} }
private async processTaskCommand(task: Task) { private async processTaskCommand(task: Task) {
const taskHandler = createTaskHandler(task.name, this.scheduler); 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) { if (taskHandler) {
await taskHandler.run(task); await taskHandler.run(task);
} else { } else {
this.logger.warn('TASK NOT FOUND', task.name); this.logger.error('Task handler not created', task.name);
this.scheduler.removeTask(task.id); this.scheduler.removeTask(task.id);
} }
} }
@ -174,7 +174,7 @@ export class Executor {
private runGrabbers() { private runGrabbers() {
try { try {
this.logger.info('Rug grabbers'); this.logger.info('Rug grabbers');
this.grabbers.grab(); this.grabberManager.grab();
} catch (e) { } catch (e) {
this.logger.warn('Grabbers fails with', e.message); this.logger.warn('Grabbers fails with', e.message);
} }

View File

@ -1,8 +1,7 @@
import { Grabber } from './Grabber'; import { Grabber } from './Grabber';
import { grabActiveVillageId } from '../Page/VillageBlock';
import { getBuildingPageAttributes, isBuildingPage } from '../Page/PageDetectors'; import { getBuildingPageAttributes, isBuildingPage } from '../Page/PageDetectors';
import { grabContractResources, hasContractResources } from '../Page/BuildingPage/BuildingPage'; import { grabContractResources, hasContractResources } from '../Page/BuildingPage/BuildingPage';
import { ContractType } from '../Scheduler'; import { ContractType } from '../Core/Contract';
export class BuildingContractGrabber extends Grabber { export class BuildingContractGrabber extends Grabber {
grab(): void { grab(): void {
@ -19,12 +18,10 @@ export class BuildingContractGrabber extends Grabber {
return; return;
} }
const villageId = grabActiveVillageId();
const contract = grabContractResources(); const contract = grabContractResources();
this.scheduler.updateResources(contract, { this.controller.updateResources(contract, {
type: ContractType.UpgradeBuilding, type: ContractType.UpgradeBuilding,
villageId,
buildId: building.buildId, buildId: building.buildId,
}); });
} }

View File

@ -1,9 +1,7 @@
import { Grabber } from './Grabber'; import { Grabber } from './Grabber';
import { grabActiveVillageId } from '../Page/VillageBlock';
import { getBuildingPageAttributes, isForgePage } from '../Page/PageDetectors'; import { getBuildingPageAttributes, isForgePage } from '../Page/PageDetectors';
import { ContractType } from '../Scheduler'; import { ContractType } from '../Core/Contract';
import { grabImprovementContracts, grabRemainingSeconds } from '../Page/BuildingPage/ForgePage'; import { grabImprovementContracts, grabRemainingSeconds } from '../Page/BuildingPage/ForgePage';
import { VillageStorage } from '../Storage/VillageStorage';
import { ProductionQueue } from '../Core/ProductionQueue'; import { ProductionQueue } from '../Core/ProductionQueue';
import { timestamp } from '../utils'; import { timestamp } from '../utils';
@ -13,29 +11,26 @@ export class ForgePageGrabber extends Grabber {
return; return;
} }
const villageId = grabActiveVillageId(); this.grabContracts();
this.grabTimer();
this.grabContracts(villageId);
this.grabTimer(villageId);
} }
private grabContracts(villageId: number): void { private grabContracts(): void {
const { buildId } = getBuildingPageAttributes(); const { buildId } = getBuildingPageAttributes();
const contracts = grabImprovementContracts(); const contracts = grabImprovementContracts();
for (let { resources, unitId } of contracts) { for (let { resources, unitId } of contracts) {
this.scheduler.updateResources(resources, { this.controller.updateResources(resources, {
type: ContractType.ImproveTrooper, type: ContractType.ImproveTrooper,
villageId,
buildId, buildId,
unitId, unitId,
}); });
} }
} }
private grabTimer(villageId: number): void { private grabTimer(): void {
const state = new VillageStorage(villageId); const storage = this.controller.getStorage();
const seconds = grabRemainingSeconds(); const seconds = grabRemainingSeconds();
state.storeQueueTaskEnding(ProductionQueue.UpgradeUnit, seconds ? seconds + timestamp() : 0); storage.storeQueueTaskEnding(ProductionQueue.UpgradeUnit, seconds ? seconds + timestamp() : 0);
} }
} }

View File

@ -1,10 +1,10 @@
import { Scheduler } from '../Scheduler'; import { VillageController } from '../VillageController';
export abstract class Grabber { export abstract class Grabber {
protected scheduler: Scheduler; protected controller: VillageController;
constructor(scheduler: Scheduler) { constructor(controller: VillageController) {
this.scheduler = scheduler; this.controller = controller;
} }
abstract grab(): void; abstract grab(): void;

View File

@ -3,28 +3,35 @@ import { VillageResourceGrabber } from './VillageResourceGrabber';
import { VillageOverviewPageGrabber } from './VillageOverviewPageGrabber'; import { VillageOverviewPageGrabber } from './VillageOverviewPageGrabber';
import { HeroPageGrabber } from './HeroPageGrabber'; import { HeroPageGrabber } from './HeroPageGrabber';
import { MarketPageGrabber } from './MarketPageGrabber'; import { MarketPageGrabber } from './MarketPageGrabber';
import { Scheduler } from '../Scheduler';
import { BuildingContractGrabber } from './BuildingContractGrabber'; import { BuildingContractGrabber } from './BuildingContractGrabber';
import { ForgePageGrabber } from './ForgePageGrabber'; import { ForgePageGrabber } from './ForgePageGrabber';
import { GuildHallPageGrabber } from './GuildHallPageGrabber'; import { GuildHallPageGrabber } from './GuildHallPageGrabber';
import { VillageControllerFactory } from '../VillageControllerFactory';
export class GrabberManager { export class GrabberManager {
private readonly grabbers: Array<Grabber> = []; private factory: VillageControllerFactory;
constructor(scheduler: Scheduler) { constructor(factory: VillageControllerFactory) {
this.grabbers = []; this.factory = factory;
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));
} }
grab() { grab() {
for (let grabber of this.grabbers) { const grabbers = this.createGrabbers();
for (let grabber of grabbers) {
grabber.grab(); grabber.grab();
} }
} }
private createGrabbers(): Array<Grabber> {
const controller = this.factory.getActive();
const grabbers: Array<Grabber> = [];
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;
}
} }

View File

@ -1,6 +1,4 @@
import { Grabber } from './Grabber'; import { Grabber } from './Grabber';
import { grabActiveVillageId } from '../Page/VillageBlock';
import { VillageStorage } from '../Storage/VillageStorage';
import { isGuildHallPage } from '../Page/PageDetectors'; import { isGuildHallPage } from '../Page/PageDetectors';
import { grabRemainingSeconds } from '../Page/BuildingPage/GuildHallPage'; import { grabRemainingSeconds } from '../Page/BuildingPage/GuildHallPage';
import { ProductionQueue } from '../Core/ProductionQueue'; import { ProductionQueue } from '../Core/ProductionQueue';
@ -12,9 +10,8 @@ export class GuildHallPageGrabber extends Grabber {
return; return;
} }
const villageId = grabActiveVillageId();
const state = new VillageStorage(villageId);
const seconds = grabRemainingSeconds(); const seconds = grabRemainingSeconds();
state.storeQueueTaskEnding(ProductionQueue.Celebration, seconds ? seconds + timestamp() : 0); const storage = this.controller.getStorage();
storage.storeQueueTaskEnding(ProductionQueue.Celebration, seconds ? seconds + timestamp() : 0);
} }
} }

View File

@ -1,6 +1,4 @@
import { Grabber } from './Grabber'; import { Grabber } from './Grabber';
import { grabActiveVillageId } from '../Page/VillageBlock';
import { VillageStorage } from '../Storage/VillageStorage';
import { isMarketSendResourcesPage } from '../Page/PageDetectors'; import { isMarketSendResourcesPage } from '../Page/PageDetectors';
import { grabIncomingMerchants } from '../Page/BuildingPage/MarketPage'; import { grabIncomingMerchants } from '../Page/BuildingPage/MarketPage';
@ -10,8 +8,7 @@ export class MarketPageGrabber extends Grabber {
return; return;
} }
const villageId = grabActiveVillageId(); const storage = this.controller.getStorage();
const state = new VillageStorage(villageId); storage.storeIncomingMerchants(grabIncomingMerchants());
state.storeIncomingMerchants(grabIncomingMerchants());
} }
} }

View File

@ -1,6 +1,5 @@
import { Grabber } from './Grabber'; import { Grabber } from './Grabber';
import { grabActiveVillageId, grabBuildingQueueInfo, grabResourcesPerformance } from '../Page/VillageBlock'; import { grabBuildingQueueInfo, grabResourcesPerformance } from '../Page/VillageBlock';
import { VillageStorage } from '../Storage/VillageStorage';
import { parseLocation, timestamp } from '../utils'; import { parseLocation, timestamp } from '../utils';
import { GrabError } from '../Errors'; import { GrabError } from '../Errors';
import { BuildingQueueInfo } from '../Game'; import { BuildingQueueInfo } from '../Game';
@ -13,8 +12,7 @@ export class VillageOverviewPageGrabber extends Grabber {
return; return;
} }
const villageId = grabActiveVillageId(); const storage = this.controller.getStorage();
const storage = new VillageStorage(villageId);
storage.storeResourcesPerformance(grabResourcesPerformance()); storage.storeResourcesPerformance(grabResourcesPerformance());
storage.storeBuildingQueueInfo(this.grabBuildingQueueInfoOrDefault()); storage.storeBuildingQueueInfo(this.grabBuildingQueueInfoOrDefault());

View File

@ -1,13 +1,10 @@
import { Grabber } from './Grabber'; import { Grabber } from './Grabber';
import { grabActiveVillageId } from '../Page/VillageBlock';
import { grabVillageResources, grabVillageResourceStorage } from '../Page/ResourcesBlock'; import { grabVillageResources, grabVillageResourceStorage } from '../Page/ResourcesBlock';
import { VillageStorage } from '../Storage/VillageStorage';
export class VillageResourceGrabber extends Grabber { export class VillageResourceGrabber extends Grabber {
grab(): void { grab(): void {
const villageId = grabActiveVillageId(); const storage = this.controller.getStorage();
const state = new VillageStorage(villageId); storage.storeResources(grabVillageResources());
state.storeResources(grabVillageResources()); storage.storeResourceStorage(grabVillageResourceStorage());
state.storeResourceStorage(grabVillageResourceStorage());
} }
} }

View File

@ -1,4 +1,4 @@
import { notify, split } from '../utils'; import { notify } from '../utils';
import { UpgradeBuildingTask } from '../Task/UpgradeBuildingTask'; import { UpgradeBuildingTask } from '../Task/UpgradeBuildingTask';
import { Scheduler } from '../Scheduler'; import { Scheduler } from '../Scheduler';
import { TrainTroopTask } from '../Task/TrainTroopTask'; import { TrainTroopTask } from '../Task/TrainTroopTask';
@ -17,15 +17,18 @@ import { createResearchButtons } from './BuildingPage/ForgePage';
import { ForgeImprovementTask } from '../Task/ForgeImprovementTask'; import { ForgeImprovementTask } from '../Task/ForgeImprovementTask';
import { createCelebrationButtons } from './BuildingPage/GuildHallPage'; import { createCelebrationButtons } from './BuildingPage/GuildHallPage';
import { CelebrationTask } from '../Task/CelebrationTask'; import { CelebrationTask } from '../Task/CelebrationTask';
import { VillageController } from '../VillageController';
export class BuildingPageController { export class BuildingPageController {
private scheduler: Scheduler; private scheduler: Scheduler;
private readonly attributes: BuildingPageAttributes; private readonly attributes: BuildingPageAttributes;
private villageController: VillageController;
private readonly logger: Logger; private readonly logger: Logger;
constructor(scheduler: Scheduler, attributes: BuildingPageAttributes) { constructor(scheduler: Scheduler, attributes: BuildingPageAttributes, villageController: VillageController) {
this.scheduler = scheduler; this.scheduler = scheduler;
this.attributes = attributes; this.attributes = attributes;
this.villageController = villageController;
this.logger = new ConsoleLogger(this.constructor.name); this.logger = new ConsoleLogger(this.constructor.name);
} }
@ -56,7 +59,7 @@ export class BuildingPageController {
} }
if (isMarketSendResourcesPage()) { if (isMarketSendResourcesPage()) {
createSendResourcesButton((res, crd) => this.onSendResources(res, crd)); createSendResourcesButton((res, crd) => this.onSendResources(crd));
} }
if (isForgePage()) { if (isForgePage()) {
@ -64,7 +67,7 @@ export class BuildingPageController {
} }
if (isGuildHallPage()) { 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 buildId = this.attributes.buildId;
const categoryId = this.attributes.categoryId; const categoryId = this.attributes.categoryId;
const villageId = grabActiveVillageId(); 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`); notify(`Building ${buildId} scheduled`);
} }
private onScheduleUpgradeBuilding(resources: Resources) { private onScheduleUpgradeBuilding(resources: Resources) {
const buildId = this.attributes.buildId; const buildId = this.attributes.buildId;
const villageId = grabActiveVillageId(); const villageId = grabActiveVillageId();
this.scheduler.scheduleTask(UpgradeBuildingTask.name, { villageId, buildId, resources }); this.villageController.addTask(UpgradeBuildingTask.name, { villageId, buildId, resources });
notify(`Upgrading ${buildId} scheduled`); notify(`Upgrading ${buildId} scheduled`);
} }
@ -94,11 +103,11 @@ export class BuildingPageController {
troopResources: resources, troopResources: resources,
resources: resources.scale(trainCount), resources: resources.scale(trainCount),
}; };
this.scheduler.scheduleTask(TrainTroopTask.name, args); this.villageController.addTask(TrainTroopTask.name, args);
notify(`Training ${trainCount} troopers scheduled`); notify(`Training ${trainCount} troopers scheduled`);
} }
private onSendResources(resources: Resources, coordinates: Coordinates) { private onSendResources(coordinates: Coordinates) {
const villageId = grabActiveVillageId(); const villageId = grabActiveVillageId();
const targetVillage = grabVillageList().find(v => v.crd.eq(coordinates)); const targetVillage = grabVillageList().find(v => v.crd.eq(coordinates));
this.scheduler.scheduleTask(SendResourcesTask.name, { this.scheduler.scheduleTask(SendResourcesTask.name, {
@ -114,7 +123,7 @@ export class BuildingPageController {
private onResearch(resources: Resources, unitId: number) { private onResearch(resources: Resources, unitId: number) {
const villageId = grabActiveVillageId(); const villageId = grabActiveVillageId();
this.scheduler.scheduleTask(ForgeImprovementTask.name, { this.villageController.addTask(ForgeImprovementTask.name, {
villageId, villageId,
buildTypeId: this.attributes.buildTypeId, buildTypeId: this.attributes.buildTypeId,
buildId: this.attributes.buildId, buildId: this.attributes.buildId,
@ -124,9 +133,9 @@ export class BuildingPageController {
notify(`Researching ${unitId} scheduled`); notify(`Researching ${unitId} scheduled`);
} }
private onCelebration(resources: Resources, idx: number) { private onCelebration(resources: Resources) {
const villageId = grabActiveVillageId(); const villageId = grabActiveVillageId();
this.scheduler.scheduleTask(CelebrationTask.name, { this.villageController.addTask(CelebrationTask.name, {
villageId, villageId,
buildTypeId: this.attributes.buildTypeId, buildTypeId: this.attributes.buildTypeId,
buildId: this.attributes.buildId, buildId: this.attributes.buildId,

View File

@ -1,7 +1,6 @@
import { DataStorage } from '../DataStorage'; import { DataStorage } from '../DataStorage';
import { Task, TaskList, TaskProvider, uniqTaskId } from './TaskProvider'; import { Task, TaskList, TaskProvider, uniqTaskId } from './TaskProvider';
const NAMESPACE = 'tasks:v1';
const QUEUE_NAME = 'queue'; const QUEUE_NAME = 'queue';
export class DataStorageTaskProvider implements TaskProvider { export class DataStorageTaskProvider implements TaskProvider {
@ -11,8 +10,8 @@ export class DataStorageTaskProvider implements TaskProvider {
this.storage = storage; this.storage = storage;
} }
static create() { static create(namespace: string) {
return new DataStorageTaskProvider(new DataStorage(NAMESPACE)); return new DataStorageTaskProvider(new DataStorage(namespace));
} }
getTasks(): TaskList { getTasks(): TaskList {

View File

@ -1,10 +1,11 @@
import { Args } from './Args'; import { Args } from './Args';
import { uniqId } from '../utils'; import { uniqId } from '../utils';
import { ResourcesInterface } from '../Core/Resources';
export type TaskId = string; export type TaskId = string;
let idSequence = 1; let idSequence = 1;
let lastTimestamp: number | null = null; let lastTimestamp: number | undefined = undefined;
export function uniqTaskId(): TaskId { export function uniqTaskId(): TaskId {
const ts = Math.floor(Date.now() / 1000); const ts = Math.floor(Date.now() / 1000);
@ -38,3 +39,15 @@ export interface TaskProvider {
getTasks(): TaskList; getTasks(): TaskList;
setTasks(tasks: TaskList): void; 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 });
}

View File

@ -11,14 +11,10 @@ export class TaskQueue {
this.logger = logger; this.logger = logger;
} }
push(name: string, args: Args, ts: number): Task { add(task: Task) {
const id = uniqTaskId();
const task = new Task(id, ts, name, args);
this.logger.info('PUSH TASK', id, ts, name, args);
let items = this.getItems(); let items = this.getItems();
items.push(task); items.push(task);
this.flushItems(items); this.flushItems(items);
return task;
} }
get(ts: number): Task | undefined { get(ts: number): Task | undefined {
@ -29,6 +25,11 @@ export class TaskQueue {
return readyItems[0]; 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 { has(predicate: (t: Task) => boolean): boolean {
const [matched, _] = this.split(predicate); const [matched, _] = this.split(predicate);
return matched.length > 0; return matched.length > 0;
@ -53,11 +54,6 @@ export class TaskQueue {
return this.getItems(); 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] { private split(predicate: (t: Task) => boolean): [TaskList, TaskList] {
const matched: TaskList = []; const matched: TaskList = [];
const other: TaskList = []; const other: TaskList = [];

View File

@ -1,5 +1,4 @@
import { timestamp } from './utils'; import { timestamp } from './utils';
import { UpgradeBuildingTask } from './Task/UpgradeBuildingTask';
import { TaskQueue } from './Queue/TaskQueue'; import { TaskQueue } from './Queue/TaskQueue';
import { SendOnAdventureTask } from './Task/SendOnAdventureTask'; import { SendOnAdventureTask } from './Task/SendOnAdventureTask';
import { BalanceHeroResourcesTask } from './Task/BalanceHeroResourcesTask'; import { BalanceHeroResourcesTask } from './Task/BalanceHeroResourcesTask';
@ -7,49 +6,49 @@ import { Logger } from './Logger';
import { GrabVillageState } from './Task/GrabVillageState'; import { GrabVillageState } from './Task/GrabVillageState';
import { Action, ActionQueue, ImmutableActionList } from './Queue/ActionQueue'; import { Action, ActionQueue, ImmutableActionList } from './Queue/ActionQueue';
import { UpdateResourceContracts } from './Task/UpdateResourceContracts'; import { UpdateResourceContracts } from './Task/UpdateResourceContracts';
import { Resources, ResourcesInterface } from './Core/Resources';
import { SendResourcesTask } from './Task/SendResourcesTask'; import { SendResourcesTask } from './Task/SendResourcesTask';
import { Args } from './Queue/Args'; import { Args } from './Queue/Args';
import { ImmutableTaskList, Task, TaskId } from './Queue/TaskProvider'; import { ImmutableTaskList, Task, TaskId, uniqTaskId, withTime } from './Queue/TaskProvider';
import { ForgeImprovementTask } from './Task/ForgeImprovementTask';
import { getProductionQueue } from './Task/TaskMap';
import { MARKET_ID } from './Core/Buildings'; import { MARKET_ID } from './Core/Buildings';
import { VillageRepositoryInterface } from './VillageRepository'; 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 { export interface NextExecution {
UpgradeBuilding, task?: Task;
ImproveTrooper, action?: Action;
}
interface ContractAttributes {
type: ContractType;
villageId?: number;
buildId?: number;
unitId?: number;
} }
export class Scheduler { export class Scheduler {
private taskQueue: TaskQueue; private taskQueue: TaskQueue;
private actionQueue: ActionQueue; private actionQueue: ActionQueue;
private villageRepository: VillageRepositoryInterface; private villageRepository: VillageRepositoryInterface;
private villageControllerFactory: VillageControllerFactory;
private logger: Logger; private logger: Logger;
constructor( constructor(
taskQueue: TaskQueue, taskQueue: TaskQueue,
actionQueue: ActionQueue, actionQueue: ActionQueue,
villageRepository: VillageRepositoryInterface, villageRepository: VillageRepositoryInterface,
villageControllerFactory: VillageControllerFactory,
logger: Logger logger: Logger
) { ) {
this.taskQueue = taskQueue; this.taskQueue = taskQueue;
this.actionQueue = actionQueue; this.actionQueue = actionQueue;
this.villageRepository = villageRepository; this.villageRepository = villageRepository;
this.villageControllerFactory = villageControllerFactory;
this.logger = logger; this.logger = logger;
// this.taskQueue.push(GrabVillageState.name, {}, timestamp()); // this.taskQueue.push(GrabVillageState.name, {}, timestamp());
// this.taskQueue.push(UpdateResourceContracts.name, {}, timestamp()); // this.taskQueue.push(UpdateResourceContracts.name, {}, timestamp());
// this.taskQueue.push(BalanceHeroResourcesTask.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(5 * 60, GrabVillageState.name);
this.createUniqTaskTimer(10 * 60, BalanceHeroResourcesTask.name); this.createUniqTaskTimer(10 * 60, BalanceHeroResourcesTask.name);
this.createUniqTaskTimer(20 * 60, UpdateResourceContracts.name); this.createUniqTaskTimer(20 * 60, UpdateResourceContracts.name);
@ -69,25 +68,67 @@ export class Scheduler {
return this.actionQueue.seeItems(); return this.actionQueue.seeItems();
} }
nextTask(ts: number) { nextTask(ts: number): NextExecution {
return this.taskQueue.get(ts); 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() { private replaceTask(task: Task): Task | undefined {
return this.actionQueue.pop(); 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 { scheduleTask(name: string, args: Args, ts?: number | undefined): void {
this.logger.info('PUSH TASK', name, args, ts); if (isProductionTask(name) && args.villageId) {
let insertedTs = calculateInsertTime(this.taskQueue.seeItems(), name, args, ts); const controller = this.villageControllerFactory.create(args.villageId);
this.taskQueue.push(name, args, insertedTs); controller.addTask(name, args);
if (args.villageId) { } else {
this.reorderVillageTasks(args.villageId); 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 { 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) { if (!alreadyHasTask) {
this.scheduleTask(name, args, ts); this.scheduleTask(name, args, ts);
} }
@ -98,55 +139,30 @@ export class Scheduler {
this.actionQueue.clear(); 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) { postponeTask(taskId: TaskId, seconds: number) {
const task = this.taskQueue.seeItems().find(t => t.id === taskId); const task = this.taskQueue.seeItems().find(t => t.id === taskId);
if (!task) { if (!task) {
return; return;
} }
const villageId = task.args.villageId; if (isProductionTask(task.name) && task.args.villageId) {
const modifyTime = (t: Task) => withTime(t, timestamp() + seconds); const controller = this.villageControllerFactory.create(task.args.villageId);
controller.postponeTask(taskId, seconds);
let predicateUsed = false; this.removeTask(taskId);
} else {
for (let taskTypePred of TASK_TYPE_PREDICATES) { const modifyTime = withTime(timestamp() + seconds);
if (taskTypePred(task.name) && villageId) {
this.taskQueue.modify(t => sameVillage(villageId, t.args) && taskTypePred(t.name), modifyTime);
predicateUsed = true;
}
}
if (!predicateUsed) {
this.taskQueue.modify(t => t.id === taskId, modifyTime); 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<Action>): void { scheduleActions(actions: Array<Action>): void {
@ -157,31 +173,6 @@ export class Scheduler {
this.actionQueue.clear(); 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<number> {
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 { dropResourceTransferTasks(fromVillageId: number, toVillageId: number): void {
this.taskQueue.remove( this.taskQueue.remove(
t => t =>
@ -205,92 +196,4 @@ export class Scheduler {
tabId: 5, tabId: 5,
}); });
} }
getProductionQueueTasks(villageId: number, queue: ProductionQueue): ReadonlyArray<Task> {
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<TaskNamePredicate> = 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();
} }

View File

@ -6,14 +6,16 @@ import { IncomingMerchant } from '../Core/Market';
import { VillageSettings, VillageSettingsDefaults } from '../Core/Village'; import { VillageSettings, VillageSettingsDefaults } from '../Core/Village';
import { ProductionQueue } from '../Core/ProductionQueue'; import { ProductionQueue } from '../Core/ProductionQueue';
import { getNumber } from '../utils'; import { getNumber } from '../utils';
import { Task, TaskList, uniqTaskId } from '../Queue/TaskProvider';
const RESOURCES_KEY = 'res'; const RESOURCES_KEY = 'resources';
const CAPACITY_KEY = 'cap'; const CAPACITY_KEY = 'capacity';
const PERFORMANCE_KEY = 'perf'; const PERFORMANCE_KEY = 'performance';
const BUILDING_QUEUE_KEY = 'bq'; const BUILDING_QUEUE_INFO_KEY = 'building_queue_info';
const INCOMING_MERCHANTS_KEY = 'im'; const INCOMING_MERCHANTS_KEY = 'incoming_merchants';
const SETTINGS_KEY = 'settings'; 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 = { const ResourceOptions = {
factory: () => new Resources(0, 0, 0, 0), factory: () => new Resources(0, 0, 0, 0),
@ -52,11 +54,11 @@ export class VillageStorage {
} }
storeBuildingQueueInfo(info: BuildingQueueInfo): void { storeBuildingQueueInfo(info: BuildingQueueInfo): void {
this.storage.set(BUILDING_QUEUE_KEY, info); this.storage.set(BUILDING_QUEUE_INFO_KEY, info);
} }
getBuildingQueueInfo(): BuildingQueueInfo { getBuildingQueueInfo(): BuildingQueueInfo {
let plain = this.storage.get(BUILDING_QUEUE_KEY); let plain = this.storage.get(BUILDING_QUEUE_INFO_KEY);
let res = new BuildingQueueInfo(0); let res = new BuildingQueueInfo(0);
return Object.assign(res, plain) as BuildingQueueInfo; return Object.assign(res, plain) as BuildingQueueInfo;
} }
@ -103,4 +105,48 @@ export class VillageStorage {
const key = this.queueKey(queue); const key = this.queueKey(queue);
this.storage.set(key, ts); this.storage.set(key, ts);
} }
getTasks(): Array<Task> {
return this.storage.getTypedList<Task>(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<Task>): void {
this.storage.set(TASK_LIST_KEY, tasks);
}
} }

View File

@ -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<ActionDefinition> {
return [];
}
}

View File

@ -1,4 +1,4 @@
import { TaskController, ActionDefinition } from './TaskController'; import { ActionDefinition, TaskController } from './TaskController';
import { GoToPageAction } from '../Action/GoToPageAction'; import { GoToPageAction } from '../Action/GoToPageAction';
import { UpgradeBuildingTask } from './UpgradeBuildingTask'; import { UpgradeBuildingTask } from './UpgradeBuildingTask';
import { ImmutableTaskList, Task } from '../Queue/TaskProvider'; import { ImmutableTaskList, Task } from '../Queue/TaskProvider';
@ -11,10 +11,9 @@ export class UpdateResourceContracts extends TaskController {
defineActions(task: Task): Array<ActionDefinition> { defineActions(task: Task): Array<ActionDefinition> {
const tasks = this.scheduler.getTaskItems(); const tasks = this.scheduler.getTaskItems();
const paths = [...this.walkUpgradeTasks(tasks), ...this.walkImprovementTask(tasks)]; const paths = uniqPaths([...this.walkUpgradeTasks(tasks), ...this.walkImprovementTask(tasks)]);
const uniq = uniqPaths(paths);
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 { private walkUpgradeTasks(tasks: ImmutableTaskList): PathList {

93
src/VillageController.ts Normal file
View File

@ -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<Task> {
return this._storage.getTasks();
}
removeTask(taskId: TaskId) {
this._storage.removeTasks(t => t.id === taskId);
}
getTasksInProductionQueue(queue: ProductionQueue): Array<Task> {
return this._storage.getTasks().filter(task => getProductionQueue(task.name) === queue);
}
getReadyProductionTask(): Task | undefined {
let sortedTasks: Array<Task> = [];
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());
}
}

View File

@ -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);
}
}

View File

@ -1,12 +1,30 @@
import { Village } from './Core/Village'; import { Village } from './Core/Village';
import { grabVillageList } from './Page/VillageBlock'; import { grabVillageList } from './Page/VillageBlock';
import { VillageNotFound } from './Errors';
export interface VillageRepositoryInterface { export interface VillageRepositoryInterface {
all(): Array<Village>; all(): Array<Village>;
getActive(): Village;
} }
export class VillageRepository implements VillageRepositoryInterface { export class VillageRepository implements VillageRepositoryInterface {
all(): Array<Village> { all(): Array<Village> {
return grabVillageList(); 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;
}
} }

View File

@ -1,5 +1,4 @@
import { Village, VillageSettings } from './Core/Village'; import { Village, VillageSettings } from './Core/Village';
import { Scheduler } from './Scheduler';
import { Resources } from './Core/Resources'; import { Resources } from './Core/Resources';
import { VillageStorage } from './Storage/VillageStorage'; import { VillageStorage } from './Storage/VillageStorage';
import { calcGatheringTimings, GatheringTime } from './Core/GatheringTimings'; import { calcGatheringTimings, GatheringTime } from './Core/GatheringTimings';
@ -8,6 +7,8 @@ import { VillageNotFound } from './Errors';
import { ProductionQueue, ProductionQueueTypes } from './Core/ProductionQueue'; import { ProductionQueue, ProductionQueueTypes } from './Core/ProductionQueue';
import { Task } from './Queue/TaskProvider'; import { Task } from './Queue/TaskProvider';
import { timestamp } from './utils'; import { timestamp } from './utils';
import { VillageControllerFactory } from './VillageControllerFactory';
import { VillageController } from './VillageController';
interface VillageStorageState { interface VillageStorageState {
resources: Resources; resources: Resources;
@ -132,14 +133,13 @@ function taskResourceReducer(resources: Resources, task: Task) {
} }
function createProductionQueueState( function createProductionQueueState(
villageId: number,
queue: ProductionQueue, queue: ProductionQueue,
storage: VillageStorage, controller: VillageController
scheduler: Scheduler
): VillageProductionQueueState { ): VillageProductionQueueState {
const storage = controller.getStorage();
const resources = storage.getResources(); const resources = storage.getResources();
const performance = storage.getResourcesPerformance(); 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 firstTaskResources = tasks.slice(0, 1).reduce(taskResourceReducer, Resources.zero());
const allTaskResources = tasks.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 } = {}; let result: { [queue: string]: VillageProductionQueueState } = {};
for (let queue of ProductionQueueTypes) { for (let queue of ProductionQueueTypes) {
result[queue] = createProductionQueueState(villageId, queue, storage, scheduler); result[queue] = createProductionQueueState(queue, controller);
} }
return result; return result;
} }
function calcFrontierResources(villageId: number, scheduler: Scheduler): Resources { function calcFrontierResources(controller: VillageController): Resources {
let result = Resources.zero(); let result = Resources.zero();
for (let queue of ProductionQueueTypes) { 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()); const firstTaskResources = tasks.slice(0, 1).reduce(taskResourceReducer, Resources.zero());
result = result.add(firstTaskResources); result = result.add(firstTaskResources);
} }
return result; return result;
} }
function createVillageOwnState(village: Village, scheduler: Scheduler): VillageOwnState { function createVillageOwnState(village: Village, controller: VillageController): VillageOwnState {
const storage = new VillageStorage(village.id); const storage = controller.getStorage();
const resources = storage.getResources(); const resources = storage.getResources();
const resourceStorage = storage.getResourceStorage(); const resourceStorage = storage.getResourceStorage();
const performance = storage.getResourcesPerformance(); const performance = storage.getResourcesPerformance();
const buildQueueInfo = storage.getBuildingQueueInfo(); const buildQueueInfo = storage.getBuildingQueueInfo();
const requiredResources = scheduler.getVillageRequiredResources(village.id); const requiredResources = controller.getVillageRequiredResources();
const frontierResources = calcFrontierResources(village.id, scheduler); const frontierResources = calcFrontierResources(controller);
const totalRequiredResources = scheduler.getTotalVillageRequiredResources(village.id); const totalRequiredResources = controller.getTotalVillageRequiredResources();
return { return {
id: village.id, id: village.id,
@ -196,24 +196,24 @@ function createVillageOwnState(village: Village, scheduler: Scheduler): VillageO
buildRemainingSeconds: buildQueueInfo.seconds, buildRemainingSeconds: buildQueueInfo.seconds,
incomingResources: calcIncomingResources(storage), incomingResources: calcIncomingResources(storage),
settings: storage.getSettings(), settings: storage.getSettings(),
queues: createAllProductionQueueStates(village.id, storage, scheduler), queues: createAllProductionQueueStates(controller),
}; };
} }
function createVillageOwnStates(villages: Array<Village>, scheduler: Scheduler): VillageOwnStateDictionary { function createVillageOwnStates(
villages: Array<Village>,
villageControllerFactory: VillageControllerFactory
): VillageOwnStateDictionary {
const result: VillageOwnStateDictionary = {}; const result: VillageOwnStateDictionary = {};
for (let village of villages) { 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; return result;
} }
function createVillageState( function createVillageState(state: VillageOwnState, ownStates: VillageOwnStateDictionary): VillageState {
state: VillageOwnState, const villageIds = Object.keys(ownStates).map(k => +k);
ownStates: VillageOwnStateDictionary,
scheduler: Scheduler
): VillageState {
const villageIds = scheduler.getResourceShipmentVillageIds(state.id);
const commitments = villageIds.reduce((memo, shipmentVillageId) => { const commitments = villageIds.reduce((memo, shipmentVillageId) => {
const shipmentVillageState = ownStates[shipmentVillageId]; const shipmentVillageState = ownStates[shipmentVillageId];
const shipmentVillageRequired = shipmentVillageState.required; const shipmentVillageRequired = shipmentVillageState.required;
@ -224,26 +224,29 @@ function createVillageState(
return { ...state, commitments, shipment: villageIds }; return { ...state, commitments, shipment: villageIds };
} }
function getVillageStates(villages: Array<Village>, scheduler: Scheduler): Array<VillageState> { function getVillageStates(
const ownStates = createVillageOwnStates(villages, scheduler); villages: Array<Village>,
return villages.map(village => createVillageState(ownStates[village.id], ownStates, scheduler)); villageControllerFactory: VillageControllerFactory
): Array<VillageState> {
const ownStates = createVillageOwnStates(villages, villageControllerFactory);
return villages.map(village => createVillageState(ownStates[village.id], ownStates));
} }
export class VillageStateRepository { export class VillageStateRepository {
private villageRepository: VillageRepositoryInterface; private villageRepository: VillageRepositoryInterface;
private scheduler: Scheduler; private villageControllerFactory: VillageControllerFactory;
constructor(villageRepository: VillageRepositoryInterface, scheduler: Scheduler) { constructor(villageRepository: VillageRepositoryInterface, villageControllerFactory: VillageControllerFactory) {
this.villageRepository = villageRepository; this.villageRepository = villageRepository;
this.scheduler = scheduler; this.villageControllerFactory = villageControllerFactory;
} }
getAllVillageStates(): Array<VillageState> { getAllVillageStates(): Array<VillageState> {
return getVillageStates(this.villageRepository.all(), this.scheduler); return getVillageStates(this.villageRepository.all(), this.villageControllerFactory);
} }
getVillageState(villageId: number): VillageState { 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); const needle = states.find(s => s.id === villageId);
if (!needle) { if (!needle) {
throw new VillageNotFound(`Village ${villageId} not found`); throw new VillageNotFound(`Village ${villageId} not found`);