Main game loop with Short Range Scan and Shield Control

This commit is contained in:
Andrew Cooper
2021-02-27 17:32:04 +11:00
parent be14ba13c1
commit 4d1a9176ec
29 changed files with 791 additions and 24 deletions

View File

@@ -0,0 +1,34 @@
using System.ComponentModel;
namespace SuperStarTrek
{
internal enum Command
{
[Description("To set course")]
NAV,
[Description("For short range sensor scan")]
SRS,
[Description("For long range sensor scan")]
LRS,
[Description("To fire phasers")]
PHA,
[Description("To fire photon torpedoes")]
TOR,
[Description("To raise or lower shields")]
SHE,
[Description("For damage control reports")]
DAM,
[Description("To call on library-computer")]
COM,
[Description("To resign your command")]
XXX
}
}

View File

@@ -0,0 +1,14 @@
using System.Reflection;
using System.ComponentModel;
namespace SuperStarTrek
{
internal static class CommandExtensions
{
internal static string GetDescription(this Command command) =>
typeof(Command)
.GetField(command.ToString())
.GetCustomAttribute<DescriptionAttribute>()
.Description;
}
}

View File

@@ -0,0 +1,97 @@
using System;
using SuperStarTrek.Objects;
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
using SuperStarTrek.Systems;
using static System.StringComparison;
namespace SuperStarTrek
{
internal class Game
{
private readonly Output _output;
private readonly Input _input;
private int _initialStardate;
private int _finalStarDate;
private double _currentStardate;
private Coordinates _currentQuadrant;
private Coordinates _currentSector;
private Galaxy _galaxy;
private int _initialKlingonCount;
private Enterprise _enterprise;
public Game()
{
_output = new Output();
_input = new Input(_output);
}
public double Stardate => _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))
{
_output.Write(Strings.Instructions);
_input.WaitForAnyKeyButEnter("to continue");
}
}
public void Play()
{
var quadrant = Initialise();
var gameOver = false;
_output.Write(Strings.Enterprise);
_output.Write(
Strings.Orders,
_galaxy.KlingonCount,
_finalStarDate,
_finalStarDate - _initialStardate,
_galaxy.StarbaseCount > 1 ? "are" : "is",
_galaxy.StarbaseCount,
_galaxy.StarbaseCount > 1 ? "s" : "");
_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);
}
}
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);
}
public bool Replay() => _galaxy.StarbaseCount > 0 && _input.GetString(Strings.ReplayPrompt, "Aye");
}
}

View File

@@ -0,0 +1,76 @@
using System;
using System.Linq;
using static System.StringComparison;
namespace SuperStarTrek
{
internal class Input
{
private readonly Output _output;
public Input(Output output)
{
_output = output;
}
public void WaitForAnyKeyButEnter(string prompt)
{
_output.Write($"Hit any key but Enter {prompt} ");
while (Console.ReadKey(intercept: true).Key == ConsoleKey.Enter);
}
public string GetString(string prompt)
{
_output.Prompt(prompt);
return Console.ReadLine();
}
public double GetNumber(string prompt)
{
_output.Prompt(prompt);
while (true)
{
var response = Console.ReadLine();
if (double.TryParse(response, out var value))
{
return value;
}
_output.WriteLine("!Number expected - retry input line");
_output.Prompt();
}
}
public bool TryGetNumber(string prompt, double minValue, double maxValue, out double value)
{
value = GetNumber($"{prompt} ({minValue}-{maxValue})");
return value >= minValue && value <= maxValue;
}
internal bool GetString(string replayPrompt, string trueValue)
=> GetString(replayPrompt).Equals(trueValue, InvariantCultureIgnoreCase);
public Command GetCommand()
{
while(true)
{
var response = GetString("Command");
if (response != "" &&
Enum.TryParse(response.Substring(0, 3), ignoreCase: true, out Command parsedCommand))
{
return parsedCommand;
}
_output.WriteLine("Enter one of the following:");
foreach (var command in Enum.GetValues(typeof(Command)).OfType<Command>())
{
_output.WriteLine($" {command} ({command.GetDescription()})");
}
_output.WriteLine();
}
}
}
}

View File

@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Text;
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
using SuperStarTrek.Systems;
namespace SuperStarTrek.Objects
{
internal class Enterprise
{
private readonly int _maxEnergy;
private readonly List<Subsystem> _systems;
private readonly Dictionary<Command, Subsystem> _commandExecutors;
private Quadrant _quadrant;
private ShieldControl _shieldControl;
public Enterprise(int maxEnergy, Coordinates sector)
{
Sector = sector;
TotalEnergy = _maxEnergy = maxEnergy;
_systems = new List<Subsystem>();
_commandExecutors = new Dictionary<Command, Subsystem>();
}
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 bool IsDocked { get; private set; }
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)
{
_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);
}
private string GetCondition() =>
(_quadrant.HasKlingons, Energy / _maxEnergy) switch
{
(true, _) => "*Red*",
(_, < 0.1) => "Yellow",
_ => "Green"
};
public bool Execute(Command command)
{
_commandExecutors[command].ExecuteCommand(_quadrant);
return false;
}
internal bool Recognises(string command)
{
throw new NotImplementedException();
}
internal string GetCommandList()
{
throw new NotImplementedException();
}
public override string ToString() => "<*>";
}
}

View File

@@ -0,0 +1,14 @@
namespace SuperStarTrek.Objects
{
internal class Klingon
{
private double _energy;
public Klingon()
{
_energy = new Random().GetDouble(100, 300);
}
public override string ToString() => "+K+";
}
}

View File

@@ -0,0 +1,7 @@
namespace SuperStarTrek.Objects
{
internal class Star
{
public override string ToString() => " * ";
}
}

View File

@@ -0,0 +1,7 @@
namespace SuperStarTrek.Objects
{
internal class Starbase
{
public override string ToString() => ">!<";
}
}

View File

@@ -0,0 +1,15 @@
using System;
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 void NextLine() => Console.WriteLine();
public void Prompt(string text = "") => Console.Write($"{text}? ");
}
}

View File

@@ -23,31 +23,20 @@
// **** CONVERTED TO MICROSOFT C# 2/20/21 BY ANDREW COOPER
// ****
using System;
using static System.StringComparison;
namespace SuperStarTrek
{
class Program
internal class Program
{
static void Main(string[] args)
static void Main()
{
Console.WriteLine(Strings.Title);
var game = new Game();
Console.Write("Do you need instructions (Y/N)? ");
var response = Console.ReadLine();
Console.WriteLine();
game.DoIntroduction();
if (!response.Equals("N", InvariantCultureIgnoreCase))
do
{
Console.WriteLine(Strings.Instructions);
Console.WriteLine("Press <Enter> to continue...");
Console.ReadLine();
}
Console.WriteLine(Strings.Enterprise);
game.Play();
} while (game.Replay());
}
}
}

View File

@@ -0,0 +1,25 @@
using SuperStarTrek.Space;
namespace SuperStarTrek
{
internal class Random
{
private static readonly System.Random _random = new();
public Coordinates GetCoordinate() => new Coordinates(Get1To8Inclusive(), Get1To8Inclusive());
// 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 GetInt(int inclusiveMinValue, int exclusiveMaxValue) =>
_random.Next(inclusiveMinValue, exclusiveMaxValue);
public double GetDouble() => _random.NextDouble();
public double GetDouble(double inclusiveMinValue, double exclusiveMaxValue)
=> _random.NextDouble() * (exclusiveMaxValue - inclusiveMinValue) + inclusiveMinValue;
}
}

View File

@@ -0,0 +1 @@
COMBAT AREA CONDITION RED

View File

@@ -53,7 +53,7 @@ LRS command = Long Range Sensor Scan
The scan is coded in the form ###, where the units digit
is the number of stars, the tens digit is the number of
starbases, and the hundreds digit is the number of
Kilngons.
Klingons.
Example - 207 = 2 Klingons, No starbases, & 7 stars.
@@ -103,3 +103,4 @@ COM command = Library-Computer
Option 5 = Galactic Region Name Map
This option prints the names of the sixteen major
galactic regions referred to in the game.

View File

@@ -0,0 +1 @@
SHIELDS DANGEROUSLY LOW

View File

@@ -0,0 +1,6 @@
Your orders are as follows:
Destroy the {0} Klingon warships which have invaded
the galaxy before they can attack federation headquarters
on stardate {1}. This gives you {2} days. There {3}
{4} starbase{5} in the galaxy for resupplying your ship.

View File

@@ -0,0 +1,3 @@
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'

View File

@@ -0,0 +1 @@
Shields dropped for docking purposes

View File

@@ -0,0 +1 @@
*** Short Range Sensors are out ***

View File

@@ -0,0 +1,5 @@
Your mission begins with your starship located
in the galactic quadrant, '{0}'.

View File

@@ -2,15 +2,20 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using static System.StringComparison;
namespace SuperStarTrek
namespace SuperStarTrek.Resources
{
internal static class Strings
{
public static string Title => GetResource();
public static string Instructions => GetResource();
public static string CombatArea => GetResource();
public static string Enterprise => GetResource();
public static string Instructions => GetResource();
public static string LowShields => GetResource();
public static string Orders => GetResource();
public static string ReplayPrompt => GetResource();
public static string ShieldsDropped => GetResource();
public static string ShortRangeSensorsOut => GetResource();
public static string StartText => GetResource();
public static string Title => GetResource();
private static string GetResource([CallerMemberName] string name = "")
{

View File

@@ -0,0 +1,27 @@
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,50 @@
using System;
namespace SuperStarTrek.Space
{
// Implements the course calculations from the original code:
// 530 FORI=1TO9:C(I,1)=0:C(I,2)=0:NEXTI
// 540 C(3,1)=-1:C(2,1)=-1:C(4,1)=-1:C(4,2)=-1:C(5,2)=-1:C(6,2)=-1
// 600 C(1,2)=1:C(2,2)=1:C(6,1)=1:C(7,1)=1:C(8,1)=1:C(8,2)=1:C(9,2)=1
//
// 3110 X1=C(C1,1)+(C(C1+1,1)-C(C1,1))*(C1-INT(C1))
// 3140 X2=C(C1,2)+(C(C1+1,2)-C(C1,2))*(C1-INT(C1))
internal class Course
{
private static readonly (int DeltaX, int DeltaY)[] cardinals = new[]
{
(0, 1),
(-1, 1),
(-1, 0),
(-1, -1),
(0, -1),
(1, -1),
(1, 0),
(1, 1),
(0, 1)
};
public Course(double direction)
{
if (direction < 1 || direction > 9)
{
throw new ArgumentOutOfRangeException(
nameof(direction),
direction,
"Must be between 1 and 9, inclusive.");
}
var cardinalDirection = (int)(direction - 1) % 8;
var fractionalDirection = direction - (int)direction;
var baseCardinal = cardinals[cardinalDirection];
var nextCardinal = cardinals[cardinalDirection + 1];
DeltaX = baseCardinal.DeltaX + (nextCardinal.DeltaX - baseCardinal.DeltaX) * fractionalDirection;
DeltaY = baseCardinal.DeltaY + (nextCardinal.DeltaY - baseCardinal.DeltaY) * fractionalDirection;
}
public double DeltaX { get; }
public double DeltaY { get; }
}
}

View File

@@ -0,0 +1,34 @@
using System.Linq;
namespace SuperStarTrek.Space
{
internal class Galaxy
{
private readonly QuadrantInfo[][] _quadrants;
public Galaxy()
{
var random = new Random();
_quadrants = Enumerable.Range(1, 8).Select(x =>
Enumerable.Range(1, 8).Select(y => QuadrantInfo.Create(new Coordinates(x, y), "")).ToArray())
.ToArray();
if (StarbaseCount == 0)
{
var randomQuadrant = this[random.GetCoordinate()];
randomQuadrant.AddStarbase();
if (randomQuadrant.KlingonCount < 2)
{
randomQuadrant.AddKlingon();
}
}
}
public QuadrantInfo this[Coordinates coordinate] => _quadrants[coordinate.X - 1][coordinate.Y - 1];
public int KlingonCount => _quadrants.SelectMany(q => q).Sum(q => q.KlingonCount);
public int StarbaseCount => _quadrants.SelectMany(q => q).Count(q => q.HasStarbase);
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SuperStarTrek.Objects;
namespace SuperStarTrek.Space
{
internal class Quadrant
{
private readonly QuadrantInfo _info;
private readonly Random _random;
private readonly Dictionary<Coordinates, object> _sectors;
private readonly Coordinates _enterpriseSector;
private readonly Coordinates _starbaseSector;
public Quadrant(QuadrantInfo info, Enterprise enterprise)
{
_info = info;
_random = new Random();
_enterpriseSector = enterprise.Sector;
_sectors = new Dictionary<Coordinates, object> { [enterprise.Sector] = enterprise };
PositionObject(() => new Klingon(), _info.KlingonCount);
if (_info.HasStarbase)
{
_starbaseSector = PositionObject(() => new Starbase());
}
PositionObject(() => new Star(), _info.StarCount);
}
public Coordinates Coordinates => _info.Coordinates;
public bool HasKlingons => _info.KlingonCount > 0;
public bool HasStarbase => _info.HasStarbase;
public bool EnterpriseIsNextToStarbase =>
_info.HasStarbase &&
Math.Abs(_enterpriseSector.X - _starbaseSector.X) <= 1 &&
Math.Abs(_enterpriseSector.Y - _starbaseSector.Y) <= 1;
public override string ToString() => _info.Name;
private Coordinates PositionObject(Func<object> objectFactory)
{
var sector = GetRandomEmptySector();
_sectors[sector] = objectFactory.Invoke();
return sector;
}
private void PositionObject(Func<object> objectFactory, int count)
{
for (int i = 0; i < count; i++)
{
PositionObject(objectFactory);
}
}
private Coordinates GetRandomEmptySector()
{
while (true)
{
var sector = _random.GetCoordinate();
if (!_sectors.ContainsKey(sector))
{
return sector;
}
}
}
public IEnumerable<string> GetDisplayLines() => Enumerable.Range(1, 8).Select(x => GetDisplayLine(x));
private string GetDisplayLine(int x)
=> string.Join(
" ",
Enumerable
.Range(1, 8)
.Select(y => new Coordinates(x, y))
.Select(c => _sectors.GetValueOrDefault(c))
.Select(o => o?.ToString() ?? " "));
}
}

View File

@@ -0,0 +1,40 @@
namespace SuperStarTrek.Space
{
internal class QuadrantInfo
{
private QuadrantInfo(Coordinates coordinates, string name, int klingonCount, int starCount, bool hasStarbase)
{
Coordinates = coordinates;
Name = name;
KlingonCount = klingonCount;
StarCount = starCount;
HasStarbase = hasStarbase;
}
public Coordinates Coordinates { get; }
public string Name { get; }
public int KlingonCount { get; private set; }
public bool HasStarbase { get; private set; }
public int StarCount { get; }
public static QuadrantInfo Create(Coordinates coordinates, string name)
{
var random = new Random();
var klingonCount = random.GetDouble() switch
{
> 0.98 => 3,
> 0.95 => 2,
> 0.80 => 1,
_ => 0
};
var hasStarbase = random.GetDouble() > 0.96;
var starCount = random.Get1To8Inclusive();
return new QuadrantInfo(coordinates, name, klingonCount, starCount, hasStarbase);
}
internal void AddKlingon() => KlingonCount += 1;
internal void AddStarbase() => HasStarbase = true;
}
}

View File

@@ -0,0 +1,53 @@
using SuperStarTrek.Objects;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems
{
internal class ShieldControl : Subsystem
{
private readonly Enterprise _enterprise;
private readonly Output _output;
private readonly Input _input;
public ShieldControl(Enterprise enterprise, Output output, Input input)
: base("Shield Control", Command.SHE)
{
_enterprise = enterprise;
_output = output;
_input = input;
}
public double Energy { get; private set; }
public override void ExecuteCommand(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;
}
_output.WriteLine("<SHIELDS UNCHANGED>");
}
private bool Validate(double requested)
{
if (requested > _enterprise.TotalEnergy)
{
_output.WriteLine("Shield Control reports, 'This is not the Federation Treasury.'");
return false;
}
return requested >= 0 && requested != Energy;
}
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SuperStarTrek.Objects;
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems
{
internal class ShortRangeSensors : Subsystem
{
private readonly Enterprise _enterprise;
private readonly Galaxy _galaxy;
private readonly Game _game;
private readonly Output _output;
public ShortRangeSensors(Enterprise enterprise, Galaxy galaxy, Game game, Output output)
: base("Short Range Sensors", Command.SRS)
{
_enterprise = enterprise;
_galaxy = galaxy;
_game = game;
_output = output;
}
public override void ExecuteCommand(Quadrant quadrant)
{
if (_enterprise.IsDocked)
{
_output.WriteLine(Strings.ShieldsDropped);
}
if (Condition < 0)
{
_output.WriteLine(Strings.ShortRangeSensorsOut);
}
_output.WriteLine("---------------------------------");
quadrant.GetDisplayLines()
.Zip(GetStatusLines(), (sectors, status) => $" {sectors} {status}")
.ToList()
.ForEach(l => _output.WriteLine(l));
_output.WriteLine("---------------------------------");
}
public IEnumerable<string> 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 $"Klingons remaining {_galaxy.KlingonCount}";
}
}
}

View File

@@ -0,0 +1,20 @@
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems
{
internal abstract class Subsystem
{
protected Subsystem(string name, Command command)
{
Name = name;
Command = command;
Condition = 0;
}
public string Name { get; }
public double Condition { get; }
public Command Command { get; }
public abstract void ExecuteCommand(Quadrant quadrant);
}
}