Some stub build plan calculator code

This commit is contained in:
6d486f49
2026-05-19 17:06:06 -04:00
parent e86cd5c9c8
commit 9be4808b6d
3 changed files with 482 additions and 1 deletions
@@ -0,0 +1,116 @@
@page "/building-calculator"
@using System.Text.Json
@using AOW4.Data
<div class="calculator-page">
<h1>Building Plan Calculator</h1>
<p>Simulates resource income each turn and tracks build completion times for ordered buildings.</p>
<section>
<h2>Build Order</h2>
<table class="calculator-table">
<thead>
<tr>
<th>Turn Requested</th>
<th>Building</th>
<th>Finish Turn</th>
<th>Industry Remaining</th>
</tr>
</thead>
<tbody>
@foreach (var entry in Result.BuildOrder)
{
<tr>
<td>@entry.RequestedTurn</td>
<td>@entry.Name</td>
<td>@(entry.BuiltFinishTurn == 0 ? "Starting" : entry.BuiltFinishTurn.ToString())</td>
<td>@entry.IndustryCostRemaining</td>
</tr>
}
</tbody>
</table>
</section>
<section>
<h2>Gold Over Time</h2>
<table class="calculator-table">
<thead>
<tr>
<th>Turn</th>
<th>Stored Gold</th>
<th>Income</th>
<th>Upkeep</th>
</tr>
</thead>
<tbody>
@foreach (var snapshot in Result.ResourceHistory)
{
<tr>
<td>@snapshot.Turn</td>
<td>@snapshot.Stored.Gold</td>
<td>@snapshot.TotalIncome.Gold</td>
<td>@snapshot.TotalUpkeep.Gold</td>
</tr>
}
</tbody>
</table>
</section>
<section>
<h2>Result JSON</h2>
<pre class="json-output">@Json</pre>
</section>
</div>
@code {
private BuildPlanResult Result = new();
private string Json = string.Empty;
protected override void OnInitialized()
{
Result = BuildingPlanCalculator.CreateSampleBuildPlan(60);
Json = JsonSerializer.Serialize(Result, new JsonSerializerOptions
{
WriteIndented = true
});
}
}
<style>
.calculator-page {
padding: 2rem;
max-width: 1100px;
margin: 0 auto;
}
.calculator-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 1.75rem;
}
.calculator-table th,
.calculator-table td {
padding: 0.75rem 1rem;
border: 1px solid #d3d3d3;
text-align: left;
font-size: 0.95rem;
}
.calculator-table th {
background-color: #f3f6fb;
font-weight: 600;
}
.json-output {
display: block;
white-space: pre-wrap;
background: #1f2937;
color: #f8fafc;
padding: 1rem;
border-radius: 0.5rem;
overflow-x: auto;
font-family: Consolas, "Courier New", monospace;
font-size: 0.9rem;
}
</style>
+360
View File
@@ -0,0 +1,360 @@
using System.Text.Json.Serialization;
namespace AOW4.Data;
public sealed class ResourceAmounts
{
public int Draft { get; set; }
public int Food { get; set; }
public int Knowledge { get; set; }
public int Industry { get; set; }
public int Magic { get; set; }
public int Gold { get; set; }
public int Imperial { get; set; }
public int Stability { get; set; }
[JsonIgnore]
public bool IsZero => Draft == 0 && Food == 0 && Knowledge == 0 && Industry == 0 && Magic == 0 && Gold == 0 && Imperial == 0 && Stability == 0;
public void Add(ResourceAmounts other)
{
Draft += other.Draft;
Food += other.Food;
Knowledge += other.Knowledge;
Industry += other.Industry;
Magic += other.Magic;
Gold += other.Gold;
Imperial += other.Imperial;
Stability += other.Stability;
}
public void Subtract(ResourceAmounts other)
{
Draft -= other.Draft;
Food -= other.Food;
Knowledge -= other.Knowledge;
Industry -= other.Industry;
Magic -= other.Magic;
Gold -= other.Gold;
Imperial -= other.Imperial;
Stability -= other.Stability;
}
public static ResourceAmounts operator +(ResourceAmounts a, ResourceAmounts b)
=> new ResourceAmounts
{
Draft = a.Draft + b.Draft,
Food = a.Food + b.Food,
Knowledge = a.Knowledge + b.Knowledge,
Industry = a.Industry + b.Industry,
Magic = a.Magic + b.Magic,
Gold = a.Gold + b.Gold,
Imperial = a.Imperial + b.Imperial,
Stability = a.Stability + b.Stability
};
public static ResourceAmounts operator -(ResourceAmounts a, ResourceAmounts b)
=> new ResourceAmounts
{
Draft = a.Draft - b.Draft,
Food = a.Food - b.Food,
Knowledge = a.Knowledge - b.Knowledge,
Industry = a.Industry - b.Industry,
Magic = a.Magic - b.Magic,
Gold = a.Gold - b.Gold,
Imperial = a.Imperial - b.Imperial,
Stability = a.Stability - b.Stability
};
}
public sealed class BuildingDefinition
{
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
public string? Image { get; set; }
public string SourceId { get; set; } = "General";
public List<string> RequirementIds { get; set; } = new();
public ResourceAmounts Income { get; set; } = new();
public ResourceAmounts Upkeep { get; set; } = new();
public ResourceAmounts Cost { get; set; } = new();
public int BoostPopulation { get; set; }
public int BoostForester { get; set; }
public int BoostFarm { get; set; }
public int BoostQuarry { get; set; }
public int BoostGoldMine { get; set; }
public int BoostConduit { get; set; }
}
public sealed class BuildOrderEntry
{
public Guid Id { get; set; } = Guid.NewGuid();
public string ItemId { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public int RequestedTurn { get; set; }
public int BuildStartTurn { get; set; }
public int BuiltFinishTurn { get; set; }
public int IndustryCostRemaining { get; set; }
public BuildingDefinition Definition { get; set; } = new();
}
public sealed class ResourceSnapshot
{
public int Turn { get; set; }
public ResourceAmounts Stored { get; set; } = new();
public ResourceAmounts TotalIncome { get; set; } = new();
public ResourceAmounts TotalUpkeep { get; set; } = new();
public string? Notes { get; set; }
}
public sealed class BuildPlanResult
{
public List<BuildOrderEntry> BuildOrder { get; set; } = new();
public List<ResourceSnapshot> ResourceHistory { get; set; } = new();
public ResourceAmounts StartingPool { get; set; } = new();
}
public sealed class BuildOrderRequest
{
public string ItemId { get; init; } = string.Empty;
public BuildingDefinition Definition { get; init; } = new();
public int RequestedTurn { get; init; }
}
public static class BuildingPlanCalculator
{
public static BuildPlanResult CalculateBuildPlan(
int totalTurns,
IEnumerable<BuildOrderRequest> buildRequests,
IEnumerable<BuildingDefinition>? startingBuildings = null,
ResourceAmounts? startingResources = null)
{
var activeBuildings = new List<BuildOrderEntry>();
var result = new BuildPlanResult
{
StartingPool = startingResources ?? new ResourceAmounts()
};
var stored = new ResourceAmounts();
if (startingResources is not null)
{
stored = new ResourceAmounts
{
Draft = startingResources.Draft,
Food = startingResources.Food,
Knowledge = startingResources.Knowledge,
Industry = startingResources.Industry,
Magic = startingResources.Magic,
Gold = startingResources.Gold,
Imperial = startingResources.Imperial,
Stability = startingResources.Stability
};
}
if (startingBuildings is not null)
{
foreach (var startingBuilding in startingBuildings)
{
var entry = new BuildOrderEntry
{
ItemId = startingBuilding.Id,
Name = startingBuilding.Name,
RequestedTurn = 1,
BuildStartTurn = 1,
BuiltFinishTurn = 0,
IndustryCostRemaining = 0,
Definition = startingBuilding
};
activeBuildings.Add(entry);
result.BuildOrder.Add(entry);
}
}
var requestsByTurn = buildRequests
.OrderBy(r => r.RequestedTurn)
.GroupBy(r => r.RequestedTurn)
.ToDictionary(g => g.Key, g => g.ToList());
var projects = new List<BuildOrderEntry>();
for (var turn = 1; turn <= totalTurns; turn++)
{
var incomeThisTurn = new ResourceAmounts();
var upkeepThisTurn = new ResourceAmounts();
foreach (var building in activeBuildings)
{
if (building.BuiltFinishTurn == 0 || turn > building.BuiltFinishTurn)
{
incomeThisTurn.Add(building.Definition.Income);
upkeepThisTurn.Add(building.Definition.Upkeep);
}
}
stored.Add(incomeThisTurn);
stored.Subtract(upkeepThisTurn);
if (requestsByTurn.TryGetValue(turn, out var requests))
{
foreach (var request in requests)
{
var nextProject = new BuildOrderEntry
{
ItemId = request.ItemId,
Name = request.Definition.Name,
RequestedTurn = turn,
BuildStartTurn = turn,
BuiltFinishTurn = 0,
IndustryCostRemaining = request.Definition.Cost.Industry,
Definition = request.Definition
};
stored.Subtract(new ResourceAmounts
{
Draft = request.Definition.Cost.Draft,
Food = request.Definition.Cost.Food,
Knowledge = request.Definition.Cost.Knowledge,
Magic = request.Definition.Cost.Magic,
Gold = request.Definition.Cost.Gold,
Imperial = request.Definition.Cost.Imperial,
Stability = 0
});
projects.Add(nextProject);
result.BuildOrder.Add(nextProject);
}
}
var availableIndustry = incomeThisTurn.Industry;
foreach (var project in projects.Where(p => p.BuiltFinishTurn == 0).OrderBy(p => p.RequestedTurn))
{
if (availableIndustry <= 0 || project.IndustryCostRemaining <= 0)
{
continue;
}
var applied = Math.Min(availableIndustry, project.IndustryCostRemaining);
project.IndustryCostRemaining -= applied;
availableIndustry -= applied;
if (project.IndustryCostRemaining <= 0)
{
project.BuiltFinishTurn = turn;
activeBuildings.Add(project);
}
}
result.ResourceHistory.Add(new ResourceSnapshot
{
Turn = turn,
Stored = new ResourceAmounts
{
Draft = stored.Draft,
Food = stored.Food,
Knowledge = stored.Knowledge,
Industry = stored.Industry,
Magic = stored.Magic,
Gold = stored.Gold,
Imperial = stored.Imperial,
Stability = stored.Stability
},
TotalIncome = incomeThisTurn,
TotalUpkeep = upkeepThisTurn,
Notes = null
});
}
return result;
}
public static IReadOnlyList<BuildingDefinition> GetDefaultStartingBuildings()
{
return new List<BuildingDefinition>
{
new BuildingDefinition
{
Id = "town-hall-1",
Name = "Town Hall I",
Description = "Starting civic center that produces basic resources and morale.",
SourceId = "General",
Income = new ResourceAmounts
{
Draft = 20,
Food = 30,
Industry = 20,
Gold = 0,
Stability = 10
},
Upkeep = new ResourceAmounts(),
Cost = new ResourceAmounts()
},
new BuildingDefinition
{
Id = "throne",
Name = "Throne",
Description = "Royal treasury building that provides steady gold income.",
SourceId = "General",
Income = new ResourceAmounts
{
Gold = 120
},
Upkeep = new ResourceAmounts(),
Cost = new ResourceAmounts()
}
};
}
public static BuildPlanResult CreateSampleBuildPlan(int totalTurns = 60)
{
var sampleRequests = new List<BuildOrderRequest>
{
new BuildOrderRequest
{
ItemId = "farm-1",
Definition = new BuildingDefinition
{
Id = "farm-1",
Name = "Farm",
Description = "Provides food income and supports future growth.",
SourceId = "General",
Income = new ResourceAmounts { Food = 15 },
Upkeep = new ResourceAmounts { Gold = 2 },
Cost = new ResourceAmounts { Gold = 80, Industry = 40 }
},
RequestedTurn = 2
},
new BuildOrderRequest
{
ItemId = "workshop-1",
Definition = new BuildingDefinition
{
Id = "workshop-1",
Name = "Workshop",
Description = "Improves production and generates industry.",
SourceId = "General",
Income = new ResourceAmounts { Industry = 20 },
Upkeep = new ResourceAmounts { Gold = 4 },
Cost = new ResourceAmounts { Gold = 110, Industry = 80 }
},
RequestedTurn = 5
},
new BuildOrderRequest
{
ItemId = "market-1",
Definition = new BuildingDefinition
{
Id = "market-1",
Name = "Market",
Description = "Provides ongoing gold income and supports trade.",
SourceId = "General",
Income = new ResourceAmounts { Gold = 40 },
Upkeep = new ResourceAmounts { Gold = 6 },
Cost = new ResourceAmounts { Gold = 180, Industry = 100 }
},
RequestedTurn = 12
}
};
return CalculateBuildPlan(totalTurns, sampleRequests, GetDefaultStartingBuildings(), new ResourceAmounts { Gold = 0 });
}
}
+6 -1
View File
@@ -12,7 +12,12 @@ public static class SectionsData
Description = "Useful calculator tools for various computations", Description = "Useful calculator tools for various computations",
Links = new List<SectionLink> Links = new List<SectionLink>
{ {
// Add calculator links here in the future new SectionLink
{
Title = "Building Plan Calculator",
Url = "/building-calculator",
Description = "Simulate build order timing and gold income over turns."
}
} }
}, },
new Section new Section