Add smart resource transfer

This commit is contained in:
Anton Vakhrushev 2020-05-24 21:05:29 +03:00
parent 301b1a6ca9
commit faed76df4d
17 changed files with 272 additions and 148 deletions

View File

@ -0,0 +1,46 @@
import { ActionController, registerAction } from './ActionController';
import { FailTaskError, taskError, TryLaterError } from '../Errors';
import { Resources } from '../Core/Resources';
import { Coordinates } from '../Core/Village';
import { aroundMinutes, timestamp } from '../utils';
import { Args } from '../Queue/Args';
import { Task } from '../Queue/TaskProvider';
import { clickSendButton, fillSendResourcesForm } from '../Page/BuildingPage/MarketPage';
import { VillageState } from '../VillageState';
import { MerchantsInfo } from '../Core/Market';
import { goToMarketSendResourcesPage, goToResourceViewPage } from '../Task/ActionBundles';
import { ResourceTransferCalculator, ResourceTransferReport } from '../ResourceTransfer';
import { ResourceTransferStorage } from '../Storage/ResourceTransferStorage';
import { path } from '../Helpers/Path';
import { MARKET_ID } from '../Core/Buildings';
@registerAction
export class FindSendResourcesPath extends ActionController {
async run(args: Args, task: Task): Promise<any> {
const reports: Array<ResourceTransferReport> = [];
const calculator = new ResourceTransferCalculator(this.villageFactory);
const villages = this.villageFactory.getAllVillages();
for (let fromVillage of villages) {
for (let toVillage of villages) {
reports.push(calculator.calc(fromVillage.id, toVillage.id));
}
}
reports.sort((r1, r2) => r2.score - r1.score);
const bestReport = reports.shift();
if (!bestReport) {
throw new FailTaskError('No best report for transfer resources');
}
console.log('Best report', bestReport);
const storage = new ResourceTransferStorage();
storage.storeReport(bestReport);
const marketPath = path('/build.php', { newdid: bestReport.fromVillageId, gid: MARKET_ID, t: 5 });
window.location.assign(marketPath);
}
}

View File

@ -1,123 +1,33 @@
import { ActionController, registerAction } from './ActionController'; import { ActionController, registerAction } from './ActionController';
import { taskError, TryLaterError } from '../Errors';
import { Resources } from '../Core/Resources';
import { Coordinates } from '../Core/Village';
import { aroundMinutes, timestamp } from '../utils';
import { Args } from '../Queue/Args'; import { Args } from '../Queue/Args';
import { Task } from '../Queue/TaskProvider'; import { Task } from '../Queue/TaskProvider';
import { clickSendButton, fillSendResourcesForm, grabMerchantsInfo } from '../Page/BuildingPage/MarketPage'; import { clickSendButton, fillSendResourcesForm } from '../Page/BuildingPage/MarketPage';
import { VillageState } from '../VillageState'; import { ResourceTransferCalculator } from '../ResourceTransfer';
import { ResourceTransferStorage } from '../Storage/ResourceTransferStorage';
import { Resources } from '../Core/Resources';
import { AbortTaskError } from '../Errors';
@registerAction @registerAction
export class SendResourcesAction extends ActionController { export class SendResourcesAction extends ActionController {
async run(args: Args, task: Task): Promise<any> { async run(args: Args, task: Task): Promise<any> {
this.ensureSameVillage(args, task); const storage = new ResourceTransferStorage();
const savedReport = storage.getReport();
const senderVillageId = args.villageId || taskError('No source village id'); const fromVillage = this.villageFactory.getVillage(savedReport.fromVillageId);
const targetVillageId = args.targetVillageId || taskError('No target village id'); const toVillage = this.villageFactory.getVillage(savedReport.toVillageId);
const coordinates = Coordinates.fromObject(args.coordinates || taskError('No coordinates')); const coordinates = toVillage.crd;
const senderVillage = this.villageFactory.createState(senderVillageId); const calculator = new ResourceTransferCalculator(this.villageFactory);
const recipientVillage = this.villageFactory.createState(targetVillageId); const report = calculator.calc(fromVillage.id, toVillage.id);
const readyToTransfer = this.getResourcesForTransfer(senderVillage, recipientVillage); console.log('To transfer report', report);
console.log('To transfer res', readyToTransfer); if (Resources.fromObject(report.resources).empty()) {
throw new AbortTaskError('No resources to transfer');
}
// Schedule recurrent task fillSendResourcesForm(report.resources, coordinates);
const timeout = senderVillage.settings.sendResourcesTimeout;
this.scheduler.scheduleTask(task.name, task.args, timestamp() + aroundMinutes(timeout));
fillSendResourcesForm(readyToTransfer, coordinates);
clickSendButton(); clickSendButton();
} }
private getMerchantsCapacity(timeout: number): number {
const merchants = grabMerchantsInfo();
const capacity = merchants.available * merchants.carry;
if (!capacity) {
throw new TryLaterError(aroundMinutes(timeout), 'No merchants');
}
return capacity;
}
private getSenderAvailableResources(senderState: VillageState): Resources {
const balance = senderState.required.balance;
const free = balance.max(Resources.zero());
console.table([
{ name: 'Sender balance', ...balance },
{ name: 'Sender free', ...free },
]);
const amount = free.amount();
const threshold = senderState.settings.sendResourcesThreshold;
const timeout = senderState.settings.sendResourcesTimeout;
if (amount < threshold) {
throw new TryLaterError(
aroundMinutes(timeout),
`No free resources (amount ${amount} < threshold ${threshold})`
);
}
return free;
}
private getRecipientRequirements(recipientState: VillageState, timeout: number): Resources {
const maxPossibleToStore = recipientState.storage.capacity.sub(recipientState.performance);
const currentResources = recipientState.resources;
const incomingResources = recipientState.incomingResources;
const requirementResources = recipientState.required.resources;
const missingResources = requirementResources
.min(maxPossibleToStore)
.sub(incomingResources)
.sub(currentResources)
.max(Resources.zero());
console.table([
{ name: 'Recipient max possible', ...maxPossibleToStore },
{ name: 'Recipient resources', ...currentResources },
{ name: 'Recipient incoming', ...incomingResources },
{ name: 'Recipient requirements', ...requirementResources },
{ name: 'Recipient missing', ...missingResources },
]);
if (missingResources.empty()) {
throw new TryLaterError(aroundMinutes(timeout), 'No missing resources');
}
return missingResources;
}
private getResourcesForTransfer(senderState: VillageState, recipientState: VillageState): Resources {
const multiplier = senderState.settings.sendResourcesMultiplier;
const timeout = senderState.settings.sendResourcesTimeout;
const senderReadyResources = this.getSenderAvailableResources(senderState).downTo(multiplier);
const recipientNeedResources = this.getRecipientRequirements(recipientState, timeout).upTo(multiplier);
const contractResources = senderReadyResources.min(recipientNeedResources);
const merchantsCapacity = this.getMerchantsCapacity(timeout);
let readyToTransfer = contractResources;
if (contractResources.amount() > merchantsCapacity) {
const merchantScale = merchantsCapacity / contractResources.amount();
readyToTransfer = contractResources.scale(merchantScale).downTo(multiplier);
}
if (readyToTransfer.empty()) {
throw new TryLaterError(aroundMinutes(timeout), 'Not enough resources to transfer');
}
console.log('Merchants capacity', merchantsCapacity);
console.table([
{ name: 'Sender', ...senderReadyResources },
{ name: 'Recipient', ...recipientNeedResources },
{ name: 'Prepared', ...contractResources },
{ name: 'Ready to transfer', ...readyToTransfer },
]);
return readyToTransfer;
}
} }

View File

@ -8,3 +8,8 @@ export class IncomingMerchant {
this.ts = ts; this.ts = ts;
} }
} }
export interface MerchantsInfo {
available: number;
carry: number;
}

View File

@ -10,7 +10,6 @@ import { ExecutionStorage } from './Storage/ExecutionStorage';
import { Action } from './Queue/ActionQueue'; 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 { VillageStateFactory } from './VillageState';
import { VillageFactory } from './VillageFactory'; import { VillageFactory } from './VillageFactory';
export interface ExecutionSettings { export interface ExecutionSettings {
@ -79,8 +78,6 @@ export class Executor {
} }
private async doTaskProcessingStep() { private async doTaskProcessingStep() {
this.runGrabbers();
const currentTs = timestamp(); const currentTs = timestamp();
const { task, action } = this.scheduler.nextTask(currentTs); const { task, action } = this.scheduler.nextTask(currentTs);
@ -92,6 +89,8 @@ export class Executor {
this.logger.info('CURRENT JOB', 'TASK', task, 'ACTION', action); this.logger.info('CURRENT JOB', 'TASK', task, 'ACTION', action);
this.runGrabbers();
try { try {
if (task && action) { if (task && action) {
return await this.processActionCommand(action, task); return await this.processActionCommand(action, task);
@ -117,7 +116,7 @@ export class Executor {
} }
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.villageFactory);
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);

View File

@ -1,6 +1,6 @@
import { Grabber } from './Grabber'; import { Grabber } from './Grabber';
import { isMarketSendResourcesPage } from '../Page/PageDetectors'; import { isMarketSendResourcesPage } from '../Page/PageDetectors';
import { grabIncomingMerchants } from '../Page/BuildingPage/MarketPage'; import { grabIncomingMerchants, grabMerchantsInfo } from '../Page/BuildingPage/MarketPage';
export class MarketPageGrabber extends Grabber { export class MarketPageGrabber extends Grabber {
grab(): void { grab(): void {
@ -9,5 +9,6 @@ export class MarketPageGrabber extends Grabber {
} }
this.storage.storeIncomingMerchants(grabIncomingMerchants()); this.storage.storeIncomingMerchants(grabIncomingMerchants());
this.storage.storeMerchantsInfo(grabMerchantsInfo());
} }
} }

View File

@ -1,7 +1,7 @@
import { getNumber, uniqId } from '../../utils'; import { getNumber, uniqId } from '../../utils';
import { Resources } from '../../Core/Resources'; import { Resources, ResourcesInterface } from '../../Core/Resources';
import { Coordinates } from '../../Core/Village'; import { Coordinates } from '../../Core/Village';
import { IncomingMerchant } from '../../Core/Market'; import { IncomingMerchant, MerchantsInfo } from '../../Core/Market';
import { grabResourcesFromList } from './BuildingPage'; import { grabResourcesFromList } from './BuildingPage';
interface SendResourcesClickHandler { interface SendResourcesClickHandler {
@ -31,13 +31,13 @@ export function createSendResourcesButton(onClickHandler: SendResourcesClickHand
jQuery(`#${id}`).on('click', createHandler()); jQuery(`#${id}`).on('click', createHandler());
} }
export function grabMerchantsInfo() { export function grabMerchantsInfo(): MerchantsInfo {
const available = getNumber(jQuery('.merchantsAvailable').text()); const available = getNumber(jQuery('.merchantsAvailable').text());
const carry = getNumber(jQuery('.carry b').text()); const carry = getNumber(jQuery('.carry b').text());
return { available, carry }; return { available, carry };
} }
export function fillSendResourcesForm(resources: Resources, crd: Coordinates) { export function fillSendResourcesForm(resources: ResourcesInterface, crd: Coordinates) {
const sendSelect = jQuery('#send_select'); const sendSelect = jQuery('#send_select');
sendSelect.find('#r1').val(resources.lumber); sendSelect.find('#r1').val(resources.lumber);
sendSelect.find('#r2').val(resources.clay); sendSelect.find('#r2').val(resources.clay);

49
src/ResourceTransfer.ts Normal file
View File

@ -0,0 +1,49 @@
import { VillageFactory } from './VillageFactory';
import { ResourcesInterface } from './Core/Resources';
export interface ResourceTransferReport {
fromVillageId: number;
toVillageId: number;
resources: ResourcesInterface;
score: number;
}
export class ResourceTransferCalculator {
private factory: VillageFactory;
constructor(factory: VillageFactory) {
this.factory = factory;
}
calc(fromVillageId: number, toVillageId: number): ResourceTransferReport {
const senderState = this.factory.createState(fromVillageId);
const senderController = this.factory.createController(fromVillageId);
const senderStorage = this.factory.createStorage(fromVillageId);
const recipientController = this.factory.createController(toVillageId);
const multiplier = senderState.settings.sendResourcesMultiplier;
const senderReadyResources = senderController.getAvailableForSendResources().downTo(multiplier);
const recipientNeedResources = recipientController.getRequiredResources().upTo(multiplier);
const contractResources = senderReadyResources.min(recipientNeedResources);
const merchantsInfo = senderStorage.getMerchantsInfo();
const merchantsCapacity = merchantsInfo.available * merchantsInfo.carry;
let readyToTransfer = contractResources;
if (contractResources.amount() && contractResources.amount() > merchantsCapacity) {
const merchantScale = merchantsCapacity / contractResources.amount();
readyToTransfer = contractResources.scale(merchantScale).downTo(multiplier);
}
console.log('Merchants capacity', merchantsCapacity);
console.table([
{ name: 'Sender', ...senderReadyResources },
{ name: 'Recipient', ...recipientNeedResources },
{ name: 'Prepared', ...contractResources },
{ name: 'Ready to transfer', ...readyToTransfer },
]);
return { fromVillageId, toVillageId, resources: readyToTransfer, score: readyToTransfer.amount() };
}
}

View File

@ -1,4 +1,4 @@
import { timestamp } from './utils'; import { around, timestamp } from './utils';
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';
@ -22,11 +22,11 @@ export interface NextExecution {
} }
export class Scheduler { export class Scheduler {
private taskQueue: TaskQueue; private readonly taskQueue: TaskQueue;
private actionQueue: ActionQueue; private readonly actionQueue: ActionQueue;
private villageRepository: VillageRepositoryInterface; private readonly villageRepository: VillageRepositoryInterface;
private villageControllerFactory: VillageFactory; private readonly villageControllerFactory: VillageFactory;
private logger: Logger; private readonly logger: Logger;
constructor( constructor(
taskQueue: TaskQueue, taskQueue: TaskQueue,
@ -51,14 +51,16 @@ export class Scheduler {
} }
this.createUniqTaskTimer(5 * 60, GrabVillageState.name); this.createUniqTaskTimer(5 * 60, GrabVillageState.name);
this.createUniqTaskTimer(10 * 60, SendResourcesTask.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);
this.createUniqTaskTimer(60 * 60, SendOnAdventureTask.name); // this.createUniqTaskTimer(60 * 60, SendOnAdventureTask.name);
} }
private createUniqTaskTimer(seconds: number, name: string, args: Args = {}) { private createUniqTaskTimer(seconds: number, name: string, args: Args = {}) {
this.scheduleUniqTask(name, args, timestamp() + seconds - 10); this.scheduleUniqTask(name, args, timestamp() + seconds - 10);
setInterval(() => this.scheduleUniqTask(name, args, timestamp()), seconds * 1000); const intervalTime = around(seconds, 0.2) * 1000;
setInterval(() => this.scheduleUniqTask(name, args, timestamp()), intervalTime);
} }
getTaskItems(): ImmutableTaskList { getTaskItems(): ImmutableTaskList {

View File

@ -13,20 +13,20 @@ export interface StatisticsStorageInterface {
} }
export class Statistics { export class Statistics {
private state: StatisticsStorageInterface; private readonly storage: StatisticsStorageInterface;
static readonly keepRecords = KEEP_RECORD_COUNT; static readonly keepRecords = KEEP_RECORD_COUNT;
constructor(storage: StatisticsStorageInterface) { constructor(storage: StatisticsStorageInterface) {
this.state = storage; this.storage = storage;
} }
incrementAction(ts: number): void { incrementAction(ts: number): void {
const stat = this.state.getActionStatistics(); const stat = this.storage.getActionStatistics();
const key = dateFormat(ts * 1000, KEY_FORMAT); const key = dateFormat(ts * 1000, KEY_FORMAT);
stat[key] = (stat[key] || 0) + 1; stat[key] = (stat[key] || 0) + 1;
this.trimStatistics(stat); this.trimStatistics(stat);
this.state.setActionStatistics(stat); this.storage.setActionStatistics(stat);
} }
private trimStatistics(stat: ActionStatistics) { private trimStatistics(stat: ActionStatistics) {
@ -49,6 +49,6 @@ export class Statistics {
} }
getActionStatistics(): ActionStatistics { getActionStatistics(): ActionStatistics {
return this.state.getActionStatistics(); return this.storage.getActionStatistics();
} }
} }

View File

@ -0,0 +1,33 @@
import { DataStorage } from '../DataStorage';
import { ResourceTransferReport } from '../ResourceTransfer';
const NAMESPACE = 'resource_transfer.v1';
const REPORT_KEY = 'report';
export class ResourceTransferStorage {
private storage: DataStorage;
constructor() {
this.storage = new DataStorage(NAMESPACE);
}
storeReport(report: ResourceTransferReport): void {
this.storage.set(REPORT_KEY, report);
}
getReport(): ResourceTransferReport {
return this.storage.getTyped<ResourceTransferReport>(REPORT_KEY, {
factory: () => ({
fromVillageId: 0,
toVillageId: 0,
resources: {
lumber: 0,
clay: 0,
iron: 0,
crop: 0,
},
score: 0,
}),
});
}
}

View File

@ -2,7 +2,7 @@ import { DataStorage } from '../DataStorage';
import { BuildingQueueInfo } from '../Game'; import { BuildingQueueInfo } from '../Game';
import { Resources, ResourcesInterface } from '../Core/Resources'; import { Resources, ResourcesInterface } from '../Core/Resources';
import { ResourceStorage } from '../Core/ResourceStorage'; import { ResourceStorage } from '../Core/ResourceStorage';
import { IncomingMerchant } from '../Core/Market'; import { IncomingMerchant, MerchantsInfo } 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';
@ -13,6 +13,7 @@ const CAPACITY_KEY = 'capacity';
const PERFORMANCE_KEY = 'performance'; const PERFORMANCE_KEY = 'performance';
const BUILDING_QUEUE_INFO_KEY = 'building_queue_info'; const BUILDING_QUEUE_INFO_KEY = 'building_queue_info';
const INCOMING_MERCHANTS_KEY = 'incoming_merchants'; const INCOMING_MERCHANTS_KEY = 'incoming_merchants';
const MERCHANTS_INFO_KEY = 'merchants_info';
const SETTINGS_KEY = 'settings'; const SETTINGS_KEY = 'settings';
const QUEUE_ENDING_TIME_KEY = 'queue_ending_time'; const QUEUE_ENDING_TIME_KEY = 'queue_ending_time';
const TASK_LIST_KEY = 'tasks'; const TASK_LIST_KEY = 'tasks';
@ -63,6 +64,16 @@ export class VillageStorage {
return Object.assign(res, plain) as BuildingQueueInfo; return Object.assign(res, plain) as BuildingQueueInfo;
} }
storeMerchantsInfo(info: MerchantsInfo): void {
this.storage.set(MERCHANTS_INFO_KEY, info);
}
getMerchantsInfo(): MerchantsInfo {
return this.storage.getTyped<MerchantsInfo>(MERCHANTS_INFO_KEY, {
factory: () => ({ available: 0, carry: 0 }),
});
}
storeIncomingMerchants(merchants: ReadonlyArray<IncomingMerchant>): void { storeIncomingMerchants(merchants: ReadonlyArray<IncomingMerchant>): void {
this.storage.set( this.storage.set(
INCOMING_MERCHANTS_KEY, INCOMING_MERCHANTS_KEY,

View File

@ -4,21 +4,23 @@ import { ClickButtonAction } from '../Action/ClickButtonAction';
import { goToMarketSendResourcesPage, goToResourceViewPage } from './ActionBundles'; import { goToMarketSendResourcesPage, goToResourceViewPage } from './ActionBundles';
import { Task } from '../Queue/TaskProvider'; import { Task } from '../Queue/TaskProvider';
import { registerTask } from './TaskMap'; import { registerTask } from './TaskMap';
import { taskError } from '../Errors'; import { FindSendResourcesPath } from '../Action/FindSendResourcesPath';
@registerTask() @registerTask()
export class SendResourcesTask extends TaskController { export class SendResourcesTask extends TaskController {
defineActions(task: Task): Array<ActionDefinition> { defineActions(task: Task): Array<ActionDefinition> {
const targetVillageId = task.args.targetVillageId || taskError('Empty target village id'); const actions: Array<ActionDefinition> = [];
const villageId = task.args.villageId || taskError('Empty village id');
return [ const villages = this.factory.getAllVillages();
goToResourceViewPage(targetVillageId), for (let village of villages) {
goToMarketSendResourcesPage(targetVillageId), actions.push(goToResourceViewPage(village.id));
goToResourceViewPage(villageId), actions.push(goToMarketSendResourcesPage(village.id));
goToMarketSendResourcesPage(villageId), }
[SendResourcesAction.name],
[ClickButtonAction.name, { selector: '#enabledButton.green.sendRessources' }], actions.push([FindSendResourcesPath.name]);
]; actions.push([SendResourcesAction.name]);
actions.push([ClickButtonAction.name, { selector: '#enabledButton.green.sendRessources' }]);
return actions;
} }
} }

View File

@ -3,14 +3,17 @@ import { CompleteTaskAction } from '../Action/CompleteTaskAction';
import { Action } from '../Queue/ActionQueue'; import { Action } from '../Queue/ActionQueue';
import { Args } from '../Queue/Args'; import { Args } from '../Queue/Args';
import { Task } from '../Queue/TaskProvider'; import { Task } from '../Queue/TaskProvider';
import { VillageFactory } from '../VillageFactory';
export type ActionDefinition = [string] | [string, Args]; export type ActionDefinition = [string] | [string, Args];
export class TaskController { export class TaskController {
protected scheduler: Scheduler; protected readonly scheduler: Scheduler;
protected readonly factory: VillageFactory;
constructor(scheduler: Scheduler) { constructor(scheduler: Scheduler, factory: VillageFactory) {
this.scheduler = scheduler; this.scheduler = scheduler;
this.factory = factory;
} }
async run(task: Task) { async run(task: Task) {

View File

@ -1,6 +1,7 @@
import { Scheduler } from '../Scheduler'; import { Scheduler } from '../Scheduler';
import { TaskController } from './TaskController'; import { TaskController } from './TaskController';
import { ProductionQueue } from '../Core/ProductionQueue'; import { ProductionQueue } from '../Core/ProductionQueue';
import { VillageFactory } from '../VillageFactory';
interface TaskOptions { interface TaskOptions {
queue?: ProductionQueue; queue?: ProductionQueue;
@ -34,11 +35,15 @@ export function getProductionQueue(name: string): ProductionQueue | undefined {
return taskDescription.queue; return taskDescription.queue;
} }
export function createTaskHandler(name: string, scheduler: Scheduler): TaskController | undefined { export function createTaskHandler(
name: string,
scheduler: Scheduler,
factory: VillageFactory
): TaskController | undefined {
const taskDescription = taskMap[name]; const taskDescription = taskMap[name];
if (taskDescription === undefined) { if (taskDescription === undefined) {
return undefined; return undefined;
} }
const constructor = (taskDescription.ctor as unknown) as typeof TaskController; const constructor = (taskDescription.ctor as unknown) as typeof TaskController;
return new constructor(scheduler); return new constructor(scheduler, factory);
} }

View File

@ -2,10 +2,13 @@ import { VillageTaskCollection } from './VillageTaskCollection';
import { Task, TaskId } from './Queue/TaskProvider'; import { Task, TaskId } from './Queue/TaskProvider';
import { Args } from './Queue/Args'; import { Args } from './Queue/Args';
import { VillageState } from './VillageState'; import { VillageState } from './VillageState';
import { Resources } from './Core/Resources';
import { TryLaterError } from './Errors';
import { aroundMinutes } from './utils';
export class VillageController { export class VillageController {
private readonly villageId: number; private readonly villageId: number;
private taskCollection: VillageTaskCollection; private readonly taskCollection: VillageTaskCollection;
private readonly state: VillageState; private readonly state: VillageState;
constructor(villageId: number, taskCollection: VillageTaskCollection, state: VillageState) { constructor(villageId: number, taskCollection: VillageTaskCollection, state: VillageState) {
@ -33,4 +36,46 @@ export class VillageController {
postponeTask(taskId: TaskId, seconds: number) { postponeTask(taskId: TaskId, seconds: number) {
this.taskCollection.postponeTask(taskId, seconds); this.taskCollection.postponeTask(taskId, seconds);
} }
getAvailableForSendResources(): Resources {
const balance = this.state.required.balance;
const free = balance.max(Resources.zero());
console.table([
{ name: 'Sender balance', ...balance },
{ name: 'Sender free', ...free },
]);
const amount = free.amount();
const threshold = this.state.settings.sendResourcesThreshold;
if (amount < threshold) {
return Resources.zero();
}
return free;
}
getRequiredResources(): Resources {
const performance = this.state.performance;
const maxPossibleToStore = this.state.storage.capacity.sub(performance);
const currentResources = this.state.resources;
const incomingResources = this.state.incomingResources;
const requirementResources = this.state.required.resources;
const missingResources = requirementResources
.min(maxPossibleToStore)
.sub(incomingResources)
.sub(currentResources)
.max(Resources.zero());
console.table([
{ name: 'Recipient max possible', ...maxPossibleToStore },
{ name: 'Recipient resources', ...currentResources },
{ name: 'Recipient incoming', ...incomingResources },
{ name: 'Recipient requirements', ...requirementResources },
{ name: 'Recipient missing', ...missingResources },
]);
return missingResources;
}
} }

View File

@ -3,6 +3,7 @@ import { VillageStorage } from './Storage/VillageStorage';
import { VillageRepository } from './VillageRepository'; import { VillageRepository } from './VillageRepository';
import { VillageTaskCollection } from './VillageTaskCollection'; import { VillageTaskCollection } from './VillageTaskCollection';
import { VillageState, VillageStateFactory } from './VillageState'; import { VillageState, VillageStateFactory } from './VillageState';
import { Village } from './Core/Village';
export class VillageFactory { export class VillageFactory {
private readonly villageRepository: VillageRepository; private readonly villageRepository: VillageRepository;
@ -11,6 +12,14 @@ export class VillageFactory {
this.villageRepository = villageRepository; this.villageRepository = villageRepository;
} }
getAllVillages(): Array<Village> {
return this.villageRepository.all();
}
getVillage(villageId: number): Village {
return this.villageRepository.get(villageId);
}
createStorage(villageId: number): VillageStorage { createStorage(villageId: number): VillageStorage {
const village = this.villageRepository.get(villageId); const village = this.villageRepository.get(villageId);
return new VillageStorage(village.id); return new VillageStorage(village.id);

View File

@ -10,6 +10,11 @@ export function randomInRange(from: number, to: number): number {
return Math.floor(from + variation); return Math.floor(from + variation);
} }
export function around(value: number, koeff: number): number {
const delta = Math.floor(value * koeff);
return randomInRange(value - delta, value + delta);
}
export async function sleepMicro() { export async function sleepMicro() {
const timeInMs = randomInRange(1500, 2500); const timeInMs = randomInRange(1500, 2500);
return await sleep(timeInMs); return await sleep(timeInMs);
@ -17,8 +22,7 @@ export async function sleepMicro() {
export function aroundMinutes(minutes: number) { export function aroundMinutes(minutes: number) {
const seconds = minutes * 60; const seconds = minutes * 60;
const delta = Math.floor(seconds * 0.1); return around(seconds, 0.1);
return randomInRange(seconds - delta, seconds + delta);
} }
export async function waitForLoad() { export async function waitForLoad() {