Merge pull request #250 from drewjcooper/84-super-star-trek-csharp

84 super star trek - C#
This commit is contained in:
Jeff Atwood
2021-03-25 11:26:53 -07:00
committed by GitHub
56 changed files with 1627 additions and 179 deletions

View File

@@ -1,6 +1,6 @@
using System.ComponentModel; using System.ComponentModel;
namespace SuperStarTrek namespace SuperStarTrek.Commands
{ {
internal enum Command internal enum Command
{ {

View File

@@ -1,7 +1,7 @@
using System.Reflection; using System.Reflection;
using System.ComponentModel; using System.ComponentModel;
namespace SuperStarTrek namespace SuperStarTrek.Commands
{ {
internal static class CommandExtensions internal static class CommandExtensions
{ {

View File

@@ -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);
}
}

View File

@@ -3,6 +3,7 @@ using SuperStarTrek.Objects;
using SuperStarTrek.Resources; using SuperStarTrek.Resources;
using SuperStarTrek.Space; using SuperStarTrek.Space;
using SuperStarTrek.Systems; using SuperStarTrek.Systems;
using SuperStarTrek.Systems.ComputerFunctions;
using static System.StringComparison; using static System.StringComparison;
namespace SuperStarTrek namespace SuperStarTrek
@@ -11,12 +12,12 @@ namespace SuperStarTrek
{ {
private readonly Output _output; private readonly Output _output;
private readonly Input _input; private readonly Input _input;
private readonly Random _random;
private int _initialStardate; private int _initialStardate;
private int _finalStarDate; private int _finalStarDate;
private double _currentStardate; private float _currentStardate;
private Coordinates _currentQuadrant; private Coordinates _currentQuadrant;
private Coordinates _currentSector;
private Galaxy _galaxy; private Galaxy _galaxy;
private int _initialKlingonCount; private int _initialKlingonCount;
private Enterprise _enterprise; private Enterprise _enterprise;
@@ -25,19 +26,17 @@ namespace SuperStarTrek
{ {
_output = new Output(); _output = new Output();
_input = new Input(_output); _input = new Input(_output);
_random = new Random();
} }
public double Stardate => _currentStardate; public float Stardate => _currentStardate;
public float StardatesRemaining => _finalStarDate - _currentStardate;
public void DoIntroduction() public void DoIntroduction()
{ {
_output.Write(Strings.Title); _output.Write(Strings.Title);
_output.Write("Do you need instructions (Y/N)? "); if (_input.GetYesNo("Do you need instructions", Input.YesNoMode.FalseOnN))
var response = Console.ReadLine();
_output.WriteLine();
if (!response.Equals("N", InvariantCultureIgnoreCase))
{ {
_output.Write(Strings.Instructions); _output.Write(Strings.Instructions);
@@ -47,8 +46,61 @@ namespace SuperStarTrek
public void Play() public void Play()
{ {
var quadrant = Initialise(); Initialise();
var gameOver = false; 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(Strings.Enterprise);
_output.Write( _output.Write(
@@ -62,36 +114,21 @@ namespace SuperStarTrek
_input.WaitForAnyKeyButEnter("when ready to accept command"); _input.WaitForAnyKeyButEnter("when ready to accept command");
_enterprise.Enter(quadrant, Strings.StartText); _enterprise.StartIn(BuildCurrentQuadrant());
while (!gameOver)
{
var command = _input.GetCommand();
gameOver = command == Command.XXX || _enterprise.Execute(command);
}
} }
private Quadrant Initialise() private Quadrant BuildCurrentQuadrant() =>
{ new Quadrant(_galaxy[_currentQuadrant], _enterprise, _random, _galaxy, _input, _output);
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);
}
public bool Replay() => _galaxy.StarbaseCount > 0 && _input.GetString(Strings.ReplayPrompt, "Aye"); 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);
} }
} }

View File

@@ -1,5 +1,7 @@
using System; using System;
using System.Linq; using System.Linq;
using SuperStarTrek.Commands;
using SuperStarTrek.Space;
using static System.StringComparison; using static System.StringComparison;
namespace SuperStarTrek namespace SuperStarTrek
@@ -25,14 +27,14 @@ namespace SuperStarTrek
return Console.ReadLine(); return Console.ReadLine();
} }
public double GetNumber(string prompt) public float GetNumber(string prompt)
{ {
_output.Prompt(prompt); _output.Prompt(prompt);
while (true) while (true)
{ {
var response = Console.ReadLine(); var response = Console.ReadLine();
if (double.TryParse(response, out var value)) if (float.TryParse(response, out var value))
{ {
return 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})"); value = GetNumber($"{prompt} ({minValue}-{maxValue})");
@@ -58,7 +67,7 @@ namespace SuperStarTrek
{ {
var response = GetString("Command"); var response = GetString("Command");
if (response != "" && if (response.Length >= 3 &&
Enum.TryParse(response.Substring(0, 3), ignoreCase: true, out Command parsedCommand)) Enum.TryParse(response.Substring(0, 3), ignoreCase: true, out Command parsedCommand))
{ {
return parsedCommand; return parsedCommand;
@@ -72,5 +81,79 @@ namespace SuperStarTrek
_output.WriteLine(); _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
}
} }
} }

View File

@@ -1,6 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Text; using System.Text;
using SuperStarTrek.Commands;
using SuperStarTrek.Resources; using SuperStarTrek.Resources;
using SuperStarTrek.Space; using SuperStarTrek.Space;
using SuperStarTrek.Systems; using SuperStarTrek.Systems;
@@ -10,84 +12,71 @@ namespace SuperStarTrek.Objects
internal class Enterprise internal class Enterprise
{ {
private readonly int _maxEnergy; private readonly int _maxEnergy;
private readonly Output _output;
private readonly List<Subsystem> _systems; private readonly List<Subsystem> _systems;
private readonly Dictionary<Command, Subsystem> _commandExecutors; private readonly Dictionary<Command, Subsystem> _commandExecutors;
private readonly Random _random;
private readonly Input _input;
private Quadrant _quadrant; 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; TotalEnergy = _maxEnergy = maxEnergy;
_systems = new List<Subsystem>(); _systems = new List<Subsystem>();
_commandExecutors = new Dictionary<Command, Subsystem>(); _commandExecutors = new Dictionary<Command, Subsystem>();
_output = output;
_random = random;
_input = input;
} }
public Coordinates Quadrant => _quadrant.Coordinates; public Quadrant Quadrant => _quadrant;
public Coordinates Sector { get; } public Coordinates QuadrantCoordinates => _quadrant.Coordinates;
public string Condition => GetCondition(); public Coordinates SectorCoordinates { get; private set; }
public double Shields => _shieldControl.Energy;
public double Energy => TotalEnergy - Shields;
public double TotalEnergy { get; private set; }
public int TorpedoCount { get; }
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<Subsystem> 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) public Enterprise Add(Subsystem system)
{ {
_systems.Add(system); _systems.Add(system);
_commandExecutors[system.Command] = system; _commandExecutors[system.Command] = system;
if (system is ShieldControl shieldControl) { _shieldControl = shieldControl; }
return this; return this;
} }
public string GetDamageReport() public void StartIn(Quadrant quadrant)
{
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)
{ {
_quadrant = quadrant; _quadrant = quadrant;
quadrant.Display(Strings.StartText);
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);
} }
private string GetCondition() => private string GetCondition() =>
(_quadrant.HasKlingons, Energy / _maxEnergy) switch (_quadrant.HasKlingons, Energy / _maxEnergy) switch
{ {
(true, _) => "*Red*", (true, _) => "*Red*",
(_, < 0.1) => "Yellow", (_, < 0.1f) => "Yellow",
_ => "Green" _ => "Green"
}; };
public bool Execute(Command command) public CommandResult Execute(Command command)
{ {
_commandExecutors[command].ExecuteCommand(_quadrant); if (command == Command.XXX) { return CommandResult.GameOver; }
return false;
return _commandExecutors[command].ExecuteCommand(_quadrant);
} }
public void Refuel() => TotalEnergy = _maxEnergy;
internal bool Recognises(string command) internal bool Recognises(string command)
{ {
throw new NotImplementedException(); throw new NotImplementedException();
@@ -99,5 +88,143 @@ namespace SuperStarTrek.Objects
} }
public override string ToString() => "<*>"; 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($" <Shields down to {ShieldControl.ShieldEnergy} units>");
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<string>();
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;
} }
} }

View File

@@ -1,14 +1,42 @@
using SuperStarTrek.Commands;
using SuperStarTrek.Space;
namespace SuperStarTrek.Objects namespace SuperStarTrek.Objects
{ {
internal class Klingon 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 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;
} }
} }

View File

@@ -1,7 +1,44 @@
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
namespace SuperStarTrek.Objects namespace SuperStarTrek.Objects
{ {
internal class Starbase 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() => ">!<"; 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);
} }
} }

View File

@@ -4,12 +4,37 @@ namespace SuperStarTrek
{ {
internal class Output internal class Output
{ {
public void Write(string text) => Console.Write(text); public Output Write(string text)
public void Write(string format, params object[] args) => Console.Write(format, args); {
public void WriteLine(string text = "") => Console.WriteLine(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}? ");
} }
} }

View File

@@ -29,6 +29,7 @@ namespace SuperStarTrek
{ {
static void Main() static void Main()
{ {
var foo = Utils.DirectionAndDistance.From(1,1).To(4,5);
var game = new Game(); var game = new Game();
game.DoIntroduction(); game.DoIntroduction();

View File

@@ -6,20 +6,20 @@ namespace SuperStarTrek
{ {
private static readonly System.Random _random = new(); 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: // 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) // 475 DEF FNR(R)=INT(RND(R)*7.98+1.01)
// Returns a value from 1 to 8, inclusive. // Returns a value from 1 to 8, inclusive.
// Note there's a slight bias away from the extreme values, 1 and 8. // 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) => public int GetInt(int inclusiveMinValue, int exclusiveMaxValue) =>
_random.Next(inclusiveMinValue, exclusiveMaxValue); _random.Next(inclusiveMinValue, exclusiveMaxValue);
public double GetDouble() => _random.NextDouble(); public float GetFloat() => (float)_random.NextDouble();
public double GetDouble(double inclusiveMinValue, double exclusiveMaxValue) public float GetFloat(float inclusiveMinValue, float exclusiveMaxValue)
=> _random.NextDouble() * (exclusiveMaxValue - inclusiveMinValue) + inclusiveMinValue; => GetFloat() * (exclusiveMaxValue - inclusiveMinValue) + inclusiveMinValue;
} }
} }

View File

@@ -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

View File

@@ -0,0 +1,4 @@
Congratulations, Captain! The last Klingon battle cruiser
menacing the Federation has been destroyed.
Your efficiency rating is {0}.

View File

@@ -0,0 +1,3 @@
Starfleet Command reviewing your record to consider
court martial!

View File

@@ -0,0 +1 @@
The Enterprise has been destroyed. The Federation will be conquered.

View File

@@ -0,0 +1,3 @@
Is is stardate {0}.
There were {1} Klingon battle cruisers left at
the end of your mission.

View File

@@ -1,3 +1,4 @@
INSTRUCTIONS FOR 'SUPER STAR TREK' INSTRUCTIONS FOR 'SUPER STAR TREK'
1. When you see "Command ?" printed, enter one of the legal 1. When you see "Command ?" printed, enter one of the legal

View File

@@ -0,0 +1,2 @@
Science Officer Spock reports, 'Sensors show no enemy ships
in this quadrant'

View File

@@ -0,0 +1 @@
Mr. Spock reports, 'Sensors show no starbases in this quadrant.'

View File

@@ -0,0 +1,2 @@
Now entering {0} quadrant . . .

View File

@@ -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}.'

View File

@@ -0,0 +1 @@
Starbase shields protect the Enterprise

View File

@@ -0,0 +1,8 @@
Antares Sirius
Rigel Deneb
Procyon Capella
Vega Betelgeuse
Canopus Aldebaran
Altair Regulus
Sagittarius Arcturus
Pollux Spica

View File

@@ -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!!

View File

@@ -0,0 +1,2 @@
Technicians standing by to effect repairs to your ship;
Estimated time to repair: {0} stardates.

View File

@@ -0,0 +1 @@
Will you authorize the repair order (Y/N)

View File

@@ -1,3 +1,5 @@
The Federation is in need of a new starship commander The Federation is in need of a new starship commander
for a similar mission -- if there is a volunteer for a similar mission -- if there is a volunteer
let him step forward and enter 'Aye' let him step forward and enter 'Aye'

View File

@@ -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!!

View File

@@ -7,14 +7,29 @@ namespace SuperStarTrek.Resources
internal static class Strings internal static class Strings
{ {
public static string CombatArea => GetResource(); 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 Enterprise => GetResource();
public static string Instructions => GetResource(); public static string Instructions => GetResource();
public static string LowShields => 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 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 ReplayPrompt => GetResource();
public static string ShieldsDropped => GetResource(); public static string ShieldsDropped => GetResource();
public static string ShortRangeSensorsOut => GetResource(); public static string ShortRangeSensorsOut => GetResource();
public static string StartText => GetResource(); public static string StartText => GetResource();
public static string Stranded => GetResource();
public static string Title => GetResource(); public static string Title => GetResource();
private static string GetResource([CallerMemberName] string name = "") private static string GetResource([CallerMemberName] string name = "")

View File

@@ -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}";
}
}

View File

@@ -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;
}
}
}

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
namespace SuperStarTrek.Space namespace SuperStarTrek.Space
{ {
@@ -24,7 +25,7 @@ namespace SuperStarTrek.Space
(0, 1) (0, 1)
}; };
public Course(double direction) internal Course(float direction)
{ {
if (direction < 1 || direction > 9) if (direction < 1 || direction > 9)
{ {
@@ -44,7 +45,53 @@ namespace SuperStarTrek.Space
DeltaY = baseCardinal.DeltaY + (nextCardinal.DeltaY - baseCardinal.DeltaY) * fractionalDirection; DeltaY = baseCardinal.DeltaY + (nextCardinal.DeltaY - baseCardinal.DeltaY) * fractionalDirection;
} }
public double DeltaX { get; } internal float DeltaX { get; }
public double DeltaY { get; } internal float DeltaY { get; }
internal IEnumerable<Coordinates> 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)
};
}
} }
} }

View File

@@ -1,17 +1,37 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using SuperStarTrek.Objects;
using SuperStarTrek.Resources;
using static System.StringSplitOptions;
namespace SuperStarTrek.Space namespace SuperStarTrek.Space
{ {
internal class Galaxy internal class Galaxy
{ {
private static readonly string[] _regionNames;
private static readonly string[] _subRegionIdentifiers;
private readonly QuadrantInfo[][] _quadrants; 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 => public Galaxy(Random random)
Enumerable.Range(1, 8).Select(y => QuadrantInfo.Create(new Coordinates(x, y), "")).ToArray()) {
_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(); .ToArray();
if (StarbaseCount == 0) 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 KlingonCount => _quadrants.SelectMany(q => q).Sum(q => q.KlingonCount);
public int StarbaseCount => _quadrants.SelectMany(q => q).Count(q => q.HasStarbase); public int StarbaseCount => _quadrants.SelectMany(q => q).Count(q => q.HasStarbase);
public IEnumerable<IEnumerable<QuadrantInfo>> Quadrants => _quadrants;
private static string GetQuadrantName(Coordinates coordinates) =>
$"{_regionNames[coordinates.RegionIndex]} {_subRegionIdentifiers[coordinates.SubRegionIndex]}";
public IEnumerable<IEnumerable<QuadrantInfo>> GetNeighborhood(Quadrant quadrant) =>
Enumerable.Range(-1, 3)
.Select(dx => dx + quadrant.Coordinates.X)
.Select(x => GetNeighborhoodRow(quadrant, x));
private IEnumerable<QuadrantInfo> 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]);
} }
} }

View File

@@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using SuperStarTrek.Commands;
using SuperStarTrek.Objects; using SuperStarTrek.Objects;
using SuperStarTrek.Resources;
namespace SuperStarTrek.Space namespace SuperStarTrek.Space
{ {
@@ -10,42 +12,56 @@ namespace SuperStarTrek.Space
private readonly QuadrantInfo _info; private readonly QuadrantInfo _info;
private readonly Random _random; private readonly Random _random;
private readonly Dictionary<Coordinates, object> _sectors; private readonly Dictionary<Coordinates, object> _sectors;
private readonly Coordinates _enterpriseSector; private readonly Enterprise _enterprise;
private readonly Coordinates _starbaseSector; 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; _info = info;
_random = new Random(); _random = random;
_output = output;
Galaxy = galaxy;
_enterpriseSector = enterprise.Sector; info.MarkAsKnown();
_sectors = new Dictionary<Coordinates, object> { [enterprise.Sector] = enterprise }; _sectors = new() { [enterprise.SectorCoordinates] = _enterprise = enterprise };
PositionObject(() => new Klingon(), _info.KlingonCount); PositionObject(sector => new Klingon(sector, _random), _info.KlingonCount);
if (_info.HasStarbase) 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 Coordinates Coordinates => _info.Coordinates;
public bool HasKlingons => _info.KlingonCount > 0; public bool HasKlingons => _info.KlingonCount > 0;
public int KlingonCount => _info.KlingonCount;
public bool HasStarbase => _info.HasStarbase; public bool HasStarbase => _info.HasStarbase;
public Starbase Starbase { get; }
internal Galaxy Galaxy { get; }
public bool EnterpriseIsNextToStarbase => public bool EnterpriseIsNextToStarbase =>
_info.HasStarbase && _info.HasStarbase &&
Math.Abs(_enterpriseSector.X - _starbaseSector.X) <= 1 && Math.Abs(_enterprise.SectorCoordinates.X - Starbase.Sector.X) <= 1 &&
Math.Abs(_enterpriseSector.Y - _starbaseSector.Y) <= 1; Math.Abs(_enterprise.SectorCoordinates.Y - Starbase.Sector.Y) <= 1;
internal IEnumerable<Klingon> Klingons => _sectors.Values.OfType<Klingon>();
public override string ToString() => _info.Name; public override string ToString() => _info.Name;
private Coordinates PositionObject(Func<object> objectFactory) private T PositionObject<T>(Func<Coordinates, T> objectFactory)
{ {
var sector = GetRandomEmptySector(); var sector = GetRandomEmptySector();
_sectors[sector] = objectFactory.Invoke(); _sectors[sector] = objectFactory.Invoke(sector);
return sector; return (T)_sectors[sector];
} }
private void PositionObject(Func<object> objectFactory, int count) private void PositionObject(Func<Coordinates, object> objectFactory, int count)
{ {
for (int i = 0; i < count; i++) 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() private Coordinates GetRandomEmptySector()
{ {
while (true) while (true)
@@ -65,15 +166,21 @@ namespace SuperStarTrek.Space
} }
} }
public IEnumerable<string> GetDisplayLines() => Enumerable.Range(1, 8).Select(x => GetDisplayLine(x)); public IEnumerable<string> GetDisplayLines() => Enumerable.Range(0, 8).Select(x => GetDisplayLine(x));
private string GetDisplayLine(int x) private string GetDisplayLine(int x)
=> string.Join( => string.Join(
" ", " ",
Enumerable Enumerable
.Range(1, 8) .Range(0, 8)
.Select(y => new Coordinates(x, y)) .Select(y => new Coordinates(x, y))
.Select(c => _sectors.GetValueOrDefault(c)) .Select(c => _sectors.GetValueOrDefault(c))
.Select(o => o?.ToString() ?? " ")); .Select(o => o?.ToString() ?? " "));
internal void SetEnterpriseSector(Coordinates sector)
{
_sectors.Remove(_enterprise.SectorCoordinates);
_sectors[sector] = _enterprise;
}
} }
} }

View File

@@ -1,7 +1,11 @@
using SuperStarTrek.Objects;
namespace SuperStarTrek.Space namespace SuperStarTrek.Space
{ {
internal class QuadrantInfo internal class QuadrantInfo
{ {
private bool _isKnown;
private QuadrantInfo(Coordinates coordinates, string name, int klingonCount, int starCount, bool hasStarbase) private QuadrantInfo(Coordinates coordinates, string name, int klingonCount, int starCount, bool hasStarbase)
{ {
Coordinates = coordinates; Coordinates = coordinates;
@@ -20,14 +24,14 @@ namespace SuperStarTrek.Space
public static QuadrantInfo Create(Coordinates coordinates, string name) public static QuadrantInfo Create(Coordinates coordinates, string name)
{ {
var random = new Random(); var random = new Random();
var klingonCount = random.GetDouble() switch var klingonCount = random.GetFloat() switch
{ {
> 0.98 => 3, > 0.98f => 3,
> 0.95 => 2, > 0.95f => 2,
> 0.80 => 1, > 0.80f => 1,
_ => 0 _ => 0
}; };
var hasStarbase = random.GetDouble() > 0.96; var hasStarbase = random.GetFloat() > 0.96f;
var starCount = random.Get1To8Inclusive(); var starCount = random.Get1To8Inclusive();
return new QuadrantInfo(coordinates, name, klingonCount, starCount, hasStarbase); return new QuadrantInfo(coordinates, name, klingonCount, starCount, hasStarbase);
@@ -36,5 +40,25 @@ namespace SuperStarTrek.Space
internal void AddKlingon() => KlingonCount += 1; internal void AddKlingon() => KlingonCount += 1;
internal void AddStarbase() => HasStarbase = true; 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;
} }
} }

View File

@@ -0,0 +1,7 @@
namespace SuperStarTrek
{
internal static class StringExtensions
{
internal static string Pluralize(this string singular, int quantity) => singular + (quantity > 1 ? "s" : "");
}
}

View File

@@ -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);
}
}

View File

@@ -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<string> GetRowData() =>
Galaxy.Quadrants.Select(row => " " + string.Join(" ", row));
}
}

View File

@@ -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"));
}
}
}

View File

@@ -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<string> 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(" ----- ----- ----- ----- ----- ----- ----- -----");
}
}
}
}

View File

@@ -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<string> GetRowData() =>
Strings.RegionNames.Split('\n').Select(n => n.TrimEnd('\r'));
}
}

View File

@@ -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}");
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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}");
}
}
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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}");
}
}
}
}

View File

@@ -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;
}
}

View File

@@ -1,3 +1,4 @@
using SuperStarTrek.Commands;
using SuperStarTrek.Objects; using SuperStarTrek.Objects;
using SuperStarTrek.Space; using SuperStarTrek.Space;
@@ -10,36 +11,35 @@ namespace SuperStarTrek.Systems
private readonly Input _input; private readonly Input _input;
public ShieldControl(Enterprise enterprise, Output output, Input input) public ShieldControl(Enterprise enterprise, Output output, Input input)
: base("Shield Control", Command.SHE) : base("Shield Control", Command.SHE, output)
{ {
_enterprise = enterprise; _enterprise = enterprise;
_output = output; _output = output;
_input = input; _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}"); _output.WriteLine($"Energy available = {_enterprise.TotalEnergy}");
var requested = _input.GetNumber($"Number of units to shields"); var requested = _input.GetNumber($"Number of units to shields");
if (Validate(requested)) if (Validate(requested))
{ {
Energy = requested; ShieldEnergy = requested;
return; }
else
{
_output.WriteLine("<SHIELDS UNCHANGED>");
} }
_output.WriteLine("<SHIELDS UNCHANGED>"); return CommandResult.Ok;
} }
private bool Validate(double requested) private bool Validate(float requested)
{ {
if (requested > _enterprise.TotalEnergy) if (requested > _enterprise.TotalEnergy)
{ {
@@ -47,7 +47,11 @@ namespace SuperStarTrek.Systems
return false; return false;
} }
return requested >= 0 && requested != Energy; return requested >= 0 && requested != ShieldEnergy;
} }
internal void AbsorbHit(int hitStrength) => ShieldEnergy -= hitStrength;
internal void DropShields() => ShieldEnergy = 0;
} }
} }

View File

@@ -2,6 +2,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using SuperStarTrek.Commands;
using SuperStarTrek.Objects; using SuperStarTrek.Objects;
using SuperStarTrek.Resources; using SuperStarTrek.Resources;
using SuperStarTrek.Space; using SuperStarTrek.Space;
@@ -16,7 +17,7 @@ namespace SuperStarTrek.Systems
private readonly Output _output; private readonly Output _output;
public ShortRangeSensors(Enterprise enterprise, Galaxy galaxy, Game game, 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; _enterprise = enterprise;
_galaxy = galaxy; _galaxy = galaxy;
@@ -24,7 +25,7 @@ namespace SuperStarTrek.Systems
_output = output; _output = output;
} }
public override void ExecuteCommand(Quadrant quadrant) protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
{ {
if (_enterprise.IsDocked) if (_enterprise.IsDocked)
{ {
@@ -42,17 +43,19 @@ namespace SuperStarTrek.Systems
.ToList() .ToList()
.ForEach(l => _output.WriteLine(l)); .ForEach(l => _output.WriteLine(l));
_output.WriteLine("---------------------------------"); _output.WriteLine("---------------------------------");
return CommandResult.Ok;
} }
public IEnumerable<string> GetStatusLines() public IEnumerable<string> GetStatusLines()
{ {
yield return $"Stardate {_game.Stardate}"; yield return $"Stardate {_game.Stardate}";
yield return $"Condition {_enterprise.Condition}"; yield return $"Condition {_enterprise.Condition}";
yield return $"Quadrant {_enterprise.Quadrant}"; yield return $"Quadrant {_enterprise.QuadrantCoordinates}";
yield return $"Sector {_enterprise.Sector}"; yield return $"Sector {_enterprise.SectorCoordinates}";
yield return $"Photon torpedoes {_enterprise.TorpedoCount}"; yield return $"Photon torpedoes {_enterprise.PhotonTubes.TorpedoCount}";
yield return $"Total energy {Math.Ceiling(_enterprise.Energy)}"; yield return $"Total energy {Math.Ceiling(_enterprise.TotalEnergy)}";
yield return $"Shields {(int)_enterprise.Shields}"; yield return $"Shields {(int)_enterprise.ShieldControl.ShieldEnergy}";
yield return $"Klingons remaining {_galaxy.KlingonCount}"; yield return $"Klingons remaining {_galaxy.KlingonCount}";
} }
} }

View File

@@ -1,20 +1,66 @@
using System;
using SuperStarTrek.Commands;
using SuperStarTrek.Space; using SuperStarTrek.Space;
namespace SuperStarTrek.Systems namespace SuperStarTrek.Systems
{ {
internal abstract class Subsystem internal abstract class Subsystem
{ {
protected Subsystem(string name, Command command) private readonly Output _output;
protected Subsystem(string name, Command command, Output output)
{ {
Name = name; Name = name;
Command = command; Command = command;
Condition = 0; Condition = 0;
_output = output;
} }
public string Name { get; } public string Name { get; }
public double Condition { get; } public float Condition { get; private set; }
public bool IsDamaged => Condition < 0;
public Command Command { get; } 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;
} }
} }

View File

@@ -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;
}
}
}

View File

@@ -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));
}
}