Files
basic-computer-games/27_Civil_War/csharp/Army.cs
2022-01-17 08:19:24 +02:00

262 lines
9.5 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace CivilWar
{
public class Army
{
private enum Resource
{
Food,
Salaries,
Ammunition
}
public Army(Side side)
{
Side = side;
}
public Side Side { get; }
// Cumulative
public int Wins { get; private set; } // W, L
public int Losses { get; private set; } // L, W
public int Draws { get; private set; } // W0
public int BattlesFought => Wins + Draws + Losses;
public bool Surrendered { get; private set; } // Y, Y2 == 5
public int CumulativeHistoricCasualties { get; private set; } // P1, P2
public int CumulativeSimulatedCasualties { get; private set; } // T1, T2
public int CumulativeHistoricMen { get; private set; } // M3, M4
private int income; // R1, R2
private int moneySpent; // Q1, Q2
private bool IsFirstBattle => income == 0;
// This battle
private int historicMen; // M1, M2
public int HistoricCasualties { get; private set; }
public int Money { get; private set; } // D(n)
public int Men { get; private set; } // M5, M6
public int Inflation { get; private set; } // I1, I2
public int InflationDisplay => Side == Side.Confederate ? Inflation + 15 : Inflation; // Confederate inflation is shown with 15 added - no idea why!
private readonly Dictionary<Resource, int> allocations = new(); // F(n), H(n), B(n) for food, salaries, ammunition
public int Strategy { get; protected set; } // Y1, Y2
public double Morale => (2.0 * allocations[Resource.Food] * allocations[Resource.Food] + allocations[Resource.Salaries] * allocations[Resource.Salaries]) / (reducedAvailableMen * reducedAvailableMen + 1); // O, O2
public int Casualties { get; protected set; } // C5, C6
public int Desertions { get; protected set; } // E, E2
public int MenLost => Casualties + Desertions;
public bool AllLost { get; private set; } // U, U2
private double reducedAvailableMen; // F1
protected virtual double FractionUnspent => (income - moneySpent) / (income + 1.0);
public void PrepareBattle(int men, int casualties)
{
historicMen = men;
HistoricCasualties = casualties;
Inflation = 10 + (Losses - Wins) * 2;
Money = 100 * (int)(men * (100 - Inflation) / 2000.0 * (1 + FractionUnspent) + 0.5);
Men = (int)(men * 1 + (CumulativeHistoricCasualties - CumulativeSimulatedCasualties) / (CumulativeHistoricMen + 1.0));
reducedAvailableMen = men * 5.0 / 6.0;
}
public virtual void AllocateResources()
{
Console.WriteLine($"{Side} General ---\nHow much do you wish to spend for");
while (true)
{
foreach (Resource resource in Enum.GetValues<Resource>())
{
if (EnterResource(resource))
break;
}
if (allocations.Values.Sum() <= Money)
return;
Console.WriteLine($"Think again! You have only ${Money}");
}
}
private bool EnterResource(Resource resource)
{
while (true)
{
Console.WriteLine($" - {resource}");
switch ((int.TryParse(Console.ReadLine(), out int val), val))
{
case (false, _):
Console.WriteLine("Not a valid number");
break;
case (_, < 0):
Console.WriteLine("Negative values not allowed");
break;
case (_, 0) when IsFirstBattle:
Console.WriteLine("No previous entries");
break;
case (_, 0):
Console.WriteLine("Assume you want to keep same allocations");
return true;
case (_, > 0):
allocations[resource] = val;
return false;
}
}
}
public virtual void DisplayMorale()
{
Console.WriteLine($"{Side} morale is {Morale switch { < 5 => "Poor", < 10 => "Fair", _ => "High" }}");
}
public virtual bool ChooseStrategy(bool isReplay) => EnterStrategy(true, "(1-5)");
protected bool EnterStrategy(bool canSurrender, string hint)
{
Console.WriteLine($"{Side} strategy {hint}");
while (true)
{
switch ((int.TryParse(Console.ReadLine(), out int val), val))
{
case (false, _):
Console.WriteLine("Not a valid number");
break;
case (_, 5) when canSurrender:
Surrendered = true;
Console.WriteLine($"The {Side} general has surrendered");
return true;
case (_, < 1 or >= 5):
Console.WriteLine($"Strategy {val} not allowed.");
break;
default:
Strategy = val;
return false;
}
}
}
public virtual void CalculateLosses(Army opponent)
{
AllLost = false;
int stratFactor = 2 * (Math.Abs(Strategy - opponent.Strategy) + 1);
Casualties = (int)Math.Round(HistoricCasualties * 0.4 * (1 + 1.0 / stratFactor) * (1 + 1 / Morale) * (1.28 + reducedAvailableMen / (allocations[Resource.Ammunition] + 1)));
Desertions = (int)Math.Round(100 / Morale);
// If losses > men present, rescale losses
if (MenLost > Men)
{
Casualties = 13 * Men / 20;
Desertions = Men - Casualties;
AllLost = true;
}
}
public void RecordResult(Side winner)
{
if (winner == Side)
Wins++;
else if (winner == Side.Both)
Draws++;
else
Losses++;
CumulativeSimulatedCasualties += MenLost;
CumulativeHistoricCasualties += HistoricCasualties;
moneySpent += allocations.Values.Sum();
income += historicMen * (100 - Inflation) / 20;
CumulativeHistoricMen += historicMen;
LearnStrategy();
}
protected virtual void LearnStrategy() { }
public void DisplayWarResult(Army opponent)
{
Console.WriteLine("\n\n\n\n");
Console.WriteLine($"The {Side} general has won {Wins} battles and lost {Losses}");
Side winner = (Surrendered, opponent.Surrendered, Wins < Losses) switch
{
(_, true, _) => Side,
(true, _, _) or (_, _, true) => opponent.Side,
_ => Side
};
Console.WriteLine($"The {winner} general has won the war\n");
}
public virtual void DisplayStrategies() { }
}
class ComputerArmy : Army
{
public int[] StrategyProb { get; } = { 25, 25, 25, 25 }; // S(n)
private readonly Random strategyRng = new();
public ComputerArmy(Side side) : base(side) { }
protected override double FractionUnspent => 0.0;
public override void AllocateResources() { }
public override void DisplayMorale() { }
public override bool ChooseStrategy(bool isReplay)
{
if (isReplay)
return EnterStrategy(false, $"(1-4; usually previous {Side} strategy)");
// Basic code comments say "If actual strategy info is in data then r-100 is extra weight given to that strategy" but there's no data or code to do it.
int strategyChosenProb = strategyRng.Next(100); // 0-99
int sumProbs = 0;
for (int i = 0; i < 4; i++)
{
sumProbs += StrategyProb[i];
if (strategyChosenProb < sumProbs)
{
Strategy = i + 1;
break;
}
}
Console.WriteLine($"{Side} strategy is {Strategy}");
return false;
}
protected override void LearnStrategy()
{
// Learn present strategy, start forgetting old ones
// - present strategy gains 3 * s, others lose s probability points, unless a strategy falls below 5 %.
const int s = 3;
int presentGain = 0;
for (int i = 0; i < 4; i++)
{
if (StrategyProb[i] >= 5)
{
StrategyProb[i] -= s;
presentGain += s;
}
}
StrategyProb[Strategy - 1] += presentGain;
}
public override void CalculateLosses(Army opponent)
{
Casualties = (int)(17.0 * HistoricCasualties * opponent.HistoricCasualties / (opponent.Casualties * 20));
Desertions = (int)(5 * opponent.Morale);
}
public override void DisplayStrategies()
{
ConsoleUtils.WriteWordWrap($"\nIntelligence suggests that the {Side} general used strategies 1, 2, 3, 4 in the following percentages:");
Console.WriteLine(string.Join(", ", StrategyProb));
}
}
}