|
|
using System;
|
|
|
using System.Collections.Generic;
|
|
|
using static isometricparkfna.CellMap;
|
|
|
using System.Linq;
|
|
|
|
|
|
using isometricparkfna.UI; //purely for news item
|
|
|
using isometricparkfna.Engines;
|
|
|
|
|
|
using TraceryNet;
|
|
|
|
|
|
namespace isometricparkfna
|
|
|
{
|
|
|
public struct Budget
|
|
|
{
|
|
|
public DateTime DateTime;
|
|
|
|
|
|
//assets
|
|
|
public decimal money;
|
|
|
|
|
|
|
|
|
//revenue
|
|
|
public decimal subsidy;
|
|
|
public decimal contracts;
|
|
|
public decimal misc;
|
|
|
|
|
|
//expenses
|
|
|
public decimal upkeep;
|
|
|
public decimal tree_planting;
|
|
|
public decimal tree_clearing;
|
|
|
public decimal miscellaneous;
|
|
|
public decimal enforcement;
|
|
|
|
|
|
|
|
|
public decimal final_money;
|
|
|
public decimal cashflow;
|
|
|
|
|
|
|
|
|
//misc
|
|
|
public int trees;
|
|
|
public int dead_trees;
|
|
|
public int crowded_trees;
|
|
|
|
|
|
}
|
|
|
|
|
|
public class Simulation
|
|
|
{
|
|
|
public const int START_YEAR = 2020;
|
|
|
public const int START_MONTH = 1;
|
|
|
public const int START_DAY = 1;
|
|
|
|
|
|
public DateTime START_DATETIME {
|
|
|
get {
|
|
|
return new DateTime(START_YEAR, START_MONTH, START_DAY);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
private const float SPONTANEOUS_NEW_TREE_CHANCE = 0.9995f;
|
|
|
private const float NEIGHBOR_NEW_TREE_CHANCE = 0.995f;
|
|
|
private const float NEIGHBOR_CROWDS_TREE_CHANCE = 0.995f;
|
|
|
|
|
|
public const int TREE_PLANT_COST = 500;
|
|
|
public const int TREE_CLEAR_COST = 250;
|
|
|
|
|
|
public const int MAX_TREES_TO_PLANT = 25;
|
|
|
public const int MAX_TREES_TO_CLEAR = 25;
|
|
|
|
|
|
public SimulationBridgeEngine BridgeEngine { get; private set; }
|
|
|
|
|
|
public decimal Subsidy {
|
|
|
get; set;
|
|
|
}
|
|
|
|
|
|
public decimal SubsidyDelta { get; set;}
|
|
|
|
|
|
public int Tick
|
|
|
{
|
|
|
get;
|
|
|
|
|
|
private set;
|
|
|
}
|
|
|
|
|
|
public float Elapsed
|
|
|
{
|
|
|
get;
|
|
|
|
|
|
private set;
|
|
|
}
|
|
|
|
|
|
public DateTime DateTime
|
|
|
{
|
|
|
get;
|
|
|
|
|
|
private set;
|
|
|
}
|
|
|
|
|
|
public decimal money;
|
|
|
|
|
|
private List<Budget> budgets;
|
|
|
|
|
|
public decimal contracts;
|
|
|
public decimal enforcement;
|
|
|
public decimal misc;
|
|
|
|
|
|
public Budget latestBudget
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
if (this.budgets.Count >= 1) {
|
|
|
return this.budgets[this.budgets.Count - 1];
|
|
|
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
return new Budget { };
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public Budget previousBudget
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
if (this.budgets.Count >= 2) {
|
|
|
return this.budgets[this.budgets.Count - 2];
|
|
|
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
return new Budget { };
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public System.Collections.Generic.IEnumerable<Budget> allBudgets()
|
|
|
{
|
|
|
foreach(var budget in this.budgets)
|
|
|
{
|
|
|
yield return budget;
|
|
|
}
|
|
|
|
|
|
}
|
|
|
private Grammar grammar;
|
|
|
private List<NewsItem> sourceNewsItems;
|
|
|
public List<NewsItem> latestNewsItems;
|
|
|
|
|
|
public float[] millisecondsPerAdvance { get; private set; }
|
|
|
public String Season {
|
|
|
get
|
|
|
{
|
|
|
if (MathUtils.BetweenInclusive(this.DateTime.Month, 3, 5))
|
|
|
{
|
|
|
return "Spring";
|
|
|
}
|
|
|
else if (MathUtils.BetweenInclusive(this.DateTime.Month, 6, 8))
|
|
|
{
|
|
|
return "Summer";
|
|
|
}
|
|
|
else if (MathUtils.BetweenInclusive(this.DateTime.Month, 9, 11))
|
|
|
{
|
|
|
return "Fall";
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
return "Winter";
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public CellMap map;
|
|
|
|
|
|
public int ticksPerAdvance;
|
|
|
private float lastAdvance;
|
|
|
public bool paused;
|
|
|
public int currentRate;
|
|
|
|
|
|
private Random random;
|
|
|
|
|
|
//forest policy params
|
|
|
private int _tree_planting;
|
|
|
public int tree_planting
|
|
|
{
|
|
|
get {
|
|
|
return _tree_planting;
|
|
|
}
|
|
|
set {
|
|
|
_tree_planting = MathUtils.Clamp(value, 0, MAX_TREES_TO_PLANT);
|
|
|
}
|
|
|
}
|
|
|
private int _tree_clearing = 0;
|
|
|
public int tree_clearing
|
|
|
{
|
|
|
get {
|
|
|
return _tree_clearing;
|
|
|
}
|
|
|
set {
|
|
|
_tree_clearing = MathUtils.Clamp(value, 0, MAX_TREES_TO_CLEAR);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public int crowded_trees
|
|
|
{
|
|
|
get {
|
|
|
return this.map.iterate_cells_with_neighbors(7).Where(c => c.hasTree).Count();
|
|
|
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public int dead_trees
|
|
|
{
|
|
|
get {
|
|
|
return this.map.iterate_cells().Where(c => (c.status == CellStatus.DeadTree)).Count();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public float healthy_percent
|
|
|
{
|
|
|
get {
|
|
|
return (float)(this.map.tree_count - this.map.iterate_cells_with_neighbors(7).Where(c => c.hasTree).Count()) / this.map.tree_count * 100;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public double average_tree_age
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
return this.map.iterate_cells().Where(c => c.hasTree).Select(c => (this.DateTime - c.planted).Days / 365.0).Average();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public double max_tree_age
|
|
|
{
|
|
|
get
|
|
|
{
|
|
|
return this.map.iterate_cells().Where(c => c.hasTree).Select(c => (this.DateTime - c.planted).Days / 365.0).Max();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
//Historical counts
|
|
|
// public List<int> tree_count
|
|
|
|
|
|
public Simulation(int width, int height, float[] millisecondsPerAdvance)
|
|
|
{
|
|
|
this.random = new Random();
|
|
|
|
|
|
this.DateTime = new DateTime(START_YEAR, START_MONTH, START_DAY);
|
|
|
|
|
|
this.map = new CellMap(width, height);
|
|
|
this.money = 100000M;
|
|
|
this.millisecondsPerAdvance = millisecondsPerAdvance;
|
|
|
|
|
|
this.paused = true;
|
|
|
|
|
|
this.budgets = new List<Budget>();
|
|
|
|
|
|
this.BridgeEngine = new SimulationBridgeEngine(this);
|
|
|
}
|
|
|
|
|
|
private void advanceSimulation()
|
|
|
{
|
|
|
var oldSeason = this.Season;
|
|
|
this.DateTime = this.DateTime.AddMonths(1);
|
|
|
this.Subsidy = Math.Max(0, this.Subsidy + this.SubsidyDelta);
|
|
|
var newSeason = this.Season;
|
|
|
var seasonChanged = oldSeason != newSeason;
|
|
|
|
|
|
this.BridgeEngine.addTick();
|
|
|
|
|
|
foreach (Cell cell in this.map.iterate_cells())
|
|
|
{
|
|
|
if (random.NextDouble() > SPONTANEOUS_NEW_TREE_CHANCE)
|
|
|
{
|
|
|
cell.addTree(this.DateTime);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
int new_planted = 0;
|
|
|
foreach (Cell cell in this.map.iterate_cells_with_neighbors(4))
|
|
|
{
|
|
|
if (random.NextDouble() > NEIGHBOR_NEW_TREE_CHANCE)
|
|
|
{
|
|
|
cell.addTree(this.DateTime);
|
|
|
new_planted += 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
int crowded_out = 0;
|
|
|
foreach (Cell cell in this.map.iterate_cells_with_neighbors(7))
|
|
|
{
|
|
|
if (random.NextDouble() > NEIGHBOR_CROWDS_TREE_CHANCE)
|
|
|
{
|
|
|
cell.markTreeDead();
|
|
|
crowded_out += 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
int trees_to_plant = this.tree_planting;
|
|
|
|
|
|
while (trees_to_plant > 0 && this.map.remaining_tree_capacity > 0) {
|
|
|
int y = random.Next(0, this.map.MapHeight);
|
|
|
int x = random.Next(0, this.map.MapWidth);
|
|
|
Cell chosen_cell = this.map.cells[x][y];
|
|
|
|
|
|
if (!chosen_cell.hasTree) {
|
|
|
chosen_cell.addTree(this.DateTime);
|
|
|
trees_to_plant -= 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
int trees_to_clear = this.tree_clearing;
|
|
|
foreach (Cell cell in this.map.iterate_cells_with_neighbors(7).Where(c => c.hasTree))
|
|
|
{
|
|
|
if (trees_to_clear > 0) {
|
|
|
cell.removeTree();
|
|
|
trees_to_clear -= 1;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
Budget newBudget = new Budget
|
|
|
{
|
|
|
DateTime = this.DateTime,
|
|
|
money = this.money,
|
|
|
trees = this.map.tree_count,
|
|
|
dead_trees = this.dead_trees,
|
|
|
crowded_trees = this.crowded_trees,
|
|
|
subsidy = this.Subsidy,
|
|
|
contracts = this.contracts,
|
|
|
enforcement = this.enforcement,
|
|
|
upkeep = (int)(this.map.tree_count * 1.5),
|
|
|
tree_planting = this.tree_planting * Simulation.TREE_PLANT_COST,
|
|
|
tree_clearing = this.tree_clearing * Simulation.TREE_CLEAR_COST,
|
|
|
misc = this.misc
|
|
|
};
|
|
|
|
|
|
newBudget = this.applyBudget(newBudget); ;
|
|
|
this.budgets.Add(newBudget);
|
|
|
|
|
|
if (seasonChanged) {
|
|
|
this.updateNews();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public Budget applyBudget(Budget budget)
|
|
|
{
|
|
|
|
|
|
this.money = budget.money
|
|
|
- (budget.upkeep + budget.tree_planting + budget.tree_clearing + budget.enforcement)
|
|
|
+ (budget.subsidy + budget.contracts + budget.misc);
|
|
|
|
|
|
|
|
|
budget.final_money = this.money;
|
|
|
budget.cashflow = budget.final_money - budget.money;
|
|
|
|
|
|
return budget;
|
|
|
}
|
|
|
|
|
|
public void setRate(int newRate) {
|
|
|
if ((newRate >= 0) && (newRate <= this.millisecondsPerAdvance.Length)) {
|
|
|
this.currentRate = newRate;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
public void updateNews() {
|
|
|
this.latestNewsItems = this.sourceNewsItems.Select(s => s.Flatten(this.grammar)).ToList().Shuffle();
|
|
|
}
|
|
|
|
|
|
public void LoadContent(List<NewsItem> sourceNewsItems, Grammar grammar) {
|
|
|
this.sourceNewsItems = sourceNewsItems;
|
|
|
this.grammar = grammar;
|
|
|
|
|
|
this.updateNews();
|
|
|
}
|
|
|
|
|
|
public void update(TimeSpan deltaTime)
|
|
|
{
|
|
|
if (!this.paused)
|
|
|
{
|
|
|
this.Elapsed += deltaTime.Milliseconds;
|
|
|
|
|
|
float millisecondsPerAdvance = this.millisecondsPerAdvance[this.currentRate];
|
|
|
|
|
|
int advancesToSimulate = (int)((this.Elapsed - this.lastAdvance) / millisecondsPerAdvance);
|
|
|
|
|
|
for (int i = 0; i < advancesToSimulate; i++)
|
|
|
{
|
|
|
this.advanceSimulation();
|
|
|
|
|
|
//Partial frames haven't been simulated so they're not counted as part of
|
|
|
//lastAdvance
|
|
|
//Example:
|
|
|
//We start at t=100 and simulate every 10 t. If we miss enough updates that
|
|
|
//it's t=125, we have 2.5 steps to simulate. However, we only want to simulate
|
|
|
//whole steps for simplicity's sake, so that means we'll simulate 2. But that means we've only simulated
|
|
|
//through t=120, so that's what we want to track in lastAdvance.
|
|
|
this.lastAdvance += advancesToSimulate * millisecondsPerAdvance;
|
|
|
}
|
|
|
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|