travian/src/VillageState.ts

303 lines
9.4 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, OrderedProductionQueues } from './Core/ProductionQueue';
import { Task } from './Queue/TaskProvider';
import { timestamp } from './utils';
import { VillageTaskCollection } from './VillageTaskCollection';
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;
isActive: boolean;
currentTaskFinishTimestamp: number;
currentTaskFinishSeconds: number;
firstTask: ResourceLineState;
allTasks: ResourceLineState;
taskCount: number;
}
interface VillageOwnState {
/**
* Village id
*/
id: number;
/**
* Village object
*/
village: Village;
/**
* Current village resources
*/
resources: Resources;
performance: Resources;
storage: VillageStorageState;
/**
* 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;
queues: { [queue: string]: VillageProductionQueueState | undefined };
}
interface VillageOwnStateDictionary {
[id: number]: VillageOwnState;
}
export interface VillageState extends VillageOwnState {
/**
* Resource commitments of this village to other (may be negative)
*/
commitments: Resources;
}
function calcResourceBalance(
resources: Resources,
current: Resources,
performance: Resources
): ResourceLineState {
return {
resources,
balance: current.sub(resources),
time: timeToAllResources(current, resources, performance),
active: !resources.empty(),
};
}
function calcStorageBalance(
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(
queue: ProductionQueue,
storage: VillageStorage,
taskCollection: VillageTaskCollection
): VillageProductionQueueState {
const resources = storage.getResources();
const performance = storage.getResourcesPerformance();
const tasks = taskCollection.getTasksInProductionQueue(queue);
const firstTaskResources = tasks.slice(0, 1).reduce(taskResourceReducer, Resources.zero());
const allTaskResources = tasks.reduce(taskResourceReducer, Resources.zero());
const taskEndingTimestamp = storage.getQueueTaskEnding(queue);
return {
queue,
isActive: tasks.length !== 0 || taskEndingTimestamp > timestamp(),
currentTaskFinishTimestamp: taskEndingTimestamp,
currentTaskFinishSeconds: Math.max(
taskEndingTimestamp ? taskEndingTimestamp - timestamp() : 0,
0
),
firstTask: calcResourceBalance(firstTaskResources, resources, performance),
allTasks: calcResourceBalance(allTaskResources, resources, performance),
taskCount: tasks.length,
};
}
function createAllProductionQueueStates(
storage: VillageStorage,
taskCollection: VillageTaskCollection
) {
let result: { [queue: string]: VillageProductionQueueState } = {};
for (let queue of OrderedProductionQueues) {
result[queue] = createProductionQueueState(queue, storage, taskCollection);
}
return result;
}
function createVillageOwnState(
village: Village,
storage: VillageStorage,
taskCollection: VillageTaskCollection
): VillageOwnState {
const resources = storage.getResources();
const resourceStorage = storage.getResourceStorage();
const performance = storage.getResourcesPerformance();
const requiredResources = taskCollection.getReadyTaskRequiredResources();
const frontierResources = taskCollection.getFrontierResources();
const totalRequiredResources = taskCollection.getAllTasksRequiredResources();
return {
id: village.id,
village,
resources,
performance,
storage: calcStorageBalance(resources, Resources.fromStorage(resourceStorage), performance),
required: calcResourceBalance(requiredResources, resources, performance),
frontierRequired: calcResourceBalance(frontierResources, resources, performance),
totalRequired: calcResourceBalance(totalRequiredResources, resources, performance),
incomingResources: calcIncomingResources(storage),
settings: storage.getSettings(),
queues: createAllProductionQueueStates(storage, taskCollection),
};
}
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;
}
}