Add smart resource transfer
This commit is contained in:
parent
301b1a6ca9
commit
faed76df4d
46
src/Action/FindSendResourcesPath.ts
Normal file
46
src/Action/FindSendResourcesPath.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,3 +8,8 @@ export class IncomingMerchant {
|
|||||||
this.ts = ts;
|
this.ts = ts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MerchantsInfo {
|
||||||
|
available: number;
|
||||||
|
carry: number;
|
||||||
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
49
src/ResourceTransfer.ts
Normal 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() };
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
src/Storage/ResourceTransferStorage.ts
Normal file
33
src/Storage/ResourceTransferStorage.ts
Normal 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,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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() {
|
||||||
|
Loading…
Reference in New Issue
Block a user