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 // **** CONVERTED TO MICROSOFT C# 2/20/21 BY ANDREW COOPER
// **** // ****
using System;
using static System.StringComparison;
namespace SuperStarTrek 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)? "); game.DoIntroduction();
var response = Console.ReadLine();
Console.WriteLine();
if (!response.Equals("N", InvariantCultureIgnoreCase)) do
{ {
Console.WriteLine(Strings.Instructions); game.Play();
} while (game.Replay());
Console.WriteLine("Press <Enter> to continue...");
Console.ReadLine();
}
Console.WriteLine(Strings.Enterprise);
} }
} }
} }

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 The scan is coded in the form ###, where the units digit
is the number of stars, the tens digit is the number of is the number of stars, the tens digit is the number of
starbases, and the hundreds digit is the number of starbases, and the hundreds digit is the number of
Kilngons. Klingons.
Example - 207 = 2 Klingons, No starbases, & 7 stars. Example - 207 = 2 Klingons, No starbases, & 7 stars.
@@ -103,3 +103,4 @@ COM command = Library-Computer
Option 5 = Galactic Region Name Map Option 5 = Galactic Region Name Map
This option prints the names of the sixteen major This option prints the names of the sixteen major
galactic regions referred to in the game. 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.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using static System.StringComparison; namespace SuperStarTrek.Resources
namespace SuperStarTrek
{ {
internal static class Strings internal static class Strings
{ {
public static string Title => GetResource(); public static string CombatArea => GetResource();
public static string Instructions => GetResource();
public static string Enterprise => 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 = "") 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);
}
}