travian/src/VillageState.ts
2020-06-24 14:04:26 +03:00

354 lines
11 KiB
TypeScript

import { Village, VillageSettings } from './Core/Village';
import { Resources } from './Core/Resources';
import { VillageStorage } from './Storage/VillageStorage';
import { calcGatheringTimings, GatheringTime } from './Core/GatheringTimings';
import { VillageRepositoryInterface } from './VillageRepository';
import { VillageNotFound } from './Errors';
import { ProductionQueue } from './Core/ProductionQueue';
import { Task } from './Queue/TaskProvider';
import { timestamp } from './utils';
import { QueueTasks, VillageTaskCollection } from './VillageTaskCollection';
import { TrainTroopTask } from './Task/TrainTroopTask';
interface VillageStorageState {
resources: Resources;
capacity: Resources;
balance: Resources;
performance: Resources;
timeToZero: GatheringTime;
timeToFull: GatheringTime;
}
/**
* State of one or more tasks, which required some resources.
*/
interface ResourceLineState {
/**
* Required resources (always positive)
*/
resources: Resources;
/**
* Balance resources (current - required, may be negative)
*/
balance: Resources;
/**
* Time to gather all type of resources (slowest time)
*/
time: GatheringTime;
/**
* Is active - resources not equals to zero.
*/
active: boolean;
}
interface VillageProductionQueueState {
queue: ProductionQueue;
tasks: Array<Task>;
isActive: boolean;
isWaiting: boolean;
currentTaskFinishTimestamp: number;
currentTaskFinishSeconds: number;
firstTask: ResourceLineState;
allTasks: ResourceLineState;
taskCount: number;
}
interface VillageProductionQueueStateDict {
[queue: string]: VillageProductionQueueState;
}
interface VillageOwnState {
/**
* Village id
*/
id: number;
/**
* Village object
*/
village: Village;
/**
* Current village resources
*/
resources: Resources;
performance: Resources;
storage: VillageStorageState;
upperCriticalLevel: Resources;
storageOptimumFullness: Resources;
isOverflowing: boolean;
queues: VillageProductionQueueStateDict;
firstReadyTask: Task | undefined;
/**
* Required resources for nearest task
*/
required: ResourceLineState;
/**
* Required resources for first tasks in production queues
*/
frontierRequired: ResourceLineState;
/**
* Required resources for all tasks
*/
totalRequired: ResourceLineState;
incomingResources: Resources;
settings: VillageSettings;
}
interface VillageOwnStateDictionary {
[id: number]: VillageOwnState;
}
export interface VillageState extends VillageOwnState {
/**
* Resource commitments of this village to other (may be negative)
*/
commitments: Resources;
}
function makeResourceBalance(
resources: Resources,
current: Resources,
performance: Resources
): ResourceLineState {
return {
resources,
balance: current.sub(resources),
time: timeToAllResources(current, resources, performance),
active: !resources.empty(),
};
}
function makeStorageBalance(
resources: Resources,
storage: Resources,
performance: Resources
): VillageStorageState {
return {
resources,
capacity: storage,
performance,
balance: storage.sub(resources),
timeToZero: timeToFastestResource(resources, Resources.zero(), performance),
timeToFull: timeToFastestResource(resources, storage, performance),
};
}
function timeToAllResources(
current: Resources,
desired: Resources,
performance: Resources
): GatheringTime {
const timings = calcGatheringTimings(current, desired, performance);
return timings.slowest;
}
function timeToFastestResource(
current: Resources,
desired: Resources,
performance: Resources
): GatheringTime {
const timings = calcGatheringTimings(current, desired, performance);
return timings.fastest;
}
function calcIncomingResources(storage: VillageStorage): Resources {
return storage.getIncomingMerchants().reduce((m, i) => m.add(i.resources), Resources.zero());
}
function taskResourceReducer(resources: Resources, task: Task) {
return task.args.resources
? resources.add(Resources.fromObject(task.args.resources))
: resources;
}
function createProductionQueueState(
taskQueueInfo: QueueTasks,
storage: VillageStorage
): VillageProductionQueueState {
const queue = taskQueueInfo.queue;
const tasks = taskQueueInfo.tasks;
const taskEndingTimestamp = taskQueueInfo.finishTs;
const resources = storage.getResources();
const performance = storage.getResourcesPerformance();
const firstTaskResources = tasks.slice(0, 1).reduce(taskResourceReducer, Resources.zero());
const allTaskResources = tasks.reduce(taskResourceReducer, Resources.zero());
const currentTimestamp = timestamp();
return {
queue,
tasks,
isActive: tasks.length !== 0 || taskEndingTimestamp > currentTimestamp,
isWaiting: tasks.length !== 0 && taskEndingTimestamp < currentTimestamp,
currentTaskFinishTimestamp: taskEndingTimestamp,
currentTaskFinishSeconds: Math.max(
taskEndingTimestamp ? taskEndingTimestamp - currentTimestamp : 0,
0
),
firstTask: makeResourceBalance(firstTaskResources, resources, performance),
allTasks: makeResourceBalance(allTaskResources, resources, performance),
taskCount: tasks.length,
};
}
function createAllProductionQueueStates(
storage: VillageStorage,
taskCollection: VillageTaskCollection
) {
let result: VillageProductionQueueStateDict = {};
for (let taskQueueInfo of taskCollection.getGroupedByQueueTasks()) {
result[taskQueueInfo.queue] = createProductionQueueState(taskQueueInfo, storage);
}
return result;
}
function getReadyForProductionTask(
queues: VillageProductionQueueStateDict,
maxResourcesForTask: Resources
): Task | undefined {
const nowTs = timestamp();
const firstReadyGroup = Object.values(queues).find(
group => group.currentTaskFinishTimestamp <= nowTs && group.tasks.length !== 0
);
if (!firstReadyGroup) {
return undefined;
}
return firstReadyGroup.tasks.find(
t =>
t.name === TrainTroopTask.name ||
!t.args.resources ||
maxResourcesForTask.allGreaterOrEqual(Resources.fromObject(t.args.resources))
);
}
function getReadyTaskRequiredResources(task: Task | undefined): Resources {
if (task && task.args.resources) {
return Resources.fromObject(task.args.resources);
}
return Resources.zero();
}
function createVillageOwnState(
village: Village,
storage: VillageStorage,
taskCollection: VillageTaskCollection
): VillageOwnState {
const settings = storage.getSettings();
const resources = storage.getResources();
const storageResources = Resources.fromStorage(storage.getResourceStorage());
const performance = storage.getResourcesPerformance();
const upperCriticalLevel = storageResources.sub(performance.scale(1));
const storageOptimumFullness = storageResources.sub(performance.scale(3));
const isOverflowing = upperCriticalLevel.anyLower(resources);
const queues = createAllProductionQueueStates(storage, taskCollection);
const firstReadyTask = getReadyForProductionTask(queues, storageOptimumFullness);
const requiredResources = getReadyTaskRequiredResources(firstReadyTask);
const frontierResources = taskCollection.getFrontierTaskResources();
const totalRequiredResources = taskCollection.getAllTasksResources();
return {
id: village.id,
village,
resources,
performance,
storage: makeStorageBalance(resources, storageResources, performance),
required: makeResourceBalance(requiredResources, resources, performance),
upperCriticalLevel,
storageOptimumFullness,
isOverflowing,
queues,
firstReadyTask,
frontierRequired: makeResourceBalance(frontierResources, resources, performance),
totalRequired: makeResourceBalance(totalRequiredResources, resources, performance),
incomingResources: calcIncomingResources(storage),
settings,
};
}
function createVillageOwnStates(
villages: Array<Village>,
storageFactory: VillageStorageFactory,
taskCollectionFactory: VillageTaskCollectionFactory
): VillageOwnStateDictionary {
const result: VillageOwnStateDictionary = {};
for (let village of villages) {
result[village.id] = createVillageOwnState(
village,
storageFactory(village.id),
taskCollectionFactory(village.id)
);
}
return result;
}
function createVillageState(
state: VillageOwnState,
ownStates: VillageOwnStateDictionary
): VillageState {
const villageIds = Object.keys(ownStates).map(k => +k);
const commitments = villageIds.reduce((memo, shipmentVillageId) => {
const shipmentVillageState = ownStates[shipmentVillageId];
const shipmentVillageRequired = shipmentVillageState.required;
const shipmentVillageIncoming = shipmentVillageState.incomingResources;
const targetVillageMissing = shipmentVillageRequired.balance
.add(shipmentVillageIncoming)
.min(Resources.zero());
return memo.add(targetVillageMissing);
}, Resources.zero());
return { ...state, commitments };
}
function getVillageStates(
villages: Array<Village>,
storageFactory: VillageStorageFactory,
taskCollectionFactory: VillageTaskCollectionFactory
): Array<VillageState> {
const ownStates = createVillageOwnStates(villages, storageFactory, taskCollectionFactory);
return villages.map(village => createVillageState(ownStates[village.id], ownStates));
}
interface VillageStorageFactory {
(villageId: number): VillageStorage;
}
interface VillageTaskCollectionFactory {
(villageId: number): VillageTaskCollection;
}
export class VillageStateFactory {
private readonly villageRepository: VillageRepositoryInterface;
private readonly storageFactory: VillageStorageFactory;
private readonly taskCollectionFactory: VillageTaskCollectionFactory;
constructor(
villageRepository: VillageRepositoryInterface,
storageFactory: VillageStorageFactory,
taskCollectionFactory: VillageTaskCollectionFactory
) {
this.villageRepository = villageRepository;
this.storageFactory = storageFactory;
this.taskCollectionFactory = taskCollectionFactory;
}
getAllVillageStates(): Array<VillageState> {
return getVillageStates(
this.villageRepository.all(),
this.storageFactory,
this.taskCollectionFactory
);
}
getVillageState(villageId: number): VillageState {
const states = getVillageStates(
this.villageRepository.all(),
this.storageFactory,
this.taskCollectionFactory
);
const needle = states.find(s => s.id === villageId);
if (!needle) {
throw new VillageNotFound(`Village ${villageId} not found`);
}
return needle;
}
}