diff --git a/84 Super Star Trek/csharp/Command.cs b/84 Super Star Trek/csharp/Commands/Command.cs similarity index 95% rename from 84 Super Star Trek/csharp/Command.cs rename to 84 Super Star Trek/csharp/Commands/Command.cs index 33d9fd01..f0d569f8 100644 --- a/84 Super Star Trek/csharp/Command.cs +++ b/84 Super Star Trek/csharp/Commands/Command.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace SuperStarTrek +namespace SuperStarTrek.Commands { internal enum Command { diff --git a/84 Super Star Trek/csharp/CommandExtensions.cs b/84 Super Star Trek/csharp/Commands/CommandExtensions.cs similarity index 91% rename from 84 Super Star Trek/csharp/CommandExtensions.cs rename to 84 Super Star Trek/csharp/Commands/CommandExtensions.cs index 2b69ea31..f8f7e9da 100644 --- a/84 Super Star Trek/csharp/CommandExtensions.cs +++ b/84 Super Star Trek/csharp/Commands/CommandExtensions.cs @@ -1,7 +1,7 @@ using System.Reflection; using System.ComponentModel; -namespace SuperStarTrek +namespace SuperStarTrek.Commands { internal static class CommandExtensions { diff --git a/84 Super Star Trek/csharp/Commands/CommandResult.cs b/84 Super Star Trek/csharp/Commands/CommandResult.cs new file mode 100644 index 00000000..1b02780c --- /dev/null +++ b/84 Super Star Trek/csharp/Commands/CommandResult.cs @@ -0,0 +1,23 @@ +namespace SuperStarTrek.Commands +{ + internal class CommandResult + { + public static readonly CommandResult Ok = new(false); + public static readonly CommandResult GameOver = new(true); + + private CommandResult(bool isGameOver) + { + IsGameOver = isGameOver; + } + + private CommandResult(float timeElapsed) + { + TimeElapsed = timeElapsed; + } + + public bool IsGameOver { get; } + public float TimeElapsed { get; } + + public static CommandResult Elapsed(float timeElapsed) => new(timeElapsed); + } +} diff --git a/84 Super Star Trek/csharp/Game.cs b/84 Super Star Trek/csharp/Game.cs index d97b8198..f2e1c819 100644 --- a/84 Super Star Trek/csharp/Game.cs +++ b/84 Super Star Trek/csharp/Game.cs @@ -3,6 +3,7 @@ using SuperStarTrek.Objects; using SuperStarTrek.Resources; using SuperStarTrek.Space; using SuperStarTrek.Systems; +using SuperStarTrek.Systems.ComputerFunctions; using static System.StringComparison; namespace SuperStarTrek @@ -11,12 +12,12 @@ namespace SuperStarTrek { private readonly Output _output; private readonly Input _input; + private readonly Random _random; private int _initialStardate; private int _finalStarDate; - private double _currentStardate; + private float _currentStardate; private Coordinates _currentQuadrant; - private Coordinates _currentSector; private Galaxy _galaxy; private int _initialKlingonCount; private Enterprise _enterprise; @@ -25,19 +26,17 @@ namespace SuperStarTrek { _output = new Output(); _input = new Input(_output); + _random = new Random(); } - public double Stardate => _currentStardate; + public float Stardate => _currentStardate; + public float StardatesRemaining => _finalStarDate - _currentStardate; public void DoIntroduction() { _output.Write(Strings.Title); - _output.Write("Do you need instructions (Y/N)? "); - var response = Console.ReadLine(); - _output.WriteLine(); - - if (!response.Equals("N", InvariantCultureIgnoreCase)) + if (_input.GetYesNo("Do you need instructions", Input.YesNoMode.FalseOnN)) { _output.Write(Strings.Instructions); @@ -47,8 +46,61 @@ namespace SuperStarTrek public void Play() { - var quadrant = Initialise(); + Initialise(); var gameOver = false; + var newQuadrantText = Strings.StartText; + + while (!gameOver) + { + _enterprise.Quadrant.Display(Strings.NowEntering); + + var command = _input.GetCommand(); + + var result = _enterprise.Execute(command); + + gameOver = result.IsGameOver || CheckIfStranded(); + _currentStardate += result.TimeElapsed; + gameOver |= _currentStardate > _finalStarDate; + } + + if (_galaxy.KlingonCount > 0) + { + _output.Write(Strings.EndOfMission, _currentStardate, _galaxy.KlingonCount); + } + else + { + _output.Write(Strings.Congratulations, GetEfficiency()); + } + } + + private void Initialise() + { + _currentStardate = _initialStardate = _random.GetInt(20, 40) * 100; + _finalStarDate = _initialStardate + _random.GetInt(25, 35); + + _currentQuadrant = _random.GetCoordinate(); + + _galaxy = new Galaxy(_random); + _initialKlingonCount = _galaxy.KlingonCount; + + _enterprise = new Enterprise(3000, _random.GetCoordinate(), _output, _random, _input); + _enterprise + .Add(new WarpEngines(_enterprise, _output, _input)) + .Add(new ShortRangeSensors(_enterprise, _galaxy, this, _output)) + .Add(new LongRangeSensors(_galaxy, _output)) + .Add(new PhaserControl(_enterprise, _output, _input, _random)) + .Add(new PhotonTubes(10, _enterprise, _output, _input)) + .Add(new ShieldControl(_enterprise, _output, _input)) + .Add(new DamageControl(_enterprise, _output)) + .Add(new LibraryComputer( + _output, + _input, + new CumulativeGalacticRecord(_output, _galaxy), + new StatusReport(this, _galaxy, _enterprise, _output), + new TorpedoDataCalculator(_enterprise, _output), + new StarbaseDataCalculator(_enterprise, _output), + new DirectionDistanceCalculator(_enterprise, _output, _input), + new GalaxyRegionMap(_output, _galaxy))); _output.Write(Strings.Enterprise); _output.Write( @@ -62,36 +114,21 @@ namespace SuperStarTrek _input.WaitForAnyKeyButEnter("when ready to accept command"); - _enterprise.Enter(quadrant, Strings.StartText); - - while (!gameOver) - { - var command = _input.GetCommand(); - - gameOver = command == Command.XXX || _enterprise.Execute(command); - } + _enterprise.StartIn(BuildCurrentQuadrant()); } - private Quadrant Initialise() - { - var random = new Random(); - - _currentStardate = _initialStardate = random.GetInt(20, 40) * 100; - _finalStarDate = _initialStardate + random.GetInt(25, 35); - - _currentQuadrant = random.GetCoordinate(); - _currentSector = random.GetCoordinate(); - - _galaxy = new Galaxy(); - _initialKlingonCount = _galaxy.KlingonCount; - - _enterprise = new Enterprise(3000, random.GetCoordinate()); - _enterprise.Add(new ShortRangeSensors(_enterprise, _galaxy, this, _output)) - .Add(new ShieldControl(_enterprise, _output, _input)); - - return new Quadrant(_galaxy[_currentQuadrant], _enterprise); - } + private Quadrant BuildCurrentQuadrant() => + new Quadrant(_galaxy[_currentQuadrant], _enterprise, _random, _galaxy, _input, _output); public bool Replay() => _galaxy.StarbaseCount > 0 && _input.GetString(Strings.ReplayPrompt, "Aye"); + + private bool CheckIfStranded() + { + if (_enterprise.IsStranded) { _output.Write(Strings.Stranded); } + return _enterprise.IsStranded; + } + + private float GetEfficiency() => + 1000 * (float)Math.Pow(_initialKlingonCount / (_currentStardate - _initialStardate), 2); } } diff --git a/84 Super Star Trek/csharp/Input.cs b/84 Super Star Trek/csharp/Input.cs index 98e5b297..3011738b 100644 --- a/84 Super Star Trek/csharp/Input.cs +++ b/84 Super Star Trek/csharp/Input.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using SuperStarTrek.Commands; +using SuperStarTrek.Space; using static System.StringComparison; namespace SuperStarTrek @@ -25,14 +27,14 @@ namespace SuperStarTrek return Console.ReadLine(); } - public double GetNumber(string prompt) + public float GetNumber(string prompt) { _output.Prompt(prompt); while (true) { var response = Console.ReadLine(); - if (double.TryParse(response, out var value)) + if (float.TryParse(response, out var value)) { return value; } @@ -42,7 +44,14 @@ namespace SuperStarTrek } } - public bool TryGetNumber(string prompt, double minValue, double maxValue, out double value) + public (float X, float Y) GetCoordinates(string prompt) + { + _output.Prompt($"{prompt} (X,Y)"); + var responses = ReadNumbers(2); + return (responses[0], responses[1]); + } + + public bool TryGetNumber(string prompt, float minValue, float maxValue, out float value) { value = GetNumber($"{prompt} ({minValue}-{maxValue})"); @@ -58,7 +67,7 @@ namespace SuperStarTrek { var response = GetString("Command"); - if (response != "" && + if (response.Length >= 3 && Enum.TryParse(response.Substring(0, 3), ignoreCase: true, out Command parsedCommand)) { return parsedCommand; @@ -72,5 +81,79 @@ namespace SuperStarTrek _output.WriteLine(); } } + + public bool TryGetCourse(string prompt, string officer, out Course course) + { + if (!TryGetNumber(prompt, 1, 9, out var direction)) + { + _output.WriteLine($"{officer} reports, 'Incorrect course data, sir!'"); + course = default; + return false; + } + + course = new Course(direction); + return true; + } + + public bool GetYesNo(string prompt, YesNoMode mode) + { + _output.Prompt($"{prompt} (Y/N)"); + var response = Console.ReadLine().ToUpperInvariant(); + + return (mode, response) switch + { + (YesNoMode.FalseOnN, "N") => false, + (YesNoMode.FalseOnN, _) => true, + (YesNoMode.TrueOnY, "Y") => true, + (YesNoMode.TrueOnY, _) => false, + _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Invalid value") + }; + } + + private float[] ReadNumbers(int quantity) + { + var numbers = new float[quantity]; + var index = 0; + bool tryAgain; + + do + { + tryAgain = false; + var responses = Console.ReadLine().Split(','); + if (responses.Length > quantity) + { + _output.WriteLine("!Extra input ingored"); + } + + for (; index < responses.Length; index++) + { + if (!float.TryParse(responses[index], out numbers[index])) + { + _output.WriteLine("!Number expected - retry input line"); + _output.Prompt(); + tryAgain = true; + break; + } + } + } while (tryAgain); + + if (index < quantity) + { + _output.Prompt("?"); + var responses = ReadNumbers(quantity - index); + for (int i = 0; i < responses.Length; i++, index++) + { + numbers[index] = responses[i]; + } + } + + return numbers; + } + + public enum YesNoMode + { + TrueOnY, + FalseOnN + } } } diff --git a/84 Super Star Trek/csharp/Objects/Enterprise.cs b/84 Super Star Trek/csharp/Objects/Enterprise.cs index 4f6055cc..4adf81df 100644 --- a/84 Super Star Trek/csharp/Objects/Enterprise.cs +++ b/84 Super Star Trek/csharp/Objects/Enterprise.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; +using SuperStarTrek.Commands; using SuperStarTrek.Resources; using SuperStarTrek.Space; using SuperStarTrek.Systems; @@ -10,84 +12,71 @@ namespace SuperStarTrek.Objects internal class Enterprise { private readonly int _maxEnergy; + private readonly Output _output; private readonly List _systems; private readonly Dictionary _commandExecutors; + private readonly Random _random; + private readonly Input _input; private Quadrant _quadrant; - private ShieldControl _shieldControl; - public Enterprise(int maxEnergy, Coordinates sector) + public Enterprise(int maxEnergy, Coordinates sector, Output output, Random random, Input input) { - Sector = sector; + SectorCoordinates = sector; TotalEnergy = _maxEnergy = maxEnergy; _systems = new List(); _commandExecutors = new Dictionary(); + _output = output; + _random = random; + _input = input; } - public Coordinates Quadrant => _quadrant.Coordinates; - public Coordinates Sector { get; } - public string Condition => GetCondition(); - public double Shields => _shieldControl.Energy; - public double Energy => TotalEnergy - Shields; - public double TotalEnergy { get; private set; } - public int TorpedoCount { get; } + public Quadrant Quadrant => _quadrant; + public Coordinates QuadrantCoordinates => _quadrant.Coordinates; + public Coordinates SectorCoordinates { get; private set; } - public bool IsDocked { get; private set; } + public string Condition => GetCondition(); + public LibraryComputer Computer => (LibraryComputer)_commandExecutors[Command.COM]; + public ShieldControl ShieldControl => (ShieldControl)_commandExecutors[Command.SHE]; + public float Energy => TotalEnergy - ShieldControl.ShieldEnergy; + public float TotalEnergy { get; private set; } + public int DamagedSystemCount => _systems.Count(s => s.IsDamaged); + public IEnumerable Systems => _systems; + public PhotonTubes PhotonTubes => (PhotonTubes)_commandExecutors[Command.TOR]; + public bool IsDocked => _quadrant.EnterpriseIsNextToStarbase; + public bool IsStranded => TotalEnergy < 10 || Energy < 10 && ShieldControl.IsDamaged; public Enterprise Add(Subsystem system) { _systems.Add(system); _commandExecutors[system.Command] = system; - if (system is ShieldControl shieldControl) { _shieldControl = shieldControl; } - return this; } - public string GetDamageReport() - { - var report = new StringBuilder(); - report.AppendLine().AppendLine().AppendLine("Device State of Repair"); - foreach (var system in _systems) - { - report.Append(system.Name.PadRight(25)).AppendLine(system.Condition.ToString(" 0.00;-0.00")); - } - report.AppendLine(); - return report.ToString(); - } - - public void Enter(Quadrant quadrant, string entryTextFormat) + public void StartIn(Quadrant quadrant) { _quadrant = quadrant; - - var _output = new Output(); - _output.Write(entryTextFormat, quadrant); - - if (quadrant.HasKlingons) - { - _output.Write(Strings.CombatArea); - if (Shields <= 200) { _output.Write(Strings.LowShields); } - } - - IsDocked = quadrant.EnterpriseIsNextToStarbase; - - Execute(Command.SRS); + quadrant.Display(Strings.StartText); } private string GetCondition() => (_quadrant.HasKlingons, Energy / _maxEnergy) switch { (true, _) => "*Red*", - (_, < 0.1) => "Yellow", + (_, < 0.1f) => "Yellow", _ => "Green" }; - public bool Execute(Command command) + public CommandResult Execute(Command command) { - _commandExecutors[command].ExecuteCommand(_quadrant); - return false; + if (command == Command.XXX) { return CommandResult.GameOver; } + + return _commandExecutors[command].ExecuteCommand(_quadrant); } + public void Refuel() => TotalEnergy = _maxEnergy; + internal bool Recognises(string command) { throw new NotImplementedException(); @@ -99,5 +88,143 @@ namespace SuperStarTrek.Objects } public override string ToString() => "<*>"; + + internal void UseEnergy(float amountUsed) + { + TotalEnergy -= amountUsed; + } + + internal CommandResult TakeHit(Coordinates sector, int hitStrength) + { + _output.WriteLine($"{hitStrength} unit hit on Enterprise from sector {sector}"); + ShieldControl.AbsorbHit(hitStrength); + + if (ShieldControl.ShieldEnergy <= 0) + { + _output.WriteLine(Strings.Destroyed); + return CommandResult.GameOver; + } + + _output.WriteLine($" "); + + if (hitStrength >= 20) + { + TakeDamage(hitStrength); + } + + return CommandResult.Ok; + } + + private void TakeDamage(float hitStrength) + { + var hitShieldRatio = hitStrength / ShieldControl.ShieldEnergy; + if (_random.GetFloat() > 0.6 || hitShieldRatio <= 0.02f) + { + return; + } + + var system = _systems[_random.Get1To8Inclusive() - 1]; + system.TakeDamage(hitShieldRatio + 0.5f * _random.GetFloat()); + _output.WriteLine($"Damage Control reports, '{system.Name} damaged by the hit.'"); + } + + internal void RepairSystems(float repairWorkDone) + { + var repairedSystems = new List(); + + foreach (var system in _systems.Where(s => s.IsDamaged)) + { + if (system.Repair(repairWorkDone)) + { + repairedSystems.Add(system.Name); + } + } + + if (repairedSystems.Any()) + { + _output.WriteLine("Damage Control report:"); + foreach (var systemName in repairedSystems) + { + _output.WriteLine($" {systemName} repair completed."); + } + } + } + + internal void VaryConditionOfRandomSystem() + { + if (_random.GetFloat() > 0.2f) { return; } + + var system = _systems[_random.Get1To8Inclusive() - 1]; + _output.Write($"Damage Control report: {system.Name} "); + if (_random.GetFloat() >= 0.6) + { + system.Repair(_random.GetFloat() * 3 + 1); + _output.WriteLine("state of repair improved"); + } + else + { + system.TakeDamage(_random.GetFloat() * 5 + 1); + _output.WriteLine("damaged"); + } + } + + internal float Move(Course course, float warpFactor, int distance) + { + var (quadrant, sector) = MoveWithinQuadrant(course, distance) ?? MoveBeyondQuadrant(course, distance); + + if (quadrant != _quadrant.Coordinates) + { + _quadrant = new Quadrant(_quadrant.Galaxy[quadrant], this, _random, _quadrant.Galaxy, _input, _output); + } + _quadrant.SetEnterpriseSector(sector); + SectorCoordinates = sector; + + TotalEnergy -= distance + 10; + if (Energy < 0) + { + _output.WriteLine("Shield Control supplies energy to complete the maneuver."); + ShieldControl.ShieldEnergy = Math.Max(0, TotalEnergy); + } + + return GetTimeElapsed(quadrant, warpFactor); + } + + private (Coordinates, Coordinates)? MoveWithinQuadrant(Course course, int distance) + { + var currentSector = SectorCoordinates; + foreach (var (sector, index) in course.GetSectorsFrom(SectorCoordinates).Select((s, i) => (s, i))) + { + if (distance == 0) { break; } + + if (_quadrant.HasObjectAt(sector)) + { + _output.WriteLine($"Warp engines shut down at sector {currentSector} dues to bad navigation"); + distance = 0; + break; + } + + currentSector = sector; + distance -= 1; + } + + return distance == 0 ? (_quadrant.Coordinates, currentSector) : null; + } + + private (Coordinates, Coordinates) MoveBeyondQuadrant(Course course, int distance) + { + var (complete, quadrant, sector) = course.GetDestination(QuadrantCoordinates, SectorCoordinates, distance); + + if (!complete) + { + _output.Write(Strings.PermissionDenied, sector, quadrant); + } + + return (quadrant, sector); + } + + private float GetTimeElapsed(Coordinates finalQuadrant, float warpFactor) => + finalQuadrant == _quadrant.Coordinates + ? Math.Min(1, (float)Math.Round(warpFactor, 1, MidpointRounding.ToZero)) + : 1; } } diff --git a/84 Super Star Trek/csharp/Objects/Klingon.cs b/84 Super Star Trek/csharp/Objects/Klingon.cs index fdf47031..4e277b07 100644 --- a/84 Super Star Trek/csharp/Objects/Klingon.cs +++ b/84 Super Star Trek/csharp/Objects/Klingon.cs @@ -1,14 +1,42 @@ +using SuperStarTrek.Commands; +using SuperStarTrek.Space; + namespace SuperStarTrek.Objects { internal class Klingon { - private double _energy; + private readonly Random _random; - public Klingon() + public Klingon(Coordinates sector, Random random) { - _energy = new Random().GetDouble(100, 300); + Sector = sector; + _random = random; + Energy = _random.GetFloat(100, 300); } + public float Energy { get; private set; } + public Coordinates Sector { get; private set; } + public override string ToString() => "+K+"; + + public CommandResult FireOn(Enterprise enterprise) + { + var attackStrength = _random.GetFloat(); + var distanceToEnterprise = Sector.GetDistanceTo(enterprise.SectorCoordinates); + var hitStrength = (int)(Energy * (2 + attackStrength) / distanceToEnterprise); + Energy /= 3 + attackStrength; + + return enterprise.TakeHit(Sector, hitStrength); + } + + internal bool TakeHit(int hitStrength) + { + if (hitStrength < 0.15 * Energy) { return false; } + + Energy -= hitStrength; + return true; + } + + internal void MoveTo(Coordinates newSector) => Sector = newSector; } } diff --git a/84 Super Star Trek/csharp/Objects/Starbase.cs b/84 Super Star Trek/csharp/Objects/Starbase.cs index 33110837..4befa73a 100644 --- a/84 Super Star Trek/csharp/Objects/Starbase.cs +++ b/84 Super Star Trek/csharp/Objects/Starbase.cs @@ -1,7 +1,44 @@ +using SuperStarTrek.Resources; +using SuperStarTrek.Space; + namespace SuperStarTrek.Objects { internal class Starbase { + private readonly Input _input; + private readonly Output _output; + private readonly float _repairDelay; + + public Starbase(Coordinates sector, Random random, Input input, Output output) + { + Sector = sector; + _repairDelay = random.GetFloat() * 0.5f; + _input = input; + _output = output; + } + + internal Coordinates Sector { get; } public override string ToString() => ">!<"; + + internal bool TryRepair(Enterprise enterprise, out float repairTime) + { + repairTime = enterprise.DamagedSystemCount * 0.1f + _repairDelay; + if (repairTime >= 1) { repairTime = 0.9f; } + + _output.Write(Strings.RepairEstimate, repairTime); + if (_input.GetYesNo(Strings.RepairPrompt, Input.YesNoMode.TrueOnY)) + { + foreach (var system in enterprise.Systems) + { + system.Repair(); + } + return true; + } + + repairTime = 0; + return false; + } + + internal void ProtectEnterprise() => _output.WriteLine(Strings.Protected); } } diff --git a/84 Super Star Trek/csharp/Output.cs b/84 Super Star Trek/csharp/Output.cs index 77e5e919..ed960eea 100644 --- a/84 Super Star Trek/csharp/Output.cs +++ b/84 Super Star Trek/csharp/Output.cs @@ -4,12 +4,37 @@ namespace SuperStarTrek { internal class Output { - public void Write(string text) => Console.Write(text); - public void Write(string format, params object[] args) => Console.Write(format, args); - public void WriteLine(string text = "") => Console.WriteLine(text); + public Output Write(string text) + { + Console.Write(text); + return this; + } - public void NextLine() => Console.WriteLine(); + public Output Write(string format, params object[] args) + { + Console.Write(format, args); + return this; + } + + public Output WriteLine(string text = "") + { + Console.WriteLine(text); + return this; + } + + + public Output NextLine() + { + Console.WriteLine(); + return this; + } + + + public Output Prompt(string text = "") + { + Console.Write($"{text}? "); + return this; + } - public void Prompt(string text = "") => Console.Write($"{text}? "); } } diff --git a/84 Super Star Trek/csharp/Program.cs b/84 Super Star Trek/csharp/Program.cs index 30360ee0..7f8395a6 100644 --- a/84 Super Star Trek/csharp/Program.cs +++ b/84 Super Star Trek/csharp/Program.cs @@ -29,6 +29,7 @@ namespace SuperStarTrek { static void Main() { + var foo = Utils.DirectionAndDistance.From(1,1).To(4,5); var game = new Game(); game.DoIntroduction(); diff --git a/84 Super Star Trek/csharp/Random.cs b/84 Super Star Trek/csharp/Random.cs index 9b7e1bff..5ed47805 100644 --- a/84 Super Star Trek/csharp/Random.cs +++ b/84 Super Star Trek/csharp/Random.cs @@ -6,20 +6,20 @@ namespace SuperStarTrek { private static readonly System.Random _random = new(); - public Coordinates GetCoordinate() => new Coordinates(Get1To8Inclusive(), Get1To8Inclusive()); + public Coordinates GetCoordinate() => new Coordinates(Get1To8Inclusive() - 1, Get1To8Inclusive() - 1); // Duplicates the algorithm used in the original code to get an integer value from 1 to 8, inclusive: // 475 DEF FNR(R)=INT(RND(R)*7.98+1.01) // Returns a value from 1 to 8, inclusive. // Note there's a slight bias away from the extreme values, 1 and 8. - public int Get1To8Inclusive() => (int)(_random.NextDouble() * 7.98 + 1.01); + public int Get1To8Inclusive() => (int)(GetFloat() * 7.98 + 1.01); public int GetInt(int inclusiveMinValue, int exclusiveMaxValue) => _random.Next(inclusiveMinValue, exclusiveMaxValue); - public double GetDouble() => _random.NextDouble(); + public float GetFloat() => (float)_random.NextDouble(); - public double GetDouble(double inclusiveMinValue, double exclusiveMaxValue) - => _random.NextDouble() * (exclusiveMaxValue - inclusiveMinValue) + inclusiveMinValue; + public float GetFloat(float inclusiveMinValue, float exclusiveMaxValue) + => GetFloat() * (exclusiveMaxValue - inclusiveMinValue) + inclusiveMinValue; } } diff --git a/84 Super Star Trek/csharp/Resources/AcceptCommand.txt b/84 Super Star Trek/csharp/Resources/AcceptCommand.txt deleted file mode 100644 index e69de29b..00000000 diff --git a/84 Super Star Trek/csharp/Resources/ComputerFunctions.txt b/84 Super Star Trek/csharp/Resources/ComputerFunctions.txt new file mode 100644 index 00000000..4827b0fe --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/ComputerFunctions.txt @@ -0,0 +1,9 @@ + +Functions available from Library-Computer: + 0 = Cumulative galactic record + 1 = Status report + 2 = Photon torpedo data + 3 = Starbase nav data + 4 = Direction/distance calculator + 5 = Galaxy 'region name' map + diff --git a/84 Super Star Trek/csharp/Resources/Congratulations.txt b/84 Super Star Trek/csharp/Resources/Congratulations.txt new file mode 100644 index 00000000..767f8653 --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/Congratulations.txt @@ -0,0 +1,4 @@ +Congratulations, Captain! The last Klingon battle cruiser +menacing the Federation has been destroyed. + +Your efficiency rating is {0}. diff --git a/84 Super Star Trek/csharp/Resources/CourtMartial.txt b/84 Super Star Trek/csharp/Resources/CourtMartial.txt new file mode 100644 index 00000000..7d05a5b8 --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/CourtMartial.txt @@ -0,0 +1,3 @@ + +Starfleet Command reviewing your record to consider +court martial! \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Resources/Destroyed.txt b/84 Super Star Trek/csharp/Resources/Destroyed.txt new file mode 100644 index 00000000..881d7b49 --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/Destroyed.txt @@ -0,0 +1 @@ +The Enterprise has been destroyed. The Federation will be conquered. diff --git a/84 Super Star Trek/csharp/Resources/EndOfMission.txt b/84 Super Star Trek/csharp/Resources/EndOfMission.txt new file mode 100644 index 00000000..f6995e31 --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/EndOfMission.txt @@ -0,0 +1,3 @@ +Is is stardate {0}. +There were {1} Klingon battle cruisers left at +the end of your mission. diff --git a/84 Super Star Trek/csharp/Resources/Instructions.txt b/84 Super Star Trek/csharp/Resources/Instructions.txt index fd74857e..34b4169c 100644 --- a/84 Super Star Trek/csharp/Resources/Instructions.txt +++ b/84 Super Star Trek/csharp/Resources/Instructions.txt @@ -1,3 +1,4 @@ + INSTRUCTIONS FOR 'SUPER STAR TREK' 1. When you see "Command ?" printed, enter one of the legal diff --git a/84 Super Star Trek/csharp/Resources/NoEnemyShips.txt b/84 Super Star Trek/csharp/Resources/NoEnemyShips.txt new file mode 100644 index 00000000..394f1057 --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/NoEnemyShips.txt @@ -0,0 +1,2 @@ +Science Officer Spock reports, 'Sensors show no enemy ships + in this quadrant' \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Resources/NoStarbase.txt b/84 Super Star Trek/csharp/Resources/NoStarbase.txt new file mode 100644 index 00000000..5bb4e5fb --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/NoStarbase.txt @@ -0,0 +1 @@ +Mr. Spock reports, 'Sensors show no starbases in this quadrant.' \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Resources/NowEntering.txt b/84 Super Star Trek/csharp/Resources/NowEntering.txt new file mode 100644 index 00000000..915b526f --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/NowEntering.txt @@ -0,0 +1,2 @@ +Now entering {0} quadrant . . . + diff --git a/84 Super Star Trek/csharp/Resources/PermissionDenied.txt b/84 Super Star Trek/csharp/Resources/PermissionDenied.txt new file mode 100644 index 00000000..c24d9da7 --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/PermissionDenied.txt @@ -0,0 +1,5 @@ +Lt. Uhura report message from Starfleet Command: + 'Permission to attempt crossing of galactic perimeter + is hereby *Denied*. Shut down your engines.' +Chief Engineer Scott reports, 'Warp engines shut down + at sector {0} of quadrant {1}.' diff --git a/84 Super Star Trek/csharp/Resources/Protected.txt b/84 Super Star Trek/csharp/Resources/Protected.txt new file mode 100644 index 00000000..27c4a5f8 --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/Protected.txt @@ -0,0 +1 @@ +Starbase shields protect the Enterprise \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Resources/RegionNames.txt b/84 Super Star Trek/csharp/Resources/RegionNames.txt new file mode 100644 index 00000000..f84fe43b --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/RegionNames.txt @@ -0,0 +1,8 @@ + Antares Sirius + Rigel Deneb + Procyon Capella + Vega Betelgeuse + Canopus Aldebaran + Altair Regulus + Sagittarius Arcturus + Pollux Spica \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Resources/RelievedOfCommand.txt b/84 Super Star Trek/csharp/Resources/RelievedOfCommand.txt new file mode 100644 index 00000000..8086e3ca --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/RelievedOfCommand.txt @@ -0,0 +1,3 @@ + +That does it, Captain!! You are hereby relieved of command +and sentenced to 99 stardates at hard labor on Cygnus 12!! \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Resources/RepairEstimate.txt b/84 Super Star Trek/csharp/Resources/RepairEstimate.txt new file mode 100644 index 00000000..12e167a5 --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/RepairEstimate.txt @@ -0,0 +1,2 @@ +Technicians standing by to effect repairs to your ship; +Estimated time to repair: {0} stardates. diff --git a/84 Super Star Trek/csharp/Resources/RepairPrompt.txt b/84 Super Star Trek/csharp/Resources/RepairPrompt.txt new file mode 100644 index 00000000..feffdb27 --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/RepairPrompt.txt @@ -0,0 +1 @@ +Will you authorize the repair order (Y/N) \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Resources/ReplayPrompt.txt b/84 Super Star Trek/csharp/Resources/ReplayPrompt.txt index 52aca0d1..3ca3a102 100644 --- a/84 Super Star Trek/csharp/Resources/ReplayPrompt.txt +++ b/84 Super Star Trek/csharp/Resources/ReplayPrompt.txt @@ -1,3 +1,5 @@ + + The Federation is in need of a new starship commander for a similar mission -- if there is a volunteer let him step forward and enter 'Aye' \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Resources/Stranded.txt b/84 Super Star Trek/csharp/Resources/Stranded.txt new file mode 100644 index 00000000..07c37952 --- /dev/null +++ b/84 Super Star Trek/csharp/Resources/Stranded.txt @@ -0,0 +1,3 @@ +** FATAL ERROR ** You've just stranded your ship in space +You have insufficient maneuvering energy, and shield control +is presently incapable of cross-circuiting to engine room!! diff --git a/84 Super Star Trek/csharp/Resources/Strings.cs b/84 Super Star Trek/csharp/Resources/Strings.cs index 930f1663..ddb768ca 100644 --- a/84 Super Star Trek/csharp/Resources/Strings.cs +++ b/84 Super Star Trek/csharp/Resources/Strings.cs @@ -7,14 +7,29 @@ namespace SuperStarTrek.Resources internal static class Strings { public static string CombatArea => GetResource(); + public static string ComputerFunctions => GetResource(); + public static string Congratulations => GetResource(); + public static string CourtMartial => GetResource(); + public static string Destroyed => GetResource(); + public static string EndOfMission => GetResource(); public static string Enterprise => GetResource(); public static string Instructions => GetResource(); public static string LowShields => GetResource(); + public static string NoEnemyShips => GetResource(); + public static string NoStarbase => GetResource(); + public static string NowEntering => GetResource(); public static string Orders => GetResource(); + public static string PermissionDenied => GetResource(); + public static string Protected => GetResource(); + public static string RegionNames => GetResource(); + public static string RelievedOfCommand => GetResource(); + public static string RepairEstimate => GetResource(); + public static string RepairPrompt => GetResource(); public static string ReplayPrompt => GetResource(); public static string ShieldsDropped => GetResource(); public static string ShortRangeSensorsOut => GetResource(); public static string StartText => GetResource(); + public static string Stranded => GetResource(); public static string Title => GetResource(); private static string GetResource([CallerMemberName] string name = "") diff --git a/84 Super Star Trek/csharp/Space/Coordinate.cs b/84 Super Star Trek/csharp/Space/Coordinate.cs deleted file mode 100644 index b3328c24..00000000 --- a/84 Super Star Trek/csharp/Space/Coordinate.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; - -namespace SuperStarTrek.Space -{ - // Represents the corrdintate of a quadrant in the galaxy, or a sector in a quadrant. - // Note that the origin is top-left, x increase downwards, and y increases to the right. - internal record Coordinates - { - public Coordinates(int x, int y) - { - X = Validated(x, nameof(x)); - Y = Validated(y, nameof(y)); - } - - public int X { get; } - public int Y { get; } - - private int Validated(int value, string argumentName) - { - if (value >= 1 && value <= 8) { return value; } - - throw new ArgumentOutOfRangeException(argumentName, value, "Must be 1 to 8 inclusive"); - } - - public override string ToString() => $"{X} , {Y}"; - } -} diff --git a/84 Super Star Trek/csharp/Space/Coordinates.cs b/84 Super Star Trek/csharp/Space/Coordinates.cs new file mode 100644 index 00000000..1619d387 --- /dev/null +++ b/84 Super Star Trek/csharp/Space/Coordinates.cs @@ -0,0 +1,67 @@ +using System; +using SuperStarTrek.Utils; + +namespace SuperStarTrek.Space +{ + // Represents the corrdintate of a quadrant in the galaxy, or a sector in a quadrant. + // Note that the origin is top-left, x increase downwards, and y increases to the right. + internal record Coordinates + { + public Coordinates(int x, int y) + { + X = Validated(x, nameof(x)); + Y = Validated(y, nameof(y)); + + RegionIndex = (X << 1) + (Y >> 2); + SubRegionIndex = Y % 4; + } + + public int X { get; } + public int Y { get; } + public int RegionIndex { get; } + public int SubRegionIndex { get; } + + private int Validated(int value, string argumentName) + { + if (value >= 0 && value <= 7) { return value; } + + throw new ArgumentOutOfRangeException(argumentName, value, "Must be 0 to 7 inclusive"); + } + + private static bool IsValid(int value) => value >= 0 && value <= 7; + + public override string ToString() => $"{X+1} , {Y+1}"; + + internal void Deconstruct(out int x, out int y) + { + x = X; + y = Y; + } + + internal static bool TryCreate(float x, float y, out Coordinates coordinates) + { + var roundedX = Round(x); + var roundedY = Round(y); + + if (IsValid(roundedX) && IsValid(roundedY)) + { + coordinates = new Coordinates(roundedX, roundedY); + return true; + } + + coordinates = default; + return false; + + static int Round(float value) => (int)Math.Round(value, MidpointRounding.AwayFromZero); + } + + internal (float Direction, float Distance) GetDirectionAndDistanceTo(Coordinates destination) => + DirectionAndDistance.From(this).To(destination); + + internal float GetDistanceTo(Coordinates destination) + { + var (_, distance) = GetDirectionAndDistanceTo(destination); + return distance; + } + } +} diff --git a/84 Super Star Trek/csharp/Space/Course.cs b/84 Super Star Trek/csharp/Space/Course.cs index dd9ecfba..a46385c3 100644 --- a/84 Super Star Trek/csharp/Space/Course.cs +++ b/84 Super Star Trek/csharp/Space/Course.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace SuperStarTrek.Space { @@ -24,7 +25,7 @@ namespace SuperStarTrek.Space (0, 1) }; - public Course(double direction) + internal Course(float direction) { if (direction < 1 || direction > 9) { @@ -44,7 +45,53 @@ namespace SuperStarTrek.Space DeltaY = baseCardinal.DeltaY + (nextCardinal.DeltaY - baseCardinal.DeltaY) * fractionalDirection; } - public double DeltaX { get; } - public double DeltaY { get; } + internal float DeltaX { get; } + internal float DeltaY { get; } + + internal IEnumerable GetSectorsFrom(Coordinates start) + { + (float x, float y) = start; + + while(true) + { + x += DeltaX; + y += DeltaY; + + if (!Coordinates.TryCreate(x, y, out var coordinates)) + { + yield break; + } + + yield return coordinates; + } + } + + internal (bool, Coordinates, Coordinates) GetDestination(Coordinates quadrant, Coordinates sector, int distance) + { + var (xComplete, quadrantX, sectorX) = GetNewCoordinate(quadrant.X, sector.X, DeltaX * distance); + var (yComplete, quadrantY, sectorY) = GetNewCoordinate(quadrant.Y, sector.Y, DeltaY * distance); + + return (xComplete && yComplete, new Coordinates(quadrantX, quadrantY), new Coordinates(sectorX, sectorY)); + } + + private (bool, int, int) GetNewCoordinate(int quadrant, int sector, float sectorsTravelled) + { + var galacticCoordinate = quadrant * 8 + sector + sectorsTravelled; + var newQuadrant = (int)(galacticCoordinate / 8); + var newSector = (int)(galacticCoordinate - newQuadrant * 8); + + if (newSector == -1) + { + newQuadrant -= 1; + newSector = 7; + } + + return newQuadrant switch + { + < 0 => (false, 0, 0), + > 7 => (false, 7, 7), + _ => (true, newQuadrant, newSector) + }; + } } } diff --git a/84 Super Star Trek/csharp/Space/Galaxy.cs b/84 Super Star Trek/csharp/Space/Galaxy.cs index 171ecdea..63838567 100644 --- a/84 Super Star Trek/csharp/Space/Galaxy.cs +++ b/84 Super Star Trek/csharp/Space/Galaxy.cs @@ -1,17 +1,37 @@ +using System.Collections; +using System.Collections.Generic; using System.Linq; +using SuperStarTrek.Objects; +using SuperStarTrek.Resources; + +using static System.StringSplitOptions; namespace SuperStarTrek.Space { internal class Galaxy { + private static readonly string[] _regionNames; + private static readonly string[] _subRegionIdentifiers; private readonly QuadrantInfo[][] _quadrants; + private readonly Random _random; - public Galaxy() + static Galaxy() { - var random = new Random(); + _regionNames = Strings.RegionNames.Split(new[] { ' ', '\n' }, RemoveEmptyEntries | TrimEntries); + _subRegionIdentifiers = new[] { "I", "II", "III", "IV" }; + } - _quadrants = Enumerable.Range(1, 8).Select(x => - Enumerable.Range(1, 8).Select(y => QuadrantInfo.Create(new Coordinates(x, y), "")).ToArray()) + public Galaxy(Random random) + { + _random = random; + + _quadrants = Enumerable + .Range(0, 8) + .Select(x => Enumerable + .Range(0, 8) + .Select(y => new Coordinates(x, y)) + .Select(c => QuadrantInfo.Create(c, GetQuadrantName(c))) + .ToArray()) .ToArray(); if (StarbaseCount == 0) @@ -26,9 +46,22 @@ namespace SuperStarTrek.Space } } - public QuadrantInfo this[Coordinates coordinate] => _quadrants[coordinate.X - 1][coordinate.Y - 1]; + public QuadrantInfo this[Coordinates coordinate] => _quadrants[coordinate.X][coordinate.Y]; public int KlingonCount => _quadrants.SelectMany(q => q).Sum(q => q.KlingonCount); public int StarbaseCount => _quadrants.SelectMany(q => q).Count(q => q.HasStarbase); + public IEnumerable> Quadrants => _quadrants; + + private static string GetQuadrantName(Coordinates coordinates) => + $"{_regionNames[coordinates.RegionIndex]} {_subRegionIdentifiers[coordinates.SubRegionIndex]}"; + + public IEnumerable> GetNeighborhood(Quadrant quadrant) => + Enumerable.Range(-1, 3) + .Select(dx => dx + quadrant.Coordinates.X) + .Select(x => GetNeighborhoodRow(quadrant, x)); + private IEnumerable GetNeighborhoodRow(Quadrant quadrant, int x) => + Enumerable.Range(-1, 3) + .Select(dy => dy + quadrant.Coordinates.Y) + .Select(y => y < 0 || y > 7 || x < 0 || x > 7 ? null : _quadrants[x][y]); } } diff --git a/84 Super Star Trek/csharp/Space/Quadrant.cs b/84 Super Star Trek/csharp/Space/Quadrant.cs index 2aa45b6d..b333cb7e 100644 --- a/84 Super Star Trek/csharp/Space/Quadrant.cs +++ b/84 Super Star Trek/csharp/Space/Quadrant.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; +using SuperStarTrek.Commands; using SuperStarTrek.Objects; +using SuperStarTrek.Resources; namespace SuperStarTrek.Space { @@ -10,42 +12,56 @@ namespace SuperStarTrek.Space private readonly QuadrantInfo _info; private readonly Random _random; private readonly Dictionary _sectors; - private readonly Coordinates _enterpriseSector; - private readonly Coordinates _starbaseSector; + private readonly Enterprise _enterprise; + private readonly Output _output; + private bool _displayed = false; - public Quadrant(QuadrantInfo info, Enterprise enterprise) + public Quadrant( + QuadrantInfo info, + Enterprise enterprise, + Random random, + Galaxy galaxy, + Input input, + Output output) { _info = info; - _random = new Random(); + _random = random; + _output = output; + Galaxy = galaxy; - _enterpriseSector = enterprise.Sector; - _sectors = new Dictionary { [enterprise.Sector] = enterprise }; - PositionObject(() => new Klingon(), _info.KlingonCount); + info.MarkAsKnown(); + _sectors = new() { [enterprise.SectorCoordinates] = _enterprise = enterprise }; + PositionObject(sector => new Klingon(sector, _random), _info.KlingonCount); if (_info.HasStarbase) { - _starbaseSector = PositionObject(() => new Starbase()); + Starbase = PositionObject(sector => new Starbase(sector, _random, input, output)); } - PositionObject(() => new Star(), _info.StarCount); + PositionObject(_ => new Star(), _info.StarCount); } public Coordinates Coordinates => _info.Coordinates; public bool HasKlingons => _info.KlingonCount > 0; + public int KlingonCount => _info.KlingonCount; public bool HasStarbase => _info.HasStarbase; + public Starbase Starbase { get; } + internal Galaxy Galaxy { get; } public bool EnterpriseIsNextToStarbase => _info.HasStarbase && - Math.Abs(_enterpriseSector.X - _starbaseSector.X) <= 1 && - Math.Abs(_enterpriseSector.Y - _starbaseSector.Y) <= 1; + Math.Abs(_enterprise.SectorCoordinates.X - Starbase.Sector.X) <= 1 && + Math.Abs(_enterprise.SectorCoordinates.Y - Starbase.Sector.Y) <= 1; + + internal IEnumerable Klingons => _sectors.Values.OfType(); public override string ToString() => _info.Name; - private Coordinates PositionObject(Func objectFactory) + private T PositionObject(Func objectFactory) { var sector = GetRandomEmptySector(); - _sectors[sector] = objectFactory.Invoke(); - return sector; + _sectors[sector] = objectFactory.Invoke(sector); + return (T)_sectors[sector]; } - private void PositionObject(Func objectFactory, int count) + private void PositionObject(Func objectFactory, int count) { for (int i = 0; i < count; i++) { @@ -53,6 +69,91 @@ namespace SuperStarTrek.Space } } + internal void Display(string textFormat) + { + if (_displayed) { return; } + + _output.Write(textFormat, this); + + if (_info.KlingonCount > 0) + { + _output.Write(Strings.CombatArea); + if (_enterprise.ShieldControl.ShieldEnergy <= 200) { _output.Write(Strings.LowShields); } + } + + _enterprise.Execute(Command.SRS); + + _displayed = true; + } + + internal bool HasObjectAt(Coordinates coordinates) => _sectors.ContainsKey(coordinates); + + internal bool TorpedoCollisionAt(Coordinates coordinates, out string message, out bool gameOver) + { + gameOver = false; + message = default; + + switch (_sectors.GetValueOrDefault(coordinates)) + { + case Klingon klingon: + message = Remove(klingon); + gameOver = Galaxy.KlingonCount == 0; + return true; + + case Star _: + message = $"Star at {coordinates} absorbed torpedo energy."; + return true; + + case Starbase _: + _sectors.Remove(coordinates); + _info.RemoveStarbase(); + message = "*** Starbase destroyed ***" + + (Galaxy.StarbaseCount > 0 ? Strings.CourtMartial : Strings.RelievedOfCommand); + gameOver = Galaxy.StarbaseCount == 0; + return true; + + default: + return false; + } + } + + internal string Remove(Klingon klingon) + { + _sectors.Remove(klingon.Sector); + _info.RemoveKlingon(); + return "*** Klingon destroyed ***"; + } + + internal CommandResult KlingonsMoveAndFire() + { + foreach (var klingon in Klingons.ToList()) + { + var newSector = GetRandomEmptySector(); + _sectors.Remove(klingon.Sector); + _sectors[newSector] = klingon; + klingon.MoveTo(newSector); + } + + return KlingonsFireOnEnterprise(); + } + + internal CommandResult KlingonsFireOnEnterprise() + { + if (EnterpriseIsNextToStarbase && Klingons.Any()) + { + Starbase.ProtectEnterprise(); + return CommandResult.Ok; + } + + foreach (var klingon in Klingons) + { + var result = klingon.FireOn(_enterprise); + if (result.IsGameOver) { return result; } + } + + return CommandResult.Ok; + } + private Coordinates GetRandomEmptySector() { while (true) @@ -65,15 +166,21 @@ namespace SuperStarTrek.Space } } - public IEnumerable GetDisplayLines() => Enumerable.Range(1, 8).Select(x => GetDisplayLine(x)); + public IEnumerable GetDisplayLines() => Enumerable.Range(0, 8).Select(x => GetDisplayLine(x)); private string GetDisplayLine(int x) => string.Join( " ", Enumerable - .Range(1, 8) + .Range(0, 8) .Select(y => new Coordinates(x, y)) .Select(c => _sectors.GetValueOrDefault(c)) .Select(o => o?.ToString() ?? " ")); + + internal void SetEnterpriseSector(Coordinates sector) + { + _sectors.Remove(_enterprise.SectorCoordinates); + _sectors[sector] = _enterprise; + } } } diff --git a/84 Super Star Trek/csharp/Space/QuadrantInfo.cs b/84 Super Star Trek/csharp/Space/QuadrantInfo.cs index ed9ed3fa..70447250 100644 --- a/84 Super Star Trek/csharp/Space/QuadrantInfo.cs +++ b/84 Super Star Trek/csharp/Space/QuadrantInfo.cs @@ -1,7 +1,11 @@ +using SuperStarTrek.Objects; + namespace SuperStarTrek.Space { internal class QuadrantInfo { + private bool _isKnown; + private QuadrantInfo(Coordinates coordinates, string name, int klingonCount, int starCount, bool hasStarbase) { Coordinates = coordinates; @@ -20,14 +24,14 @@ namespace SuperStarTrek.Space public static QuadrantInfo Create(Coordinates coordinates, string name) { var random = new Random(); - var klingonCount = random.GetDouble() switch + var klingonCount = random.GetFloat() switch { - > 0.98 => 3, - > 0.95 => 2, - > 0.80 => 1, + > 0.98f => 3, + > 0.95f => 2, + > 0.80f => 1, _ => 0 }; - var hasStarbase = random.GetDouble() > 0.96; + var hasStarbase = random.GetFloat() > 0.96f; var starCount = random.Get1To8Inclusive(); return new QuadrantInfo(coordinates, name, klingonCount, starCount, hasStarbase); @@ -36,5 +40,25 @@ namespace SuperStarTrek.Space internal void AddKlingon() => KlingonCount += 1; internal void AddStarbase() => HasStarbase = true; + + internal void MarkAsKnown() => _isKnown = true; + + internal string Scan() + { + _isKnown = true; + return ToString(); + } + + public override string ToString() => _isKnown ? $"{KlingonCount}{(HasStarbase ? 1 : 0)}{StarCount}" : "***"; + + internal void RemoveKlingon() + { + if (KlingonCount > 0) + { + KlingonCount -= 1; + } + } + + internal void RemoveStarbase() => HasStarbase = false; } } diff --git a/84 Super Star Trek/csharp/StringExtensions.cs b/84 Super Star Trek/csharp/StringExtensions.cs new file mode 100644 index 00000000..02e9794f --- /dev/null +++ b/84 Super Star Trek/csharp/StringExtensions.cs @@ -0,0 +1,7 @@ +namespace SuperStarTrek +{ + internal static class StringExtensions + { + internal static string Pluralize(this string singular, int quantity) => singular + (quantity > 1 ? "s" : ""); + } +} \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Systems/ComputerFunctions/ComputerFunction.cs b/84 Super Star Trek/csharp/Systems/ComputerFunctions/ComputerFunction.cs new file mode 100644 index 00000000..400c1f62 --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/ComputerFunctions/ComputerFunction.cs @@ -0,0 +1,18 @@ +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems.ComputerFunctions +{ + internal abstract class ComputerFunction + { + protected ComputerFunction(string description, Output output) + { + Description = description; + Output = output; + } + + internal string Description { get; } + protected Output Output { get; } + + internal abstract void Execute(Quadrant quadrant); + } +} \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Systems/ComputerFunctions/CumulativeGalacticRecord.cs b/84 Super Star Trek/csharp/Systems/ComputerFunctions/CumulativeGalacticRecord.cs new file mode 100644 index 00000000..f02677ca --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/ComputerFunctions/CumulativeGalacticRecord.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems.ComputerFunctions +{ + internal class CumulativeGalacticRecord : GalacticReport + { + internal CumulativeGalacticRecord(Output output, Galaxy galaxy) + : base("Cumulative galactic record", output, galaxy) + { + } + + protected override void WriteHeader(Quadrant quadrant) => + Output.NextLine().WriteLine($"Computer record of galaxy for quadrant {quadrant.Coordinates}").NextLine(); + + protected override IEnumerable GetRowData() => + Galaxy.Quadrants.Select(row => " " + string.Join(" ", row)); + } +} \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Systems/ComputerFunctions/DirectionDistanceCalculator.cs b/84 Super Star Trek/csharp/Systems/ComputerFunctions/DirectionDistanceCalculator.cs new file mode 100644 index 00000000..ff6239ed --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/ComputerFunctions/DirectionDistanceCalculator.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using SuperStarTrek.Objects; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems.ComputerFunctions +{ + internal class DirectionDistanceCalculator : NavigationCalculator + { + private readonly Enterprise _enterprise; + private readonly Input _input; + + public DirectionDistanceCalculator(Enterprise enterprise, Output output, Input input) + : base("Starbase nav data", output) + { + _enterprise = enterprise; + _input = input; + } + + internal override void Execute(Quadrant quadrant) + { + Output.WriteLine("Direction/distance calculator:") + .Write($"You are at quadrant {_enterprise.QuadrantCoordinates}") + .WriteLine($" sector {_enterprise.SectorCoordinates}") + .WriteLine("Please enter"); + + WriteDirectionAndDistance( + _input.GetCoordinates(" Initial coordinates"), + _input.GetCoordinates(" Final coordinates")); + } + } +} \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Systems/ComputerFunctions/GalacticReport.cs b/84 Super Star Trek/csharp/Systems/ComputerFunctions/GalacticReport.cs new file mode 100644 index 00000000..8b26cc2e --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/ComputerFunctions/GalacticReport.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems.ComputerFunctions +{ + internal abstract class GalacticReport : ComputerFunction + { + public GalacticReport(string description, Output output, Galaxy galaxy) + : base(description, output) + { + Galaxy = galaxy; + } + + protected Galaxy Galaxy { get; } + + protected abstract void WriteHeader(Quadrant quadrant); + + protected abstract IEnumerable GetRowData(); + + internal sealed override void Execute(Quadrant quadrant) + { + WriteHeader(quadrant); + Output.WriteLine(" 1 2 3 4 5 6 7 8") + .WriteLine(" ----- ----- ----- ----- ----- ----- ----- -----"); + + foreach (var (row, index) in GetRowData().Select((r, i) => (r, i))) + { + Output.WriteLine($" {index+1} {row}") + .WriteLine(" ----- ----- ----- ----- ----- ----- ----- -----"); + } + } + } +} \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Systems/ComputerFunctions/GalaxyRegionMap.cs b/84 Super Star Trek/csharp/Systems/ComputerFunctions/GalaxyRegionMap.cs new file mode 100644 index 00000000..f6fe0305 --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/ComputerFunctions/GalaxyRegionMap.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; +using SuperStarTrek.Resources; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems.ComputerFunctions +{ + internal class GalaxyRegionMap : GalacticReport + { + internal GalaxyRegionMap(Output output, Galaxy galaxy) + : base("Galaxy 'region name' map", output, galaxy) + { + } + + protected override void WriteHeader(Quadrant quadrant) => + Output.WriteLine(" The Galaxy"); + + protected override IEnumerable GetRowData() => + Strings.RegionNames.Split('\n').Select(n => n.TrimEnd('\r')); + } +} \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Systems/ComputerFunctions/NavigationCalculator.cs b/84 Super Star Trek/csharp/Systems/ComputerFunctions/NavigationCalculator.cs new file mode 100644 index 00000000..680f3d18 --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/ComputerFunctions/NavigationCalculator.cs @@ -0,0 +1,29 @@ +using SuperStarTrek.Space; +using SuperStarTrek.Utils; + +namespace SuperStarTrek.Systems.ComputerFunctions +{ + internal abstract class NavigationCalculator : ComputerFunction + { + protected NavigationCalculator(string description, Output output) + : base(description, output) + { + } + + protected void WriteDirectionAndDistance(Coordinates from, Coordinates to) + { + var (direction, distance) = from.GetDirectionAndDistanceTo(to); + Write(direction, distance); + } + + protected void WriteDirectionAndDistance((float X, float Y) from, (float X, float Y) to) + { + var (direction, distance) = DirectionAndDistance.From(from.X, from.Y).To(to.X, to.Y); + Write(direction, distance); + } + + private void Write(float direction, float distance) => + Output.WriteLine($"Direction = {direction}") + .WriteLine($"Distance = {distance}"); + } +} \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Systems/ComputerFunctions/StarbaseDataCalculator.cs b/84 Super Star Trek/csharp/Systems/ComputerFunctions/StarbaseDataCalculator.cs new file mode 100644 index 00000000..0d2c5b50 --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/ComputerFunctions/StarbaseDataCalculator.cs @@ -0,0 +1,30 @@ +using SuperStarTrek.Objects; +using SuperStarTrek.Resources; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems.ComputerFunctions +{ + internal class StarbaseDataCalculator : NavigationCalculator + { + private readonly Enterprise _enterprise; + + public StarbaseDataCalculator(Enterprise enterprise, Output output) + : base("Starbase nav data", output) + { + _enterprise = enterprise; + } + + internal override void Execute(Quadrant quadrant) + { + if (!quadrant.HasStarbase) + { + Output.WriteLine(Strings.NoStarbase); + return; + } + + Output.WriteLine("From Enterprise to Starbase:"); + + WriteDirectionAndDistance(_enterprise.SectorCoordinates, quadrant.Starbase.Sector); + } + } +} \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Systems/ComputerFunctions/StatusReport.cs b/84 Super Star Trek/csharp/Systems/ComputerFunctions/StatusReport.cs new file mode 100644 index 00000000..f80ba7bb --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/ComputerFunctions/StatusReport.cs @@ -0,0 +1,41 @@ +using SuperStarTrek.Commands; +using SuperStarTrek.Objects; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems.ComputerFunctions +{ + internal class StatusReport : ComputerFunction + { + private readonly Game _game; + private readonly Galaxy _galaxy; + private readonly Enterprise _enterprise; + + public StatusReport(Game game, Galaxy galaxy, Enterprise enterprise, Output output) + : base("Status report", output) + { + _game = game; + _galaxy = galaxy; + _enterprise = enterprise; + } + + internal override void Execute(Quadrant quadrant) + { + Output.WriteLine(" Status report:") + .Write("Klingon".Pluralize(_galaxy.KlingonCount)).WriteLine($" left: {_galaxy.KlingonCount}") + .WriteLine($"Mission must be completed in {_game.StardatesRemaining:0.#} stardates."); + + if (_galaxy.StarbaseCount > 0) + { + Output.Write($"The Federation is maintaining {_galaxy.StarbaseCount} ") + .Write("starbase".Pluralize(_galaxy.StarbaseCount)).WriteLine(" in the galaxy."); + } + else + { + Output.WriteLine("Your stupidity has left you on your own in") + .WriteLine(" the galaxy -- you have no starbases left!"); + } + + _enterprise.Execute(Command.DAM); + } + } +} \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Systems/ComputerFunctions/TorpedoDataCalculator.cs b/84 Super Star Trek/csharp/Systems/ComputerFunctions/TorpedoDataCalculator.cs new file mode 100644 index 00000000..09c71859 --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/ComputerFunctions/TorpedoDataCalculator.cs @@ -0,0 +1,33 @@ +using SuperStarTrek.Objects; +using SuperStarTrek.Resources; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems.ComputerFunctions +{ + internal class TorpedoDataCalculator : NavigationCalculator + { + private readonly Enterprise _enterprise; + + public TorpedoDataCalculator(Enterprise enterprise, Output output) + : base("Photon torpedo data", output) + { + _enterprise = enterprise; + } + + internal override void Execute(Quadrant quadrant) + { + if (!quadrant.HasKlingons) + { + Output.WriteLine(Strings.NoEnemyShips); + return; + } + + Output.WriteLine("From Enterprise to Klingon battle cruiser".Pluralize(quadrant.KlingonCount)); + + foreach (var klingon in quadrant.Klingons) + { + WriteDirectionAndDistance(_enterprise.SectorCoordinates, klingon.Sector); + } + } + } +} \ No newline at end of file diff --git a/84 Super Star Trek/csharp/Systems/DamageControl.cs b/84 Super Star Trek/csharp/Systems/DamageControl.cs new file mode 100644 index 00000000..6ce41bbe --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/DamageControl.cs @@ -0,0 +1,54 @@ +using SuperStarTrek.Commands; +using SuperStarTrek.Objects; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems +{ + internal class DamageControl : Subsystem + { + private readonly Enterprise _enterprise; + private readonly Output _output; + + public DamageControl(Enterprise enterprise, Output output) + : base("Damage Control", Command.DAM, output) + { + _enterprise = enterprise; + _output = output; + } + + protected override CommandResult ExecuteCommandCore(Quadrant quadrant) + { + if (IsDamaged) + { + _output.WriteLine("Damage Control report not available"); + } + else + { + _output.NextLine(); + WriteDamageReport(); + } + + if (_enterprise.DamagedSystemCount > 0 && _enterprise.IsDocked) + { + if (quadrant.Starbase.TryRepair(_enterprise, out var repairTime)) + { + WriteDamageReport(); + return CommandResult.Elapsed(repairTime); + } + } + + return CommandResult.Ok; + } + + public void WriteDamageReport() + { + _output.NextLine().WriteLine("Device State of Repair"); + foreach (var system in _enterprise.Systems) + { + _output.Write(system.Name.PadRight(25)) + .WriteLine(((int)(system.Condition * 100) * 0.01).ToString(" 0.##;-0.##")); + } + _output.NextLine(); + } + } +} diff --git a/84 Super Star Trek/csharp/Systems/LibraryComputer.cs b/84 Super Star Trek/csharp/Systems/LibraryComputer.cs new file mode 100644 index 00000000..b2e47d2a --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/LibraryComputer.cs @@ -0,0 +1,47 @@ +using SuperStarTrek.Commands; +using SuperStarTrek.Space; +using SuperStarTrek.Systems.ComputerFunctions; + +namespace SuperStarTrek.Systems +{ + internal class LibraryComputer : Subsystem + { + private readonly Output _output; + private readonly Input _input; + private readonly ComputerFunction[] _functions; + + public LibraryComputer(Output output, Input input, params ComputerFunction[] functions) + : base("Library-Computer", Command.COM, output) + { + _output = output; + _input = input; + _functions = functions; + } + + protected override bool CanExecuteCommand() => IsOperational("Computer disabled"); + + protected override CommandResult ExecuteCommandCore(Quadrant quadrant) + { + var index = GetFunctionIndex(); + _output.NextLine(); + + _functions[index].Execute(quadrant); + + return CommandResult.Ok; + } + + private int GetFunctionIndex() + { + while (true) + { + var index = (int)_input.GetNumber("Computer active and waiting command"); + if (index >= 0 && index <= 5) { return index; } + + for (int i = 0; i < _functions.Length; i++) + { + _output.WriteLine($" {i} = {_functions[i].Description}"); + } + } + } + } +} diff --git a/84 Super Star Trek/csharp/Systems/LongRangeSensors.cs b/84 Super Star Trek/csharp/Systems/LongRangeSensors.cs new file mode 100644 index 00000000..84ed7a87 --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/LongRangeSensors.cs @@ -0,0 +1,39 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using SuperStarTrek.Commands; +using SuperStarTrek.Objects; +using SuperStarTrek.Resources; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems +{ + internal class LongRangeSensors : Subsystem + { + private readonly Galaxy _galaxy; + private readonly Output _output; + + public LongRangeSensors(Galaxy galaxy, Output output) + : base("Long Range Sensors", Command.LRS, output) + { + _galaxy = galaxy; + _output = output; + } + + protected override bool CanExecuteCommand() => IsOperational("{name} are inoperable"); + + protected override CommandResult ExecuteCommandCore(Quadrant quadrant) + { + _output.WriteLine($"Long range scan for quadrant {quadrant.Coordinates}"); + _output.WriteLine("-------------------"); + foreach (var quadrants in _galaxy.GetNeighborhood(quadrant)) + { + _output.WriteLine(": " + string.Join(" : ", quadrants.Select(q => q?.Scan() ?? "***")) + " :"); + _output.WriteLine("-------------------"); + } + + return CommandResult.Ok; + } + } +} diff --git a/84 Super Star Trek/csharp/Systems/PhaserControl.cs b/84 Super Star Trek/csharp/Systems/PhaserControl.cs new file mode 100644 index 00000000..18566294 --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/PhaserControl.cs @@ -0,0 +1,97 @@ +using System.Linq; +using SuperStarTrek.Commands; +using SuperStarTrek.Objects; +using SuperStarTrek.Resources; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems +{ + internal class PhaserControl : Subsystem + { + private readonly Enterprise _enterprise; + private readonly Output _output; + private readonly Input _input; + private readonly Random _random; + + public PhaserControl(Enterprise enterprise, Output output, Input input, Random random) + : base("Phaser Control", Command.PHA, output) + { + _enterprise = enterprise; + _output = output; + _input = input; + _random = random; + } + + protected override bool CanExecuteCommand() => IsOperational("Phasers inoperative"); + + protected override CommandResult ExecuteCommandCore(Quadrant quadrant) + { + if (!quadrant.HasKlingons) + { + _output.WriteLine(Strings.NoEnemyShips); + return CommandResult.Ok; + } + + if (_enterprise.Computer.IsDamaged) + { + _output.WriteLine("Computer failure hampers accuracy"); + } + + _output.Write($"Phasers locked on target; "); + + var phaserStrength = GetPhaserStrength(); + if (phaserStrength < 0) { return CommandResult.Ok; } + + _enterprise.UseEnergy(phaserStrength); + + var perEnemyStrength = GetPerTargetPhaserStrength(phaserStrength, quadrant.KlingonCount); + + foreach (var klingon in quadrant.Klingons.ToList()) + { + ResolveHitOn(klingon, perEnemyStrength, quadrant); + } + + return quadrant.KlingonsFireOnEnterprise(); + } + + private float GetPhaserStrength() + { + while (true) + { + _output.WriteLine($"Energy available = {_enterprise.Energy} units"); + var phaserStrength = _input.GetNumber("Number of units to fire"); + + if (phaserStrength <= _enterprise.Energy) { return phaserStrength; } + } + } + + private float GetPerTargetPhaserStrength(float phaserStrength, int targetCount) + { + if (_enterprise.Computer.IsDamaged) + { + phaserStrength *= _random.GetFloat(); + } + + return phaserStrength / targetCount; + } + + private void ResolveHitOn(Klingon klingon, float perEnemyStrength, Quadrant quadrant) + { + var distance = _enterprise.SectorCoordinates.GetDistanceTo(klingon.Sector); + var hitStrength = (int)(perEnemyStrength / distance * (2 + _random.GetFloat())); + + if (klingon.TakeHit(hitStrength)) + { + _output.WriteLine($"{hitStrength} unit hit on Klingon at sector {klingon.Sector}"); + _output.WriteLine( + klingon.Energy <= 0 + ? quadrant.Remove(klingon) + : $" (sensors show {klingon.Energy} units remaining)"); + } + else + { + _output.WriteLine($"Sensors show no damage to enemy at {klingon.Sector}"); + } + } + } +} diff --git a/84 Super Star Trek/csharp/Systems/PhotonTubes.cs b/84 Super Star Trek/csharp/Systems/PhotonTubes.cs new file mode 100644 index 00000000..a807d16c --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/PhotonTubes.cs @@ -0,0 +1,66 @@ +using SuperStarTrek.Commands; +using SuperStarTrek.Objects; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems +{ + internal class PhotonTubes : Subsystem + { + private readonly int _tubeCount; + private readonly Enterprise _enterprise; + private readonly Output _output; + private readonly Input _input; + + public PhotonTubes(int tubeCount, Enterprise enterprise, Output output, Input input) + : base("Photon Tubes", Command.TOR, output) + { + TorpedoCount = _tubeCount = tubeCount; + _enterprise = enterprise; + _output = output; + _input = input; + } + + public int TorpedoCount { get; private set; } + + protected override bool CanExecuteCommand() => HasTorpedoes() && IsOperational("{name} are not operational"); + + private bool HasTorpedoes() + { + if (TorpedoCount > 0) { return true; } + + _output.WriteLine("All photon torpedoes expended"); + return false; + } + + protected override CommandResult ExecuteCommandCore(Quadrant quadrant) + { + if (!_input.TryGetCourse("Photon torpedo course", "Ensign Chekov", out var course)) + { + return CommandResult.Ok; + } + + TorpedoCount -= 1; + + var isHit = false; + _output.WriteLine("Torpedo track:"); + foreach (var sector in course.GetSectorsFrom(_enterprise.SectorCoordinates)) + { + _output.WriteLine($" {sector}"); + + if (quadrant.TorpedoCollisionAt(sector, out var message, out var gameOver)) + { + _output.WriteLine(message); + isHit = true; + if (gameOver) { return CommandResult.GameOver; } + break; + } + } + + if (!isHit) { _output.WriteLine("Torpedo missed!"); } + + return quadrant.KlingonsFireOnEnterprise(); + } + + internal void ReplenishTorpedoes() => TorpedoCount = _tubeCount; + } +} diff --git a/84 Super Star Trek/csharp/Systems/ShieldControl.cs b/84 Super Star Trek/csharp/Systems/ShieldControl.cs index e7aca413..a7518359 100644 --- a/84 Super Star Trek/csharp/Systems/ShieldControl.cs +++ b/84 Super Star Trek/csharp/Systems/ShieldControl.cs @@ -1,3 +1,4 @@ +using SuperStarTrek.Commands; using SuperStarTrek.Objects; using SuperStarTrek.Space; @@ -10,36 +11,35 @@ namespace SuperStarTrek.Systems private readonly Input _input; public ShieldControl(Enterprise enterprise, Output output, Input input) - : base("Shield Control", Command.SHE) + : base("Shield Control", Command.SHE, output) { _enterprise = enterprise; _output = output; _input = input; } - public double Energy { get; private set; } + public float ShieldEnergy { get; set; } - public override void ExecuteCommand(Quadrant quadrant) + protected override bool CanExecuteCommand() => IsOperational("{name} inoperable"); + + protected override CommandResult ExecuteCommandCore(Quadrant quadrant) { - if (Condition < 0) - { - _output.WriteLine("Shield Control inoperable"); - return; - } - _output.WriteLine($"Energy available = {_enterprise.TotalEnergy}"); var requested = _input.GetNumber($"Number of units to shields"); if (Validate(requested)) { - Energy = requested; - return; + ShieldEnergy = requested; + } + else + { + _output.WriteLine(""); } - _output.WriteLine(""); + return CommandResult.Ok; } - private bool Validate(double requested) + private bool Validate(float requested) { if (requested > _enterprise.TotalEnergy) { @@ -47,7 +47,11 @@ namespace SuperStarTrek.Systems return false; } - return requested >= 0 && requested != Energy; + return requested >= 0 && requested != ShieldEnergy; } + + internal void AbsorbHit(int hitStrength) => ShieldEnergy -= hitStrength; + + internal void DropShields() => ShieldEnergy = 0; } } diff --git a/84 Super Star Trek/csharp/Systems/ShortRangeSensors.cs b/84 Super Star Trek/csharp/Systems/ShortRangeSensors.cs index d22dda70..ad3929d1 100644 --- a/84 Super Star Trek/csharp/Systems/ShortRangeSensors.cs +++ b/84 Super Star Trek/csharp/Systems/ShortRangeSensors.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using SuperStarTrek.Commands; using SuperStarTrek.Objects; using SuperStarTrek.Resources; using SuperStarTrek.Space; @@ -16,7 +17,7 @@ namespace SuperStarTrek.Systems private readonly Output _output; public ShortRangeSensors(Enterprise enterprise, Galaxy galaxy, Game game, Output output) - : base("Short Range Sensors", Command.SRS) + : base("Short Range Sensors", Command.SRS, output) { _enterprise = enterprise; _galaxy = galaxy; @@ -24,7 +25,7 @@ namespace SuperStarTrek.Systems _output = output; } - public override void ExecuteCommand(Quadrant quadrant) + protected override CommandResult ExecuteCommandCore(Quadrant quadrant) { if (_enterprise.IsDocked) { @@ -42,17 +43,19 @@ namespace SuperStarTrek.Systems .ToList() .ForEach(l => _output.WriteLine(l)); _output.WriteLine("---------------------------------"); + + return CommandResult.Ok; } public IEnumerable GetStatusLines() { yield return $"Stardate {_game.Stardate}"; yield return $"Condition {_enterprise.Condition}"; - yield return $"Quadrant {_enterprise.Quadrant}"; - yield return $"Sector {_enterprise.Sector}"; - yield return $"Photon torpedoes {_enterprise.TorpedoCount}"; - yield return $"Total energy {Math.Ceiling(_enterprise.Energy)}"; - yield return $"Shields {(int)_enterprise.Shields}"; + yield return $"Quadrant {_enterprise.QuadrantCoordinates}"; + yield return $"Sector {_enterprise.SectorCoordinates}"; + yield return $"Photon torpedoes {_enterprise.PhotonTubes.TorpedoCount}"; + yield return $"Total energy {Math.Ceiling(_enterprise.TotalEnergy)}"; + yield return $"Shields {(int)_enterprise.ShieldControl.ShieldEnergy}"; yield return $"Klingons remaining {_galaxy.KlingonCount}"; } } diff --git a/84 Super Star Trek/csharp/Systems/Subsystem.cs b/84 Super Star Trek/csharp/Systems/Subsystem.cs index ae2ef7d6..9e5f3570 100644 --- a/84 Super Star Trek/csharp/Systems/Subsystem.cs +++ b/84 Super Star Trek/csharp/Systems/Subsystem.cs @@ -1,20 +1,66 @@ +using System; +using SuperStarTrek.Commands; using SuperStarTrek.Space; namespace SuperStarTrek.Systems { internal abstract class Subsystem { - protected Subsystem(string name, Command command) + private readonly Output _output; + + protected Subsystem(string name, Command command, Output output) { Name = name; Command = command; Condition = 0; + _output = output; } public string Name { get; } - public double Condition { get; } + public float Condition { get; private set; } + public bool IsDamaged => Condition < 0; public Command Command { get; } - public abstract void ExecuteCommand(Quadrant quadrant); + protected virtual bool CanExecuteCommand() => true; + + protected bool IsOperational(string notOperationalMessage) + { + if (IsDamaged) + { + _output.WriteLine(notOperationalMessage.Replace("{name}", Name)); + return false; + } + + return true; + } + + public CommandResult ExecuteCommand(Quadrant quadrant) + => CanExecuteCommand() ? ExecuteCommandCore(quadrant) : CommandResult.Ok; + + protected abstract CommandResult ExecuteCommandCore(Quadrant quadrant); + + public virtual void Repair() + { + if (IsDamaged) + { + Condition = 0; + } + } + + public virtual bool Repair(float repairWorkDone) + { + if (IsDamaged) + { + Condition += repairWorkDone; + if (Condition > -0.1f && Condition < 0) + { + Condition = -0.1f; + } + } + + return !IsDamaged; + } + + internal void TakeDamage(float damage) => Condition -= damage; } } diff --git a/84 Super Star Trek/csharp/Systems/WarpEngines.cs b/84 Super Star Trek/csharp/Systems/WarpEngines.cs new file mode 100644 index 00000000..0dcf396c --- /dev/null +++ b/84 Super Star Trek/csharp/Systems/WarpEngines.cs @@ -0,0 +1,82 @@ +using System; +using SuperStarTrek.Commands; +using SuperStarTrek.Objects; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Systems +{ + internal class WarpEngines : Subsystem + { + private readonly Enterprise _enterprise; + private readonly Output _output; + private readonly Input _input; + + public WarpEngines(Enterprise enterprise, Output output, Input input) + : base("Warp Engines", Command.NAV, output) + { + _enterprise = enterprise; + _output = output; + _input = input; + } + + protected override CommandResult ExecuteCommandCore(Quadrant quadrant) + { + if (_input.TryGetCourse("Course", " Lt. Sulu", out var course) && + TryGetWarpFactor(out var warpFactor) && + TryGetDistanceToMove(warpFactor, out var distanceToMove)) + { + var result = quadrant.KlingonsMoveAndFire(); + if (result.IsGameOver) { return result; } + + _enterprise.RepairSystems(warpFactor); + _enterprise.VaryConditionOfRandomSystem(); + var timeElapsed = _enterprise.Move(course, warpFactor, distanceToMove); + + if (_enterprise.IsDocked) + { + _enterprise.ShieldControl.DropShields(); + _enterprise.Refuel(); + _enterprise.PhotonTubes.ReplenishTorpedoes(); + } + + return CommandResult.Elapsed(timeElapsed); + } + + return CommandResult.Ok; + } + + private bool TryGetWarpFactor(out float warpFactor) + { + var maximumWarp = IsDamaged ? 0.2f : 8; + if (_input.TryGetNumber("Warp Factor", 0, maximumWarp, out warpFactor)) + { + return warpFactor > 0; + } + + _output.WriteLine( + IsDamaged && warpFactor > maximumWarp + ? "Warp engines are damaged. Maximum speed = warp 0.2" + : $" Chief Engineer Scott reports, 'The engines won't take warp {warpFactor} !'"); + + return false; + } + + private bool TryGetDistanceToMove(float warpFactor, out int distanceToTravel) + { + distanceToTravel = (int)Math.Round(warpFactor * 8, MidpointRounding.AwayFromZero); + if (distanceToTravel <= _enterprise.Energy) { return true; } + + _output.WriteLine("Engineering reports, 'Insufficient energy available") + .WriteLine($" for maneuvering at warp {warpFactor} !'"); + + if (distanceToTravel <= _enterprise.TotalEnergy && !_enterprise.ShieldControl.IsDamaged) + { + _output.Write($"Deflector control room acknowledges {_enterprise.ShieldControl.ShieldEnergy} ") + .WriteLine("units of energy") + .WriteLine(" presently deployed to shields."); + } + + return false; + } + } +} diff --git a/84 Super Star Trek/csharp/Utils/DirectionAndDistance.cs b/84 Super Star Trek/csharp/Utils/DirectionAndDistance.cs new file mode 100644 index 00000000..3fa9f4b8 --- /dev/null +++ b/84 Super Star Trek/csharp/Utils/DirectionAndDistance.cs @@ -0,0 +1,65 @@ +using System; +using SuperStarTrek.Space; + +namespace SuperStarTrek.Utils +{ + internal class DirectionAndDistance + { + private readonly float _fromX; + private readonly float _fromY; + + private DirectionAndDistance(float fromX, float fromY) + { + _fromX = fromX; + _fromY = fromY; + } + + public static DirectionAndDistance From(Coordinates coordinates) => From(coordinates.X, coordinates.Y); + + public static DirectionAndDistance From(float x, float y) => new DirectionAndDistance(x, y); + + public (float Direction, float Distance) To(Coordinates coordinates) => To(coordinates.X, coordinates.Y); + + public (float Direction, float Distance) To(float x, float y) + { + var deltaX = x - _fromX; + var deltaY = y - _fromY; + + return (GetDirection(deltaX, deltaY), GetDistance(deltaX, deltaY)); + } + + // The algorithm here is mathematically equivalent to the following code in the original, + // where X is deltaY and A is deltaX + // 8220 X=X-A:A=C1-W1:IFX<0THEN8350 + // 8250 IFA<0THEN8410 + // 8260 IFX>0THEN8280 + // 8270 IFA=0THENC1=5:GOTO8290 + // 8280 C1=1 + // 8290 IFABS(A)<=ABS(X)THEN8330 + // 8310 PRINT"DIRECTION =";C1+(((ABS(A)-ABS(X))+ABS(A))/ABS(A)):GOTO8460 + // 8330 PRINT"DIRECTION =";C1+(ABS(A)/ABS(X)):GOTO8460 + // 8350 IFA>0THENC1=3:GOTO8420 + // 8360 IFX<>0THENC1=5:GOTO8290 + // 8410 C1=7 + // 8420 IFABS(A)>=ABS(X)THEN8450 + // 8430 PRINT"DIRECTION =";C1+(((ABS(X)-ABS(A))+ABS(X))/ABS(X)):GOTO8460 + // 8450 PRINT"DIRECTION =";C1+(ABS(X)/ABS(A)) + // 8460 PRINT"DISTANCE =";SQR(X^2+A^2):IFH8=1THEN1990 + private float GetDirection(float deltaX, float deltaY) + { + var deltaXDominant = Math.Abs(deltaX) > Math.Abs(deltaY); + var fractionalPart = deltaXDominant ? deltaY / deltaX : -deltaX / deltaY; + var nearestCardinal = deltaXDominant switch + { + true => deltaX > 0 ? 7 : 3, + false => deltaY > 0 ? 1 : 5 + }; + + var direction = nearestCardinal + fractionalPart; + return direction < 1 ? direction + 8 : direction; + } + + private float GetDistance(float deltaX, float deltaY) => + (float)Math.Sqrt(Math.Pow(deltaX, 2) + Math.Pow(deltaY, 2)); + } +} \ No newline at end of file