Merge pull request #673 from drewjcooper/rework-with-common-lib

Rework with common lib
This commit is contained in:
Jeff Atwood
2022-03-22 15:57:46 -05:00
committed by GitHub
99 changed files with 2531 additions and 2736 deletions

View File

@@ -12,4 +12,6 @@ public sealed class ConsoleIO : TextIO
: base(Console.In, Console.Out)
{
}
public override char ReadCharacter() => Console.ReadKey(intercept: true).KeyChar;
}

View File

@@ -8,6 +8,12 @@ namespace Games.Common.IO;
/// </summary>
public interface IReadWrite
{
/// <summary>
/// Reads a character from input.
/// </summary>
/// <returns>The character read.</returns>
char ReadCharacter();
/// <summary>
/// Reads a <see cref="float" /> value from input.
/// </summary>
@@ -82,9 +88,35 @@ public interface IReadWrite
/// <param name="value">The <see cref="float" /> to be written.</param>
void WriteLine(float value);
/// <summary>
/// Writes an <see cref="object" /> to output.
/// </summary>
/// <param name="value">The <see cref="object" /> to be written.</param>
void Write(object value);
/// <summary>
/// Writes an <see cref="object" /> to output.
/// </summary>
/// <param name="value">The <see cref="object" /> to be written.</param>
void WriteLine(object value);
/// <summary>
/// Writes a formatted string to output.
/// </summary>
/// <param name="format">The format <see cref="string" /> to be written.</param>
/// <param name="value">The values to be inserted into the format.</param>
void Write(string format, params object[] values);
/// <summary>
/// Writes a formatted string to output followed by a new-line.
/// </summary>
/// <param name="format">The format <see cref="string" /> to be written.</param>
/// <param name="value">The values to be inserted into the format.</param>
void WriteLine(string format, params object[] values);
/// <summary>
/// Writes the contents of a <see cref="Stream" /> to output.
/// </summary>
/// <param name="stream">The <see cref="Stream" /> to be written.</param>
void Write(Stream stream);
void Write(Stream stream, bool keepOpen = false);
}

View File

@@ -29,6 +29,15 @@ public class TextIO : IReadWrite
_numberTokenReader = TokenReader.ForNumbers(this);
}
public virtual char ReadCharacter()
{
while(true)
{
var ch = _input.Read();
if (ch != -1) { return (char)ch; }
}
}
public float ReadNumber(string prompt) => ReadNumbers(prompt, 1)[0];
public (float, float) Read2Numbers(string prompt)
@@ -95,13 +104,23 @@ public class TextIO : IReadWrite
public void WriteLine(float value) => _output.WriteLine(GetString(value));
public void Write(Stream stream)
public void Write(object value) => _output.Write(value.ToString());
public void WriteLine(object value) => _output.WriteLine(value.ToString());
public void Write(string format, params object[] values) => _output.Write(format, values);
public void WriteLine(string format, params object[] values) => _output.WriteLine(format, values);
public void Write(Stream stream, bool keepOpen = false)
{
using var reader = new StreamReader(stream);
while (!reader.EndOfStream)
{
_output.WriteLine(reader.ReadLine());
}
if (!keepOpen) { stream?.Dispose(); }
}
private string GetString(float value) => value < 0 ? $"{value} " : $" {value} ";

View File

@@ -6,68 +6,67 @@ using System.Text;
using static Hexapawn.Pawn;
namespace Hexapawn
namespace Hexapawn;
internal class Board : IEnumerable<Pawn>, IEquatable<Board>
{
internal class Board : IEnumerable<Pawn>, IEquatable<Board>
private readonly Pawn[] _cells;
public Board()
{
private readonly Pawn[] _cells;
public Board()
_cells = new[]
{
_cells = new[]
Black, Black, Black,
None, None, None,
White, White, White
};
}
public Board(params Pawn[] cells)
{
_cells = cells;
}
public Pawn this[int index]
{
get => _cells[index - 1];
set => _cells[index - 1] = value;
}
public Board Reflected => new(Cell.AllCells.Select(c => this[c.Reflected]).ToArray());
public IEnumerator<Pawn> GetEnumerator() => _cells.OfType<Pawn>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public override string ToString()
{
var builder = new StringBuilder().AppendLine();
for (int row = 0; row < 3; row++)
{
builder.Append(" ");
for (int col = 0; col < 3; col++)
{
Black, Black, Black,
None, None, None,
White, White, White
};
}
public Board(params Pawn[] cells)
{
_cells = cells;
}
public Pawn this[int index]
{
get => _cells[index - 1];
set => _cells[index - 1] = value;
}
public Board Reflected => new(Cell.AllCells.Select(c => this[c.Reflected]).ToArray());
public IEnumerator<Pawn> GetEnumerator() => _cells.OfType<Pawn>().GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public override string ToString()
{
var builder = new StringBuilder().AppendLine();
for (int row = 0; row < 3; row++)
{
builder.Append(" ");
for (int col = 0; col < 3; col++)
{
builder.Append(_cells[row * 3 + col]);
}
builder.AppendLine();
builder.Append(_cells[row * 3 + col]);
}
return builder.ToString();
builder.AppendLine();
}
return builder.ToString();
}
public bool Equals(Board other) => other?.Zip(this).All(x => x.First == x.Second) ?? false;
public bool Equals(Board other) => other?.Zip(this).All(x => x.First == x.Second) ?? false;
public override bool Equals(object obj) => Equals(obj as Board);
public override bool Equals(object obj) => Equals(obj as Board);
public override int GetHashCode()
public override int GetHashCode()
{
var hash = 19;
for (int i = 0; i < 9; i++)
{
var hash = 19;
for (int i = 0; i < 9; i++)
{
hash = hash * 53 + _cells[i].GetHashCode();
}
return hash;
hash = hash * 53 + _cells[i].GetHashCode();
}
return hash;
}
}

View File

@@ -1,53 +1,41 @@
using System;
using System.Collections.Generic;
namespace Hexapawn
namespace Hexapawn;
// Represents a cell on the board, numbered 1 to 9, with support for finding the reflection of the reference around
// the middle column of the board.
internal class Cell
{
// Represents a cell on the board, numbered 1 to 9, with support for finding the reflection of the reference around
// the middle column of the board.
internal class Cell
private static readonly Cell[] _cells = new Cell[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
private static readonly Cell[] _reflected = new Cell[] { 3, 2, 1, 6, 5, 4, 9, 8, 7 };
private readonly int _number;
private Cell(int number)
{
private static readonly Cell[] _cells = new Cell[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
private static readonly Cell[] _reflected = new Cell[] { 3, 2, 1, 6, 5, 4, 9, 8, 7 };
private readonly int _number;
private Cell(int number)
if (number < 1 || number > 9)
{
if (number < 1 || number > 9)
{
throw new ArgumentOutOfRangeException(nameof(number), number, "Must be from 1 to 9");
}
_number = number;
throw new ArgumentOutOfRangeException(nameof(number), number, "Must be from 1 to 9");
}
// Facilitates enumerating all the cells.
public static IEnumerable<Cell> AllCells => _cells;
// Takes a value input by the user and attempts to create a Cell reference
public static bool TryCreate(float input, out Cell cell)
{
if (IsInteger(input) && input >= 1 && input <= 9)
{
cell = (int)input;
return true;
}
cell = default;
return false;
static bool IsInteger(float value) => value - (int)value == 0;
}
// Returns the reflection of the cell reference about the middle column of the board.
public Cell Reflected => _reflected[_number - 1];
// Allows the cell reference to be used where an int is expected, such as the indexer in Board.
public static implicit operator int(Cell c) => c._number;
public static implicit operator Cell(int number) => new(number);
public override string ToString() => _number.ToString();
_number = number;
}
// Facilitates enumerating all the cells.
public static IEnumerable<Cell> AllCells => _cells;
// Takes a value input by the user and attempts to create a Cell reference
public static bool TryCreate(float input, out Cell cell)
{
if (IsInteger(input) && input >= 1 && input <= 9)
{
cell = (int)input;
return true;
}
cell = default;
return false;
static bool IsInteger(float value) => value - (int)value == 0;
}
// Returns the reflection of the cell reference about the middle column of the board.
public Cell Reflected => _reflected[_number - 1];
// Allows the cell reference to be used where an int is expected, such as the indexer in Board.
public static implicit operator int(Cell c) => c._number;
public static implicit operator Cell(int number) => new(number);
public override string ToString() => _number.ToString();
}

View File

@@ -1,146 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Games.Common.IO;
using Games.Common.Randomness;
using static Hexapawn.Pawn;
using static Hexapawn.Cell;
namespace Hexapawn
namespace Hexapawn;
/// <summary>
/// Encapsulates the logic of the computer player.
/// </summary>
internal class Computer
{
/// <summary>
/// Encapsulates the logic of the computer player.
/// </summary>
internal class Computer : IPlayer
private readonly TextIO _io;
private readonly IRandom _random;
private readonly Dictionary<Board, List<Move>> _potentialMoves;
private (List<Move>, Move) _lastMove;
public Computer(TextIO io, IRandom random)
{
private readonly Random _random = new();
private readonly Dictionary<Board, List<Move>> _potentialMoves;
private (List<Move>, Move) _lastMove;
_io = io;
_random = random;
public Computer()
// This dictionary implements the data in the original code, which encodes board positions for which the
// computer has a legal move, and the list of possible moves for each position:
// 900 DATA -1,-1,-1,1,0,0,0,1,1,-1,-1,-1,0,1,0,1,0,1
// 905 DATA -1,0,-1,-1,1,0,0,0,1,0,-1,-1,1,-1,0,0,0,1
// 910 DATA -1,0,-1,1,1,0,0,1,0,-1,-1,0,1,0,1,0,0,1
// 915 DATA 0,-1,-1,0,-1,1,1,0,0,0,-1,-1,-1,1,1,1,0,0
// 920 DATA -1,0,-1,-1,0,1,0,1,0,0,-1,-1,0,1,0,0,0,1
// 925 DATA 0,-1,-1,0,1,0,1,0,0,-1,0,-1,1,0,0,0,0,1
// 930 DATA 0,0,-1,-1,-1,1,0,0,0,-1,0,0,1,1,1,0,0,0
// 935 DATA 0,-1,0,-1,1,1,0,0,0,-1,0,0,-1,-1,1,0,0,0
// 940 DATA 0,0,-1,-1,1,0,0,0,0,0,-1,0,1,-1,0,0,0,0
// 945 DATA -1,0,0,-1,1,0,0,0,0
// 950 DATA 24,25,36,0,14,15,36,0,15,35,36,47,36,58,59,0
// 955 DATA 15,35,36,0,24,25,26,0,26,57,58,0
// 960 DATA 26,35,0,0,47,48,0,0,35,36,0,0,35,36,0,0
// 965 DATA 36,0,0,0,47,58,0,0,15,0,0,0
// 970 DATA 26,47,0,0,47,58,0,0,35,36,47,0,28,58,0,0,15,47,0,0
//
// The original code loaded this data into two arrays.
// 40 FOR I=1 TO 19: FOR J=1 TO 9: READ B(I,J): NEXT J: NEXT I
// 45 FOR I=1 TO 19: FOR J=1 TO 4: READ M(I,J): NEXT J: NEXT I
//
// When finding moves for the computer the first array was searched for the current board position, or the
// reflection of it, and the resulting index was used in the second array to get the possible moves.
// With this dictionary we can just use the current board as the index, and retrieve a list of moves for
// consideration by the computer.
_potentialMoves = new()
{
// This dictionary implements the data in the original code, which encodes board positions for which the
// computer has a legal move, and the list of possible moves for each position:
// 900 DATA -1,-1,-1,1,0,0,0,1,1,-1,-1,-1,0,1,0,1,0,1
// 905 DATA -1,0,-1,-1,1,0,0,0,1,0,-1,-1,1,-1,0,0,0,1
// 910 DATA -1,0,-1,1,1,0,0,1,0,-1,-1,0,1,0,1,0,0,1
// 915 DATA 0,-1,-1,0,-1,1,1,0,0,0,-1,-1,-1,1,1,1,0,0
// 920 DATA -1,0,-1,-1,0,1,0,1,0,0,-1,-1,0,1,0,0,0,1
// 925 DATA 0,-1,-1,0,1,0,1,0,0,-1,0,-1,1,0,0,0,0,1
// 930 DATA 0,0,-1,-1,-1,1,0,0,0,-1,0,0,1,1,1,0,0,0
// 935 DATA 0,-1,0,-1,1,1,0,0,0,-1,0,0,-1,-1,1,0,0,0
// 940 DATA 0,0,-1,-1,1,0,0,0,0,0,-1,0,1,-1,0,0,0,0
// 945 DATA -1,0,0,-1,1,0,0,0,0
// 950 DATA 24,25,36,0,14,15,36,0,15,35,36,47,36,58,59,0
// 955 DATA 15,35,36,0,24,25,26,0,26,57,58,0
// 960 DATA 26,35,0,0,47,48,0,0,35,36,0,0,35,36,0,0
// 965 DATA 36,0,0,0,47,58,0,0,15,0,0,0
// 970 DATA 26,47,0,0,47,58,0,0,35,36,47,0,28,58,0,0,15,47,0,0
//
// The original code loaded this data into two arrays.
// 40 FOR I=1 TO 19: FOR J=1 TO 9: READ B(I,J): NEXT J: NEXT I
// 45 FOR I=1 TO 19: FOR J=1 TO 4: READ M(I,J): NEXT J: NEXT I
//
// When finding moves for the computer the first array was searched for the current board position, or the
// reflection of it, and the resulting index was used in the second array to get the possible moves.
// With this dictionary we can just use the current board as the index, and retrieve a list of moves for
// consideration by the computer.
_potentialMoves = new()
{
[new(Black, Black, Black, White, None, None, None, White, White)] = Moves((2, 4), (2, 5), (3, 6)),
[new(Black, Black, Black, None, White, None, White, None, White)] = Moves((1, 4), (1, 5), (3, 6)),
[new(Black, None, Black, Black, White, None, None, None, White)] = Moves((1, 5), (3, 5), (3, 6), (4, 7)),
[new(None, Black, Black, White, Black, None, None, None, White)] = Moves((3, 6), (5, 8), (5, 9)),
[new(Black, None, Black, White, White, None, None, White, None)] = Moves((1, 5), (3, 5), (3, 6)),
[new(Black, Black, None, White, None, White, None, None, White)] = Moves((2, 4), (2, 5), (2, 6)),
[new(None, Black, Black, None, Black, White, White, None, None)] = Moves((2, 6), (5, 7), (5, 8)),
[new(None, Black, Black, Black, White, White, White, None, None)] = Moves((2, 6), (3, 5)),
[new(Black, None, Black, Black, None, White, None, White, None)] = Moves((4, 7), (4, 8)),
[new(None, Black, Black, None, White, None, None, None, White)] = Moves((3, 5), (3, 6)),
[new(None, Black, Black, None, White, None, White, None, None)] = Moves((3, 5), (3, 6)),
[new(Black, None, Black, White, None, None, None, None, White)] = Moves((3, 6)),
[new(None, None, Black, Black, Black, White, None, None, None)] = Moves((4, 7), (5, 8)),
[new(Black, None, None, White, White, White, None, None, None)] = Moves((1, 5)),
[new(None, Black, None, Black, White, White, None, None, None)] = Moves((2, 6), (4, 7)),
[new(Black, None, None, Black, Black, White, None, None, None)] = Moves((4, 7), (5, 8)),
[new(None, None, Black, Black, White, None, None, None, None)] = Moves((3, 5), (3, 6), (4, 7)),
[new(None, Black, None, White, Black, None, None, None, None)] = Moves((2, 8), (5, 8)),
[new(Black, None, None, Black, White, None, None, None, None)] = Moves((1, 5), (4, 7))
};
}
public int Wins { get; private set; }
public void AddWin() => Wins++;
// Try to make a move. We first try to find a legal move for the current board position.
public bool TryMove(Board board)
{
if (TryGetMoves(board, out var moves, out var reflected) &&
TrySelectMove(moves, out var move))
{
// We've found a move, so we record it as the last move made, and then announce and make the move.
_lastMove = (moves, move);
// If we found the move from a reflacted match of the board we need to make the reflected move.
if (reflected) { move = move.Reflected; }
Console.WriteLine($"I move {move}");
move.Execute(board);
return true;
}
// We haven't found a move for this board position, so remove the previous move that led to this board
// position from future consideration. We don't want to make that move again, because we now know it's a
// non-winning move.
ExcludeLastMoveFromFuturePlay();
return false;
}
// Looks up the given board and its reflection in the potential moves dictionary. If it's found then we have a
// list of potential moves. If the board is not found in the dictionary then the computer has no legal moves,
// and the human player wins.
private bool TryGetMoves(Board board, out List<Move> moves, out bool reflected)
{
if (_potentialMoves.TryGetValue(board, out moves))
{
reflected = false;
return true;
}
if (_potentialMoves.TryGetValue(board.Reflected, out moves))
{
reflected = true;
return true;
}
reflected = default;
return false;
}
// Get a random move from the list. If the list is empty, then we've previously eliminated all the moves for
// this board position as being non-winning moves. We therefore resign the game.
private bool TrySelectMove(List<Move> moves, out Move move)
{
if (moves.Any())
{
move = moves[_random.Next(moves.Count)];
return true;
}
Console.Write("I resign.");
move = null;
return false;
}
private void ExcludeLastMoveFromFuturePlay()
{
var (moves, move) = _lastMove;
moves.Remove(move);
}
private static List<Move> Moves(params Move[] moves) => moves.ToList();
public bool IsFullyAdvanced(Board board) =>
board[9] == Black || board[8] == Black || board[7] == Black;
[new(Black, Black, Black, White, None, None, None, White, White)] = Moves((2, 4), (2, 5), (3, 6)),
[new(Black, Black, Black, None, White, None, White, None, White)] = Moves((1, 4), (1, 5), (3, 6)),
[new(Black, None, Black, Black, White, None, None, None, White)] = Moves((1, 5), (3, 5), (3, 6), (4, 7)),
[new(None, Black, Black, White, Black, None, None, None, White)] = Moves((3, 6), (5, 8), (5, 9)),
[new(Black, None, Black, White, White, None, None, White, None)] = Moves((1, 5), (3, 5), (3, 6)),
[new(Black, Black, None, White, None, White, None, None, White)] = Moves((2, 4), (2, 5), (2, 6)),
[new(None, Black, Black, None, Black, White, White, None, None)] = Moves((2, 6), (5, 7), (5, 8)),
[new(None, Black, Black, Black, White, White, White, None, None)] = Moves((2, 6), (3, 5)),
[new(Black, None, Black, Black, None, White, None, White, None)] = Moves((4, 7), (4, 8)),
[new(None, Black, Black, None, White, None, None, None, White)] = Moves((3, 5), (3, 6)),
[new(None, Black, Black, None, White, None, White, None, None)] = Moves((3, 5), (3, 6)),
[new(Black, None, Black, White, None, None, None, None, White)] = Moves((3, 6)),
[new(None, None, Black, Black, Black, White, None, None, None)] = Moves((4, 7), (5, 8)),
[new(Black, None, None, White, White, White, None, None, None)] = Moves((1, 5)),
[new(None, Black, None, Black, White, White, None, None, None)] = Moves((2, 6), (4, 7)),
[new(Black, None, None, Black, Black, White, None, None, None)] = Moves((4, 7), (5, 8)),
[new(None, None, Black, Black, White, None, None, None, None)] = Moves((3, 5), (3, 6), (4, 7)),
[new(None, Black, None, White, Black, None, None, None, None)] = Moves((2, 8), (5, 8)),
[new(Black, None, None, Black, White, None, None, None, None)] = Moves((1, 5), (4, 7))
};
}
// Try to make a move. We first try to find a legal move for the current board position.
public bool TryMove(Board board)
{
if (TryGetMoves(board, out var moves, out var reflected) &&
TrySelectMove(moves, out var move))
{
// We've found a move, so we record it as the last move made, and then announce and make the move.
_lastMove = (moves, move);
// If we found the move from a reflacted match of the board we need to make the reflected move.
if (reflected) { move = move.Reflected; }
_io.WriteLine($"I move {move}");
move.Execute(board);
return true;
}
// We haven't found a move for this board position, so remove the previous move that led to this board
// position from future consideration. We don't want to make that move again, because we now know it's a
// non-winning move.
ExcludeLastMoveFromFuturePlay();
return false;
}
// Looks up the given board and its reflection in the potential moves dictionary. If it's found then we have a
// list of potential moves. If the board is not found in the dictionary then the computer has no legal moves,
// and the human player wins.
private bool TryGetMoves(Board board, out List<Move> moves, out bool reflected)
{
if (_potentialMoves.TryGetValue(board, out moves))
{
reflected = false;
return true;
}
if (_potentialMoves.TryGetValue(board.Reflected, out moves))
{
reflected = true;
return true;
}
reflected = default;
return false;
}
// Get a random move from the list. If the list is empty, then we've previously eliminated all the moves for
// this board position as being non-winning moves. We therefore resign the game.
private bool TrySelectMove(List<Move> moves, out Move move)
{
if (moves.Any())
{
move = moves[_random.Next(moves.Count)];
return true;
}
_io.WriteLine("I resign.");
move = null;
return false;
}
private void ExcludeLastMoveFromFuturePlay()
{
var (moves, move) = _lastMove;
moves.Remove(move);
}
private static List<Move> Moves(params Move[] moves) => moves.ToList();
public bool IsFullyAdvanced(Board board) =>
board[9] == Black || board[8] == Black || board[7] == Black;
}

View File

@@ -1,48 +1,40 @@
using System;
using Games.Common.IO;
namespace Hexapawn
namespace Hexapawn;
// A single game of Hexapawn
internal class Game
{
// Runs a single game of Hexapawn
internal class Game
private readonly TextIO _io;
private readonly Board _board;
public Game(TextIO io)
{
private readonly Board _board;
private readonly Human _human;
private readonly Computer _computer;
_board = new Board();
_io = io;
}
public Game(Human human, Computer computer)
public object Play(Human human, Computer computer)
{
_io.WriteLine(_board);
while(true)
{
_board = new Board();
_human = human;
_computer = computer;
}
public IPlayer Play()
{
Console.WriteLine(_board);
while(true)
human.Move(_board);
_io.WriteLine(_board);
if (!computer.TryMove(_board))
{
_human.Move(_board);
Console.WriteLine(_board);
if (!_computer.TryMove(_board))
{
return _human;
}
Console.WriteLine(_board);
if (_computer.IsFullyAdvanced(_board) || _human.HasNoPawns(_board))
{
return _computer;
}
if (!_human.HasLegalMove(_board))
{
Console.Write("You can't move, so ");
return _computer;
}
return human;
}
_io.WriteLine(_board);
if (computer.IsFullyAdvanced(_board) || human.HasNoPawns(_board))
{
return computer;
}
if (!human.HasLegalMove(_board))
{
_io.Write("You can't move, so ");
return computer;
}
}
}

View File

@@ -1,27 +1,47 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Games.Common.IO;
using Games.Common.Randomness;
using Hexapawn.Resources;
namespace Hexapawn
namespace Hexapawn;
// Runs series of games between the computer and the human player
internal class GameSeries
{
// Runs series of games between the computer and the human player
internal class GameSeries
private readonly TextIO _io;
private readonly Computer _computer;
private readonly Human _human;
private readonly Dictionary<object, int> _wins;
public GameSeries(TextIO io, IRandom random)
{
private readonly Computer _computer = new();
private readonly Human _human = new();
_io = io;
_computer = new(io, random);
_human = new(io);
_wins = new() { [_computer] = 0, [_human] = 0 };
}
public void Play()
public void Play()
{
_io.Write(Resource.Streams.Title);
if (_io.GetYesNo("Instructions") == 'Y')
{
while (true)
{
var game = new Game(_human, _computer);
_io.Write(Resource.Streams.Instructions);
}
var winner = game.Play();
winner.AddWin();
Console.WriteLine(winner == _computer ? "I win." : "You win.");
while (true)
{
var game = new Game(_io);
Console.Write($"I have won {_computer.Wins} and you {_human.Wins}");
Console.WriteLine($" out of {_computer.Wins + _human.Wins} games.");
Console.WriteLine();
}
var winner = game.Play(_human, _computer);
_wins[winner]++;
_io.WriteLine(winner == _computer ? "I win." : "You win.");
_io.Write($"I have won {_wins[_computer]} and you {_wins[_human]}");
_io.WriteLine($" out of {_wins.Values.Sum()} games.");
_io.WriteLine();
}
}
}

View File

@@ -2,7 +2,15 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\*.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\00_Common\dotnet\Games.Common\Games.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,64 +1,67 @@
using System;
using System.Linq;
using Games.Common.IO;
using static Hexapawn.Cell;
using static Hexapawn.Move;
using static Hexapawn.Pawn;
namespace Hexapawn
namespace Hexapawn;
internal class Human
{
internal class Human : IPlayer
private readonly TextIO _io;
public Human(TextIO io)
{
public int Wins { get; private set; }
_io = io;
}
public void Move(Board board)
public void Move(Board board)
{
while (true)
{
while (true)
{
var move = Input.GetMove("Your move");
var move = _io.ReadMove("Your move");
if (TryExecute(board, move)) { return; }
if (TryExecute(board, move)) { return; }
Console.WriteLine("Illegal move.");
}
}
public void AddWin() => Wins++;
public bool HasLegalMove(Board board)
{
foreach (var from in AllCells.Where(c => c > 3))
{
if (board[from] != White) { continue; }
if (HasLegalMove(board, from))
{
return true;
}
}
return false;
}
private bool HasLegalMove(Board board, Cell from) =>
Right(from).IsRightDiagonalToCapture(board) ||
Straight(from).IsStraightMoveToEmptySpace(board) ||
from > 4 && Left(from).IsLeftDiagonalToCapture(board);
public bool HasNoPawns(Board board) => board.All(c => c != White);
public bool TryExecute(Board board, Move move)
{
if (board[move.From] != White) { return false; }
if (move.IsStraightMoveToEmptySpace(board) ||
move.IsLeftDiagonalToCapture(board) ||
move.IsRightDiagonalToCapture(board))
{
move.Execute(board);
return true;
}
return false;
_io.WriteLine("Illegal move.");
}
}
public bool HasLegalMove(Board board)
{
foreach (var from in AllCells.Where(c => c > 3))
{
if (board[from] != White) { continue; }
if (HasLegalMove(board, from))
{
return true;
}
}
return false;
}
private bool HasLegalMove(Board board, Cell from) =>
Right(from).IsRightDiagonalToCapture(board) ||
Straight(from).IsStraightMoveToEmptySpace(board) ||
from > 4 && Left(from).IsLeftDiagonalToCapture(board);
public bool HasNoPawns(Board board) => board.All(c => c != White);
public bool TryExecute(Board board, Move move)
{
if (board[move.From] != White) { return false; }
if (move.IsStraightMoveToEmptySpace(board) ||
move.IsLeftDiagonalToCapture(board) ||
move.IsRightDiagonalToCapture(board))
{
move.Execute(board);
return true;
}
return false;
}
}

View File

@@ -1,8 +0,0 @@
namespace Hexapawn
{
// An interface implemented by a player of the game to track the number of wins.
internal interface IPlayer
{
void AddWin();
}
}

View File

@@ -0,0 +1,42 @@
using System;
using System.Linq;
using Games.Common.IO;
namespace Hexapawn;
// Provides input methods which emulate the BASIC interpreter's keyboard input routines
internal static class IReadWriteExtensions
{
internal static char GetYesNo(this IReadWrite io, string prompt)
{
while (true)
{
var response = io.ReadString($"{prompt} (Y-N)").FirstOrDefault();
if ("YyNn".Contains(response))
{
return char.ToUpperInvariant(response);
}
}
}
// Implements original code:
// 120 PRINT "YOUR MOVE";
// 121 INPUT M1,M2
// 122 IF M1=INT(M1)AND M2=INT(M2)AND M1>0 AND M1<10 AND M2>0 AND M2<10 THEN 130
// 123 PRINT "ILLEGAL CO-ORDINATES."
// 124 GOTO 120
internal static Move ReadMove(this IReadWrite io, string prompt)
{
while(true)
{
var (from, to) = io.Read2Numbers(prompt);
if (Move.TryCreate(from, to, out var move))
{
return move;
}
io.WriteLine("Illegal Coordinates.");
}
}
}

View File

@@ -1,112 +0,0 @@
using System;
using System.Linq;
namespace Hexapawn
{
// Provides input methods which emulate the BASIC interpreter's keyboard input routines
internal static class Input
{
internal static char GetYesNo(string prompt)
{
while (true)
{
Console.Write($"{prompt} (Y-N)? ");
var response = Console.ReadLine().FirstOrDefault();
if ("YyNn".Contains(response))
{
return char.ToUpperInvariant(response);
}
}
}
// Implements original code:
// 120 PRINT "YOUR MOVE";
// 121 INPUT M1,M2
// 122 IF M1=INT(M1)AND M2=INT(M2)AND M1>0 AND M1<10 AND M2>0 AND M2<10 THEN 130
// 123 PRINT "ILLEGAL CO-ORDINATES."
// 124 GOTO 120
internal static Move GetMove(string prompt)
{
while(true)
{
ReadNumbers(prompt, out var from, out var to);
if (Move.TryCreate(from, to, out var move))
{
return move;
}
Console.WriteLine("Illegal Coordinates.");
}
}
internal static void Prompt(string text = "") => Console.Write($"{text}? ");
internal static void ReadNumbers(string prompt, out float number1, out float number2)
{
while (!TryReadNumbers(prompt, out number1, out number2))
{
prompt = "";
}
}
private static bool TryReadNumbers(string prompt, out float number1, out float number2)
{
Prompt(prompt);
var inputValues = ReadStrings();
if (!TryParseNumber(inputValues[0], out number1))
{
number2 = default;
return false;
}
if (inputValues.Length == 1)
{
return TryReadNumber("?", out number2);
}
if (!TryParseNumber(inputValues[1], out number2))
{
number2 = default;
return false;
}
if (inputValues.Length > 2)
{
Console.WriteLine("!Extra input ingored");
}
return true;
}
private static bool TryReadNumber(string prompt, out float number)
{
Prompt(prompt);
var inputValues = ReadStrings();
if (!TryParseNumber(inputValues[0], out number))
{
return false;
}
if (inputValues.Length > 1)
{
Console.WriteLine("!Extra input ingored");
}
return true;
}
private static string[] ReadStrings() => Console.ReadLine().Split(',', StringSplitOptions.TrimEntries);
private static bool TryParseNumber(string text, out float number)
{
if (float.TryParse(text, out number)) { return true; }
Console.WriteLine("!Number expected - retry input line");
number = default;
return false;
}
}
}

View File

@@ -1,68 +1,67 @@
using static Hexapawn.Pawn;
namespace Hexapawn
namespace Hexapawn;
/// <summary>
/// Represents a move which may, or may not, be legal.
/// </summary>
internal class Move
{
/// <summary>
/// Represents a move which may, or may not, be legal.
/// </summary>
internal class Move
private readonly Cell _from;
private readonly Cell _to;
private readonly int _metric;
public Move(Cell from, Cell to)
{
private readonly Cell _from;
private readonly Cell _to;
private readonly int _metric;
public Move(Cell from, Cell to)
{
_from = from;
_to = to;
_metric = _from - _to;
}
public void Deconstruct(out Cell from, out Cell to)
{
from = _from;
to = _to;
}
public Cell From => _from;
// Produces the mirror image of the current moved, reflected around the central column of the board.
public Move Reflected => (_from.Reflected, _to.Reflected);
// Allows a tuple of two ints to be implicitly converted to a Move.
public static implicit operator Move((int From, int To) value) => new(value.From, value.To);
// Takes floating point coordinates, presumably from keyboard input, and attempts to create a Move object.
public static bool TryCreate(float input1, float input2, out Move move)
{
if (Cell.TryCreate(input1, out var from) &&
Cell.TryCreate(input2, out var to))
{
move = (from, to);
return true;
}
move = default;
return false;
}
public static Move Right(Cell from) => (from, from - 2);
public static Move Straight(Cell from) => (from, from - 3);
public static Move Left(Cell from) => (from, from - 4);
public bool IsStraightMoveToEmptySpace(Board board) => _metric == 3 && board[_to] == None;
public bool IsLeftDiagonalToCapture(Board board) => _metric == 4 && _from != 7 && board[_to] == Black;
public bool IsRightDiagonalToCapture(Board board) =>
_metric == 2 && _from != 9 && _from != 6 && board[_to] == Black;
public void Execute(Board board)
{
board[_to] = board[_from];
board[_from] = None;
}
public override string ToString() => $"from {_from} to {_to}";
_from = from;
_to = to;
_metric = _from - _to;
}
public void Deconstruct(out Cell from, out Cell to)
{
from = _from;
to = _to;
}
public Cell From => _from;
// Produces the mirror image of the current moved, reflected around the central column of the board.
public Move Reflected => (_from.Reflected, _to.Reflected);
// Allows a tuple of two ints to be implicitly converted to a Move.
public static implicit operator Move((int From, int To) value) => new(value.From, value.To);
// Takes floating point coordinates, presumably from keyboard input, and attempts to create a Move object.
public static bool TryCreate(float input1, float input2, out Move move)
{
if (Cell.TryCreate(input1, out var from) &&
Cell.TryCreate(input2, out var to))
{
move = (from, to);
return true;
}
move = default;
return false;
}
public static Move Right(Cell from) => (from, from - 2);
public static Move Straight(Cell from) => (from, from - 3);
public static Move Left(Cell from) => (from, from - 4);
public bool IsStraightMoveToEmptySpace(Board board) => _metric == 3 && board[_to] == None;
public bool IsLeftDiagonalToCapture(Board board) => _metric == 4 && _from != 7 && board[_to] == Black;
public bool IsRightDiagonalToCapture(Board board) =>
_metric == 2 && _from != 9 && _from != 6 && board[_to] == Black;
public void Execute(Board board)
{
board[_to] = board[_from];
board[_from] = None;
}
public override string ToString() => $"from {_from} to {_to}";
}

View File

@@ -1,19 +1,19 @@
namespace Hexapawn
namespace Hexapawn;
// Represents the contents of a cell on the board
internal class Pawn
{
// Represents the contents of a cell on the board
internal class Pawn
public static readonly Pawn Black = new('X');
public static readonly Pawn White = new('O');
public static readonly Pawn None = new('.');
private readonly char _symbol;
private Pawn(char symbol)
{
public static readonly Pawn Black = new('X');
public static readonly Pawn White = new('O');
public static readonly Pawn None = new('.');
private readonly char _symbol;
private Pawn(char symbol)
{
_symbol = symbol;
}
public override string ToString() => _symbol.ToString();
_symbol = symbol;
}
public override string ToString() => _symbol.ToString();
}

View File

@@ -1,71 +1,6 @@
using System;
using Games.Common.IO;
using Games.Common.Randomness;
using Hexapawn;
namespace Hexapawn
{
// Hexapawn: Interpretation of hexapawn game as presented in
// Martin Gardner's "The Unexpected Hanging and Other Mathematic
// al Diversions", Chapter Eight: A Matchbox Game-Learning Machine.
// Original version for H-P timeshare system by R.A. Kaapke 5/5/76
// Instructions by Jeff Dalton
// Conversion to MITS BASIC by Steve North
// Conversion to C# by Andrew Cooper
class Program
{
static void Main()
{
DisplayTitle();
new GameSeries(new ConsoleIO(), new RandomNumberGenerator()).Play();
if (Input.GetYesNo("Instructions") == 'Y')
{
DisplayInstructions();
}
var games = new GameSeries();
games.Play();
}
private static void DisplayTitle()
{
Console.WriteLine(" Hexapawn");
Console.WriteLine(" Creative Computing Morristown, New Jersey");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine();
}
private static void DisplayInstructions()
{
Console.WriteLine();
Console.WriteLine("This program plays the game of Hexapawn.");
Console.WriteLine("Hexapawn is played with Chess pawns on a 3 by 3 board.");
Console.WriteLine("The pawns are move as in Chess - one space forward to");
Console.WriteLine("an empty space, or one space forward and diagonally to");
Console.WriteLine("capture an opposing man. On the board, your pawns");
Console.WriteLine("are 'O', the computer's pawns are 'X', and empty");
Console.WriteLine("squares are '.'. To enter a move, type the number of");
Console.WriteLine("the square you are moving from, followed by the number");
Console.WriteLine("of the square you will move to. The numbers must be");
Console.WriteLine("separated by a comma.");
Console.WriteLine();
Console.WriteLine("The computer starts a series of games knowing only when");
Console.WriteLine("the game is won (a draw is impossible) and how to move.");
Console.WriteLine("It has no strategy at first and just moves randomly.");
Console.WriteLine("However, it learns from each game. Thus winning becomes");
Console.WriteLine("more and more difficult. Also, to help offset your");
Console.WriteLine("initial advantage, you will not be told how to win the");
Console.WriteLine("game but must learn this by playing.");
Console.WriteLine();
Console.WriteLine("The numbering of the board is as follows:");
Console.WriteLine(" 123");
Console.WriteLine(" 456");
Console.WriteLine(" 789");
Console.WriteLine();
Console.WriteLine("For example, to move your rightmost pawn forward,");
Console.WriteLine("you would type 9,6 in response to the question");
Console.WriteLine("'Your move ?'. Since I'm a good sport, you'll always");
Console.WriteLine("go first.");
Console.WriteLine();
}
}
}

View File

@@ -0,0 +1,30 @@
This program plays the game of Hexapawn.
Hexapawn is played with Chess pawns on a 3 by 3 board.
The pawns are move as in Chess - one space forward to
an empty space, or one space forward and diagonally to
capture an opposing man. On the board, your pawns
are 'O', the computer's pawns are 'X', and empty
squares are '.'. To enter a move, type the number of
the square you are moving from, followed by the number
of the square you will move to. The numbers must be
separated by a comma.
The computer starts a series of games knowing only when
the game is won (a draw is impossible) and how to move.
It has no strategy at first and just moves randomly.
However, it learns from each game. Thus winning becomes
more and more difficult. Also, to help offset your
initial advantage, you will not be told how to win the
game but must learn this by playing.
The numbering of the board is as follows:
123
456
789
For example, to move your rightmost pawn forward,
you would type 9,6 in response to the question
'Your move ?'. Since I'm a good sport, you'll always
go first.

View File

@@ -0,0 +1,17 @@
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Hexapawn.Resources;
internal static class Resource
{
internal static class Streams
{
public static Stream Instructions => GetStream();
public static Stream Title => GetStream();
}
private static Stream GetStream([CallerMemberName] string name = null)
=> Assembly.GetExecutingAssembly().GetManifestResourceStream($"Hexapawn.Resources.{name}.txt");
}

View File

@@ -0,0 +1,5 @@
Hexapawn
Creative Computing Morristown, New Jersey

View File

@@ -1,26 +0,0 @@
using System;
using System.Collections.Generic;
namespace Love
{
// Provides input methods which emulate the BASIC interpreter's keyboard input routines
internal static class Input
{
private static void Prompt(string text = "") => Console.Write($"{text}? ");
public static string ReadLine(string prompt)
{
Prompt(prompt);
var values = ReadStrings();
if (values.Length > 1)
{
Console.WriteLine("!Extra input ingored");
}
return values[0];
}
private static string[] ReadStrings() => Console.ReadLine().Split(',', StringSplitOptions.TrimEntries);
}
}

View File

@@ -2,10 +2,15 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Strings\Intro.txt" />
<EmbeddedResource Include="Resources\Intro.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\00_Common\dotnet\Games.Common\Games.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,57 +1,55 @@
using System.IO;
using System.Text;
namespace Love
namespace Love;
internal class LovePattern
{
internal class LovePattern
private const int _lineLength = 60;
private readonly int[] _segmentLengths = new[] {
60, 1, 12, 26, 9, 12, 3, 8, 24, 17, 8, 4, 6, 23, 21, 6, 4, 6, 22, 12, 5,
6, 5, 4, 6, 21, 11, 8, 6, 4, 4, 6, 21, 10, 10, 5, 4, 4, 6, 21, 9, 11, 5,
4, 4, 6, 21, 8, 11, 6, 4, 4, 6, 21, 7, 11, 7, 4, 4, 6, 21, 6, 11, 8, 4,
4, 6, 19, 1, 1, 5, 11, 9, 4, 4, 6, 19, 1, 1, 5, 10, 10, 4, 4, 6, 18, 2,
1, 6, 8, 11, 4, 4, 6, 17, 3, 1, 7, 5, 13, 4, 4, 6, 15, 5, 2, 23, 5, 1,
29, 5, 17, 8, 1, 29, 9, 9, 12, 1, 13, 5, 40, 1, 1, 13, 5, 40, 1, 4, 6,
13, 3, 10, 6, 12, 5, 1, 5, 6, 11, 3, 11, 6, 14, 3, 1, 5, 6, 11, 3, 11,
6, 15, 2, 1, 6, 6, 9, 3, 12, 6, 16, 1, 1, 6, 6, 9, 3, 12, 6, 7, 1, 10,
7, 6, 7, 3, 13, 6, 6, 2, 10, 7, 6, 7, 3, 13, 14, 10, 8, 6, 5, 3, 14, 6,
6, 2, 10, 8, 6, 5, 3, 14, 6, 7, 1, 10, 9, 6, 3, 3, 15, 6, 16, 1, 1, 9,
6, 3, 3, 15, 6, 15, 2, 1, 10, 6, 1, 3, 16, 6, 14, 3, 1, 10, 10, 16, 6,
12, 5, 1, 11, 8, 13, 27, 1, 11, 8, 13, 27, 1, 60
};
private readonly StringBuilder _pattern = new();
public LovePattern(string message)
{
private readonly int[] _segmentLengths = new[] {
60, 1, 12, 26, 9, 12, 3, 8, 24, 17, 8, 4, 6, 23, 21, 6, 4, 6, 22, 12, 5,
6, 5, 4, 6, 21, 11, 8, 6, 4, 4, 6, 21, 10, 10, 5, 4, 4, 6, 21, 9, 11, 5,
4, 4, 6, 21, 8, 11, 6, 4, 4, 6, 21, 7, 11, 7, 4, 4, 6, 21, 6, 11, 8, 4,
4, 6, 19, 1, 1, 5, 11, 9, 4, 4, 6, 19, 1, 1, 5, 10, 10, 4, 4, 6, 18, 2,
1, 6, 8, 11, 4, 4, 6, 17, 3, 1, 7, 5, 13, 4, 4, 6, 15, 5, 2, 23, 5, 1,
29, 5, 17, 8, 1, 29, 9, 9, 12, 1, 13, 5, 40, 1, 1, 13, 5, 40, 1, 4, 6,
13, 3, 10, 6, 12, 5, 1, 5, 6, 11, 3, 11, 6, 14, 3, 1, 5, 6, 11, 3, 11,
6, 15, 2, 1, 6, 6, 9, 3, 12, 6, 16, 1, 1, 6, 6, 9, 3, 12, 6, 7, 1, 10,
7, 6, 7, 3, 13, 6, 6, 2, 10, 7, 6, 7, 3, 13, 14, 10, 8, 6, 5, 3, 14, 6,
6, 2, 10, 8, 6, 5, 3, 14, 6, 7, 1, 10, 9, 6, 3, 3, 15, 6, 16, 1, 1, 9,
6, 3, 3, 15, 6, 15, 2, 1, 10, 6, 1, 3, 16, 6, 14, 3, 1, 10, 10, 16, 6,
12, 5, 1, 11, 8, 13, 27, 1, 11, 8, 13, 27, 1, 60
};
Fill(new SourceCharacters(_lineLength, message));
}
public int LineLength => 60;
private void Fill(SourceCharacters source)
{
var lineLength = 0;
internal void Write(SourceCharacters source, Stream destination)
foreach (var segmentLength in _segmentLengths)
{
using var writer = new StreamWriter(destination);
WritePadding(writer);
var lineLength = 0;
foreach (var segmentLength in _segmentLengths)
foreach (var character in source.GetCharacters(segmentLength))
{
foreach (var character in source.GetCharacters(segmentLength))
{
writer.Write(character);
}
lineLength += segmentLength;
if (lineLength >= LineLength)
{
writer.WriteLine();
lineLength = 0;
}
_pattern.Append(character);
}
WritePadding(writer);
}
private void WritePadding(StreamWriter writer)
{
for (int i = 0; i < 10; i++)
lineLength += segmentLength;
if (lineLength >= _lineLength)
{
writer.WriteLine();
_pattern.AppendLine();
lineLength = 0;
}
}
}
public override string ToString() =>
new StringBuilder()
.AppendLines(10)
.Append(_pattern)
.AppendLines(10)
.ToString();
}

View File

@@ -1,31 +1,11 @@
using System;
using System.Reflection;
using Games.Common.IO;
using Love;
using Love.Resources;
namespace Love
{
internal class Program
{
static void Main(string[] args)
{
DisplayIntro();
var io = new ConsoleIO();
var message = Input.ReadLine("Your message, please");
var pattern = new LovePattern();
io.Write(Resource.Streams.Intro);
var source = new SourceCharacters(pattern.LineLength, message);
var message = io.ReadString("Your message, please");
using var destination = Console.OpenStandardOutput();
pattern.Write(source, destination);
}
private static void DisplayIntro()
{
using var stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("Love.Strings.Intro.txt");
using var stdout = Console.OpenStandardOutput();
stream.CopyTo(stdout);
}
}
}
io.Write(new LovePattern(message));

View File

@@ -7,3 +7,4 @@ A tribute to the great American artist, Robert Indiana.
His greatest work will be reproduced with a message of
your choice up to 60 characters. If you can't think of
a message, simply type the word 'LOVE'

View File

@@ -0,0 +1,16 @@
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Love.Resources;
internal static class Resource
{
internal static class Streams
{
public static Stream Intro => GetStream();
}
private static Stream GetStream([CallerMemberName] string name = null)
=> Assembly.GetExecutingAssembly().GetManifestResourceStream($"Love.Resources.{name}.txt");
}

View File

@@ -1,38 +1,37 @@
using System;
namespace Love
namespace Love;
internal class SourceCharacters
{
internal class SourceCharacters
private readonly int _lineLength;
private readonly char[][] _chars;
private int _currentRow;
private int _currentIndex;
public SourceCharacters(int lineLength, string message)
{
private readonly int _lineLength;
private readonly char[][] _chars;
private int _currentRow;
private int _currentIndex;
_lineLength = lineLength;
_chars = new[] { new char[lineLength], new char[lineLength] };
public SourceCharacters(int lineLength, string message)
for (int i = 0; i < lineLength; i++)
{
_lineLength = lineLength;
_chars = new[] { new char[lineLength], new char[lineLength] };
for (int i = 0; i < lineLength; i++)
{
_chars[0][i] = message[i % message.Length];
_chars[1][i] = ' ';
}
}
public ReadOnlySpan<char> GetCharacters(int count)
{
var span = new ReadOnlySpan<char>(_chars[_currentRow], _currentIndex, count);
_currentRow = 1 - _currentRow;
_currentIndex += count;
if (_currentIndex >= _lineLength)
{
_currentIndex = _currentRow = 0;
}
return span;
_chars[0][i] = message[i % message.Length];
_chars[1][i] = ' ';
}
}
public ReadOnlySpan<char> GetCharacters(int count)
{
var span = new ReadOnlySpan<char>(_chars[_currentRow], _currentIndex, count);
_currentRow = 1 - _currentRow;
_currentIndex += count;
if (_currentIndex >= _lineLength)
{
_currentIndex = _currentRow = 0;
}
return span;
}
}

View File

@@ -0,0 +1,16 @@
using System.Text;
namespace Love;
internal static class StringBuilderExtensions
{
internal static StringBuilder AppendLines(this StringBuilder builder, int count)
{
for (int i = 0; i < count; i++)
{
builder.AppendLine();
}
return builder;
}
}

View File

@@ -0,0 +1,13 @@
namespace Mugwump;
internal struct Distance
{
private readonly float _value;
public Distance(float deltaX, float deltaY)
{
_value = (float)Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
}
public override string ToString() => _value.ToString("0.0");
}

View File

@@ -1,36 +1,55 @@
using System;
using System.Linq;
using System.Reflection;
namespace Mugwump
namespace Mugwump;
internal class Game
{
internal class Game
private readonly TextIO _io;
private readonly IRandom _random;
internal Game(TextIO io, IRandom random)
{
private readonly Grid _grid;
_io = io;
_random = random;
}
private Game(Random random)
internal void Play(Func<bool> playAgain = null)
{
DisplayIntro();
while (playAgain?.Invoke() ?? true)
{
_grid = new Grid(Enumerable.Range(1, 4).Select(id => new Mugwump(id, random.Next(10), random.Next(10))));
}
Play(new Grid(_io, _random));
public static void Play(Random random) => new Game(random).Play();
private void Play()
{
for (int turn = 1; turn <= 10; turn++)
{
var guess = Input.ReadGuess($"Turn no. {turn} -- what is your guess");
if (_grid.Check(guess))
{
Console.WriteLine();
Console.WriteLine($"You got them all in {turn} turns!");
return;
}
}
Console.WriteLine();
Console.WriteLine("Sorry, that's 10 tries. Here is where they're hiding:");
_grid.Reveal();
_io.WriteLine();
_io.WriteLine("That was fun! Let's play again.......");
_io.WriteLine("Four more mugwumps are now in hiding.");
}
}
private void DisplayIntro()
{
using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Mugwump.Strings.Intro.txt");
_io.Write(stream);
}
private void Play(Grid grid)
{
for (int turn = 1; turn <= 10; turn++)
{
var guess = _io.ReadGuess($"Turn no. {turn} -- what is your guess");
if (grid.Check(guess))
{
_io.WriteLine();
_io.WriteLine($"You got them all in {turn} turns!");
return;
}
}
_io.WriteLine();
_io.WriteLine("Sorry, that's 10 tries. Here is where they're hiding:");
grid.Reveal();
}
}

View File

@@ -1,40 +1,40 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Mugwump
namespace Mugwump;
internal class Grid
{
internal class Grid
private readonly TextIO _io;
private readonly List<Mugwump> _mugwumps;
public Grid(TextIO io, IRandom random)
{
private readonly List<Mugwump> _mugwumps;
_io = io;
_mugwumps = Enumerable.Range(1, 4).Select(id => new Mugwump(id, random.NextPosition(10, 10))).ToList();
}
public Grid(IEnumerable<Mugwump> mugwumps)
public bool Check(Position guess)
{
foreach (var mugwump in _mugwumps.ToList())
{
_mugwumps = mugwumps.ToList();
var (found, distance) = mugwump.FindFrom(guess);
_io.WriteLine(found ? $"You have found {mugwump}" : $"You are {distance} units from {mugwump}");
if (found)
{
_mugwumps.Remove(mugwump);
}
}
public bool Check(Position guess)
return _mugwumps.Count == 0;
}
public void Reveal()
{
foreach (var mugwump in _mugwumps)
{
foreach (var mugwump in _mugwumps.ToList())
{
var (found, distance) = mugwump.FindFrom(guess);
Console.WriteLine(found ? $"You have found {mugwump}" : $"You are {distance} units from {mugwump}");
if (found)
{
_mugwumps.Remove(mugwump);
}
}
return _mugwumps.Count == 0;
}
public void Reveal()
{
foreach (var mugwump in _mugwumps.ToList())
{
Console.WriteLine(mugwump.Reveal());
}
_io.WriteLine(mugwump.Reveal());
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Mugwump;
internal static class IRandomExtensions
{
internal static Position NextPosition(this IRandom random, int maxX, int maxY) =>
new(random.Next(maxX), random.Next(maxY));
}

View File

@@ -1,66 +0,0 @@
using System;
using System.Collections.Generic;
namespace Mugwump
{
// Provides input methods which emulate the BASIC interpreter's keyboard input routines
internal static class Input
{
internal static Position ReadGuess(string prompt)
{
Console.WriteLine();
Console.WriteLine();
var input = ReadNumbers(prompt, 2);
return new Position(input[0], input[1]);
}
private static void Prompt(string text = "") => Console.Write($"{text}? ");
private static List<float> ReadNumbers(string prompt, int requiredCount)
{
var numbers = new List<float>();
while (!TryReadNumbers(prompt, requiredCount, numbers))
{
prompt = "";
}
return numbers;
}
private static bool TryReadNumbers(string prompt, int requiredCount, List<float> numbers)
{
Prompt(prompt);
var inputValues = ReadStrings();
foreach (var value in inputValues)
{
if (numbers.Count == requiredCount)
{
Console.WriteLine("!Extra input ingored");
return true;
}
if (!TryParseNumber(value, out var number))
{
return false;
}
numbers.Add(number);
}
return numbers.Count == requiredCount || TryReadNumbers("?", requiredCount, numbers);
}
private static string[] ReadStrings() => Console.ReadLine().Split(',', StringSplitOptions.TrimEntries);
private static bool TryParseNumber(string text, out float number)
{
if (float.TryParse(text, out number)) { return true; }
Console.WriteLine("!Number expected - retry input line");
number = default;
return false;
}
}
}

View File

@@ -1,20 +1,19 @@
namespace Mugwump
namespace Mugwump;
internal class Mugwump
{
internal class Mugwump
private readonly int _id;
private readonly Position _position;
public Mugwump(int id, Position position)
{
private readonly int _id;
private readonly Position _position;
public Mugwump(int id, int x, int y)
{
_id = id;
_position = new Position(x, y);
}
public (bool, Distance) FindFrom(Position guess) => (guess == _position, guess - _position);
public string Reveal() => $"{this} is at {_position}";
public override string ToString() => $"Mugwump {_id}";
_id = id;
_position = position;
}
public (bool, Distance) FindFrom(Position guess) => (guess == _position, guess - _position);
public string Reveal() => $"{this} is at {_position}";
public override string ToString() => $"Mugwump {_id}";
}

View File

@@ -2,10 +2,15 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Strings\Intro.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\00_Common\dotnet\Games.Common\Games.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,16 +0,0 @@
using System;
namespace Mugwump
{
internal class Distance
{
private readonly float _value;
public Distance(float deltaX, float deltaY)
{
_value = (float)Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
}
public override string ToString() => $"{_value:0.0}";
}
}

View File

@@ -1,9 +1,8 @@
namespace Mugwump
{
internal record Position(float X, float Y)
{
public override string ToString() => $"( {X} , {Y} )";
namespace Mugwump;
public static Distance operator -(Position p1, Position p2) => new(p1.X - p2.X, p1.Y - p2.Y);
}
internal record struct Position(float X, float Y)
{
public override string ToString() => $"( {X} , {Y} )";
public static Distance operator -(Position p1, Position p2) => new(p1.X - p2.X, p1.Y - p2.Y);
}

View File

@@ -1,33 +1,12 @@
using System;
using System.Reflection;
global using System;
global using Games.Common.IO;
global using Games.Common.Randomness;
namespace Mugwump
{
class Program
{
static void Main(string[] args)
{
DisplayIntro();
using Mugwump;
var random = new Random();
var random = new RandomNumberGenerator();
var io = new ConsoleIO();
while (true)
{
Game.Play(random);
var game = new Game(io, random);
Console.WriteLine();
Console.WriteLine("That was fun! Let's play again.......");
Console.WriteLine("Four more mugwumps are now in hiding.");
}
}
private static void DisplayIntro()
{
using var stream = Assembly.GetExecutingAssembly()
.GetManifestResourceStream("Mugwump.Strings.Intro.txt");
using var stdout = Console.OpenStandardOutput();
stream.CopyTo(stdout);
}
}
}
game.Play();

View File

@@ -0,0 +1,13 @@
namespace Mugwump;
// Provides input methods which emulate the BASIC interpreter's keyboard input routines
internal static class TextIOExtensions
{
internal static Position ReadGuess(this TextIO io, string prompt)
{
io.WriteLine();
io.WriteLine();
var (x, y) = io.Read2Numbers(prompt);
return new Position(x, y);
}
}

View File

@@ -1,94 +1,105 @@
using System;
using Games.Common.IO;
using Games.Common.Randomness;
using Stars.Resources;
namespace Stars
namespace Stars;
internal class Game
{
internal class Game
{
private readonly int _maxNumber;
private readonly int _maxGuessCount;
private readonly Random _random;
private readonly TextIO _io;
private readonly IRandom _random;
private readonly int _maxNumber;
private readonly int _maxGuessCount;
public Game(int maxNumber, int maxGuessCount)
public Game(TextIO io, IRandom random, int maxNumber, int maxGuessCount)
{
_io = io;
_random = random;
_maxNumber = maxNumber;
_maxGuessCount = maxGuessCount;
}
internal void Play(Func<bool> playAgain)
{
DisplayIntroduction();
do
{
_maxNumber = maxNumber;
_maxGuessCount = maxGuessCount;
_random = new Random();
Play();
} while (playAgain.Invoke());
}
private void DisplayIntroduction()
{
_io.Write(Resource.Streams.Title);
if (_io.ReadString("Do you want instructions").Equals("N", StringComparison.InvariantCultureIgnoreCase))
{
return;
}
internal void DisplayInstructions()
_io.WriteLine(Resource.Formats.Instructions, _maxNumber, _maxGuessCount);
}
private void Play()
{
_io.WriteLine();
_io.WriteLine();
var target = _random.Next(_maxNumber) + 1;
_io.WriteLine("Ok, I am thinking of a number. Start guessing.");
AcceptGuesses(target);
}
private void AcceptGuesses(int target)
{
for (int guessCount = 1; guessCount <= _maxGuessCount; guessCount++)
{
if (Input.GetString("Do you want instructions? ").Equals("N", StringComparison.InvariantCultureIgnoreCase))
_io.WriteLine();
var guess = _io.ReadNumber("Your guess");
if (guess == target)
{
DisplayWin(guessCount);
return;
}
Console.WriteLine($"I am thinking of a number between 1 and {_maxNumber}.");
Console.WriteLine("Try to guess my number. After you guess, I");
Console.WriteLine("will type one or more stars (*). The more");
Console.WriteLine("stars I type, the close you are to my number.");
Console.WriteLine("One star (*) means far away, seven stars (*******)");
Console.WriteLine($"means really close! You get {_maxGuessCount} guesses.");
DisplayStars(target, guess);
}
internal void Play()
DisplayLoss(target);
}
private void DisplayStars(int target, float guess)
{
var stars = Math.Abs(guess - target) switch
{
Console.WriteLine();
Console.WriteLine();
>= 64 => "*",
>= 32 => "**",
>= 16 => "***",
>= 8 => "****",
>= 4 => "*****",
>= 2 => "******",
_ => "*******"
};
var target = _random.Next(_maxNumber) + 1;
_io.WriteLine(stars);
}
Console.WriteLine("Ok, I am thinking of a number. Start guessing.");
private void DisplayWin(int guessCount)
{
_io.WriteLine();
_io.WriteLine(new string('*', 79));
_io.WriteLine();
_io.WriteLine($"You got it in {guessCount} guesses!!! Let's play again...");
}
AcceptGuesses(target);
}
private void AcceptGuesses(int target)
{
for (int guessCount = 1; guessCount <= _maxGuessCount; guessCount++)
{
Console.WriteLine();
var guess = Input.GetNumber("Your guess? ");
if (guess == target)
{
DisplayWin(guessCount);
return;
}
DisplayStars(target, guess);
}
DisplayLoss(target);
}
private static void DisplayStars(int target, float guess)
{
var stars = Math.Abs(guess - target) switch
{
>= 64 => "*",
>= 32 => "**",
>= 16 => "***",
>= 8 => "****",
>= 4 => "*****",
>= 2 => "******",
_ => "*******"
};
Console.WriteLine(stars);
}
private static void DisplayWin(int guessCount)
{
Console.WriteLine();
Console.WriteLine(new string('*', 79));
Console.WriteLine();
Console.WriteLine($"You got it in {guessCount} guesses!!! Let's play again...");
}
private void DisplayLoss(int target)
{
Console.WriteLine();
Console.WriteLine($"Sorry, that's {_maxGuessCount} guesses. The number was {target}.");
}
private void DisplayLoss(int target)
{
_io.WriteLine();
_io.WriteLine($"Sorry, that's {_maxGuessCount} guesses. The number was {target}.");
}
}

View File

@@ -1,31 +0,0 @@
using System;
namespace Stars
{
internal static class Input
{
// Float, because that's what the BASIC input operation returns
internal static float GetNumber(string prompt)
{
Console.Write(prompt);
while (true)
{
var response = Console.ReadLine();
if (float.TryParse(response, out var value))
{
return value;
}
Console.WriteLine("!Number expected - retry input line");
Console.Write("? ");
}
}
internal static string GetString(string prompt)
{
Console.Write(prompt);
return Console.ReadLine();
}
}
}

View File

@@ -1,30 +1,7 @@
using System;
using Games.Common.IO;
using Games.Common.Randomness;
using Stars;
namespace Stars
{
class Program
{
static void Main(string[] args)
{
DisplayTitle();
var game = new Game(new ConsoleIO(), new RandomNumberGenerator(), maxNumber: 100, maxGuessCount: 7);
var game = new Game(maxNumber: 100, maxGuessCount: 7);
game.DisplayInstructions();
while (true)
{
game.Play();
}
}
private static void DisplayTitle()
{
Console.WriteLine(" Stars");
Console.WriteLine(" Creative Computing Morristown, New Jersey");
Console.WriteLine();
Console.WriteLine();
Console.WriteLine();
}
}
}
game.Play(() => true);

View File

@@ -0,0 +1,6 @@
I am thinking of a number between 1 and {0}.
Try to guess my number. After you guess, I
will type one or more stars (*). The more
stars I type, the closer you are to my number.
One star (*) means far away, seven stars (*******)
means really close! You get {1} guesses.

View File

@@ -0,0 +1,28 @@
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Stars.Resources;
internal static class Resource
{
internal static class Streams
{
public static Stream Title => GetStream();
}
internal static class Formats
{
public static string Instructions => GetString();
}
private static string GetString([CallerMemberName] string name = null)
{
using var stream = GetStream(name);
using var reader = new StreamReader(stream);
return reader.ReadToEnd();
}
private static Stream GetStream([CallerMemberName] string name = null)
=> Assembly.GetExecutingAssembly().GetManifestResourceStream($"Stars.Resources.{name}.txt");
}

View File

@@ -0,0 +1,5 @@
Stars
Creative Computing Morristown, New Jersey

View File

@@ -2,7 +2,15 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/*.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\00_Common\dotnet\Games.Common\Games.Common.csproj" />
</ItemGroup>
</Project>

View File

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

View File

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

View File

@@ -1,23 +1,22 @@
namespace SuperStarTrek.Commands
namespace SuperStarTrek.Commands;
internal class CommandResult
{
internal class CommandResult
public static readonly CommandResult Ok = new(false);
public static readonly CommandResult GameOver = new(true);
private CommandResult(bool isGameOver)
{
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);
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

@@ -1,131 +1,128 @@
using System;
using Games.Common.IO;
using Games.Common.Randomness;
using SuperStarTrek.Objects;
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
using SuperStarTrek.Systems;
using SuperStarTrek.Systems.ComputerFunctions;
namespace SuperStarTrek
namespace SuperStarTrek;
internal class Game
{
internal class Game
private readonly TextIO _io;
private readonly IRandom _random;
private int _initialStardate;
private int _finalStarDate;
private float _currentStardate;
private Coordinates _currentQuadrant;
private Galaxy _galaxy;
private int _initialKlingonCount;
private Enterprise _enterprise;
internal Game(TextIO io, IRandom random)
{
private readonly Output _output;
private readonly Input _input;
private readonly Random _random;
private int _initialStardate;
private int _finalStarDate;
private float _currentStardate;
private Coordinates _currentQuadrant;
private Galaxy _galaxy;
private int _initialKlingonCount;
private Enterprise _enterprise;
internal Game(Output output, Input input, Random random)
{
_output = output;
_input = input;
_random = random;
}
internal float Stardate => _currentStardate;
internal float StardatesRemaining => _finalStarDate - _currentStardate;
internal void DoIntroduction()
{
_output.Write(Strings.Title);
if (_input.GetYesNo("Do you need instructions", Input.YesNoMode.FalseOnN))
{
_output.Write(Strings.Instructions);
_input.WaitForAnyKeyButEnter("to continue");
}
}
internal void Play()
{
Initialise();
var gameOver = false;
while (!gameOver)
{
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.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.StartIn(BuildCurrentQuadrant());
}
private Quadrant BuildCurrentQuadrant() =>
new Quadrant(_galaxy[_currentQuadrant], _enterprise, _random, _galaxy, _input, _output);
internal 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);
_io = io;
_random = random;
}
internal float Stardate => _currentStardate;
internal float StardatesRemaining => _finalStarDate - _currentStardate;
internal void DoIntroduction()
{
_io.Write(Strings.Title);
if (_io.GetYesNo("Do you need instructions", IReadWriteExtensions.YesNoMode.FalseOnN))
{
_io.Write(Strings.Instructions);
_io.WaitForAnyKeyButEnter("to continue");
}
}
internal void Play()
{
Initialise();
var gameOver = false;
while (!gameOver)
{
var command = _io.ReadCommand();
var result = _enterprise.Execute(command);
gameOver = result.IsGameOver || CheckIfStranded();
_currentStardate += result.TimeElapsed;
gameOver |= _currentStardate > _finalStarDate;
}
if (_galaxy.KlingonCount > 0)
{
_io.Write(Strings.EndOfMission, _currentStardate, _galaxy.KlingonCount);
}
else
{
_io.Write(Strings.Congratulations, CalculateEfficiency());
}
}
private void Initialise()
{
_currentStardate = _initialStardate = _random.Next(20, 40) * 100;
_finalStarDate = _initialStardate + _random.Next(25, 35);
_currentQuadrant = _random.NextCoordinate();
_galaxy = new Galaxy(_random);
_initialKlingonCount = _galaxy.KlingonCount;
_enterprise = new Enterprise(3000, _random.NextCoordinate(), _io, _random);
_enterprise
.Add(new WarpEngines(_enterprise, _io))
.Add(new ShortRangeSensors(_enterprise, _galaxy, this, _io))
.Add(new LongRangeSensors(_galaxy, _io))
.Add(new PhaserControl(_enterprise, _io, _random))
.Add(new PhotonTubes(10, _enterprise, _io))
.Add(new ShieldControl(_enterprise, _io))
.Add(new DamageControl(_enterprise, _io))
.Add(new LibraryComputer(
_io,
new CumulativeGalacticRecord(_io, _galaxy),
new StatusReport(this, _galaxy, _enterprise, _io),
new TorpedoDataCalculator(_enterprise, _io),
new StarbaseDataCalculator(_enterprise, _io),
new DirectionDistanceCalculator(_enterprise, _io),
new GalaxyRegionMap(_io, _galaxy)));
_io.Write(Strings.Enterprise);
_io.Write(
Strings.Orders,
_galaxy.KlingonCount,
_finalStarDate,
_finalStarDate - _initialStardate,
_galaxy.StarbaseCount > 1 ? "are" : "is",
_galaxy.StarbaseCount,
_galaxy.StarbaseCount > 1 ? "s" : "");
_io.WaitForAnyKeyButEnter("when ready to accept command");
_enterprise.StartIn(BuildCurrentQuadrant());
}
private Quadrant BuildCurrentQuadrant() => new(_galaxy[_currentQuadrant], _enterprise, _random, _galaxy, _io);
internal bool Replay() => _galaxy.StarbaseCount > 0 && _io.ReadExpectedString(Strings.ReplayPrompt, "Aye");
private bool CheckIfStranded()
{
if (_enterprise.IsStranded) { _io.Write(Strings.Stranded); }
return _enterprise.IsStranded;
}
private float CalculateEfficiency() =>
1000 * (float)Math.Pow(_initialKlingonCount / (_currentStardate - _initialStardate), 2);
}

View File

@@ -0,0 +1,16 @@
using Games.Common.Randomness;
using SuperStarTrek.Space;
namespace SuperStarTrek;
internal static class IRandomExtensions
{
internal static Coordinates NextCoordinate(this IRandom random) =>
new Coordinates(random.Next1To8Inclusive() - 1, random.Next1To8Inclusive() - 1);
// Duplicates the algorithm used in the original code to get an integer value from 1 to 8, inclusive:
// 475 DEF FNR(R)=INT(RND(R)*7.98+1.01)
// Returns a value from 1 to 8, inclusive.
// Note there's a slight bias away from the extreme values, 1 and 8.
internal static int Next1To8Inclusive(this IRandom random) => (int)(random.NextFloat() * 7.98 + 1.01);
}

View File

@@ -0,0 +1,89 @@
using System;
using System.Linq;
using Games.Common.IO;
using SuperStarTrek.Commands;
using SuperStarTrek.Space;
using static System.StringComparison;
namespace SuperStarTrek;
internal static class IReadWriteExtensions
{
internal static void WaitForAnyKeyButEnter(this IReadWrite io, string prompt)
{
io.Write($"Hit any key but Enter {prompt} ");
while (io.ReadCharacter() == '\r');
}
internal static (float X, float Y) GetCoordinates(this IReadWrite io, string prompt) =>
io.Read2Numbers($"{prompt} (X,Y)");
internal static bool TryReadNumberInRange(
this IReadWrite io,
string prompt,
float minValue,
float maxValue,
out float value)
{
value = io.ReadNumber($"{prompt} ({minValue}-{maxValue})");
return value >= minValue && value <= maxValue;
}
internal static bool ReadExpectedString(this IReadWrite io, string prompt, string trueValue) =>
io.ReadString(prompt).Equals(trueValue, InvariantCultureIgnoreCase);
internal static Command ReadCommand(this IReadWrite io)
{
while(true)
{
var response = io.ReadString("Command");
if (response.Length >= 3 &&
Enum.TryParse(response.Substring(0, 3), ignoreCase: true, out Command parsedCommand))
{
return parsedCommand;
}
io.WriteLine("Enter one of the following:");
foreach (var command in Enum.GetValues(typeof(Command)).OfType<Command>())
{
io.WriteLine($" {command} ({command.GetDescription()})");
}
io.WriteLine();
}
}
internal static bool TryReadCourse(this IReadWrite io, string prompt, string officer, out Course course)
{
if (!io.TryReadNumberInRange(prompt, 1, 9, out var direction))
{
io.WriteLine($"{officer} reports, 'Incorrect course data, sir!'");
course = default;
return false;
}
course = new Course(direction);
return true;
}
internal static bool GetYesNo(this IReadWrite io, string prompt, YesNoMode mode)
{
var response = io.ReadString($"{prompt} (Y/N)").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")
};
}
internal enum YesNoMode
{
TrueOnY,
FalseOnN
}
}

View File

@@ -1,159 +0,0 @@
using System;
using System.Linq;
using SuperStarTrek.Commands;
using SuperStarTrek.Space;
using static System.StringComparison;
namespace SuperStarTrek
{
internal class Input
{
private readonly Output _output;
internal Input(Output output)
{
_output = output;
}
internal void WaitForAnyKeyButEnter(string prompt)
{
_output.Write($"Hit any key but Enter {prompt} ");
while (Console.ReadKey(intercept: true).Key == ConsoleKey.Enter);
}
internal string GetString(string prompt)
{
_output.Prompt(prompt);
return Console.ReadLine();
}
internal float GetNumber(string prompt)
{
_output.Prompt(prompt);
while (true)
{
var response = Console.ReadLine();
if (float.TryParse(response, out var value))
{
return value;
}
_output.WriteLine("!Number expected - retry input line");
_output.Prompt();
}
}
internal (float X, float Y) GetCoordinates(string prompt)
{
_output.Prompt($"{prompt} (X,Y)");
var responses = ReadNumbers(2);
return (responses[0], responses[1]);
}
internal bool TryGetNumber(string prompt, float minValue, float maxValue, out float value)
{
value = GetNumber($"{prompt} ({minValue}-{maxValue})");
return value >= minValue && value <= maxValue;
}
internal bool GetString(string replayPrompt, string trueValue) =>
GetString(replayPrompt).Equals(trueValue, InvariantCultureIgnoreCase);
internal Command GetCommand()
{
while(true)
{
var response = GetString("Command");
if (response.Length >= 3 &&
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();
}
}
internal 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;
}
internal 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;
}
internal enum YesNoMode
{
TrueOnY,
FalseOnN
}
}
}

View File

@@ -1,231 +1,230 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Games.Common.IO;
using Games.Common.Randomness;
using SuperStarTrek.Commands;
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
using SuperStarTrek.Systems;
namespace SuperStarTrek.Objects
namespace SuperStarTrek.Objects;
internal class Enterprise
{
internal class Enterprise
private readonly int _maxEnergy;
private readonly IReadWrite _io;
private readonly List<Subsystem> _systems;
private readonly Dictionary<Command, Subsystem> _commandExecutors;
private readonly IRandom _random;
private Quadrant _quadrant;
public Enterprise(int maxEnergy, Coordinates sector, IReadWrite io, IRandom random)
{
private readonly int _maxEnergy;
private readonly Output _output;
private readonly List<Subsystem> _systems;
private readonly Dictionary<Command, Subsystem> _commandExecutors;
private readonly Random _random;
private readonly Input _input;
private Quadrant _quadrant;
SectorCoordinates = sector;
TotalEnergy = _maxEnergy = maxEnergy;
public Enterprise(int maxEnergy, Coordinates sector, Output output, Random random, Input input)
{
SectorCoordinates = sector;
TotalEnergy = _maxEnergy = maxEnergy;
_systems = new List<Subsystem>();
_commandExecutors = new Dictionary<Command, Subsystem>();
_output = output;
_random = random;
_input = input;
}
internal Quadrant Quadrant => _quadrant;
internal Coordinates QuadrantCoordinates => _quadrant.Coordinates;
internal Coordinates SectorCoordinates { get; private set; }
internal string Condition => GetCondition();
internal LibraryComputer Computer => (LibraryComputer)_commandExecutors[Command.COM];
internal ShieldControl ShieldControl => (ShieldControl)_commandExecutors[Command.SHE];
internal float Energy => TotalEnergy - ShieldControl.ShieldEnergy;
internal float TotalEnergy { get; private set; }
internal int DamagedSystemCount => _systems.Count(s => s.IsDamaged);
internal IEnumerable<Subsystem> Systems => _systems;
internal PhotonTubes PhotonTubes => (PhotonTubes)_commandExecutors[Command.TOR];
internal bool IsDocked => _quadrant.EnterpriseIsNextToStarbase;
internal bool IsStranded => TotalEnergy < 10 || Energy < 10 && ShieldControl.IsDamaged;
internal Enterprise Add(Subsystem system)
{
_systems.Add(system);
_commandExecutors[system.Command] = system;
return this;
}
internal void StartIn(Quadrant quadrant)
{
_quadrant = quadrant;
quadrant.Display(Strings.StartText);
}
private string GetCondition() =>
IsDocked switch
{
true => "Docked",
false when _quadrant.HasKlingons => "*Red*",
false when Energy / _maxEnergy < 0.1f => "Yellow",
false => "Green"
};
internal CommandResult Execute(Command command)
{
if (command == Command.XXX) { return CommandResult.GameOver; }
return _commandExecutors[command].ExecuteCommand(_quadrant);
}
internal void Refuel() => TotalEnergy = _maxEnergy;
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;
_systems = new List<Subsystem>();
_commandExecutors = new Dictionary<Command, Subsystem>();
_io = io;
_random = random;
}
internal Quadrant Quadrant => _quadrant;
internal Coordinates QuadrantCoordinates => _quadrant.Coordinates;
internal Coordinates SectorCoordinates { get; private set; }
internal string Condition => GetCondition();
internal LibraryComputer Computer => (LibraryComputer)_commandExecutors[Command.COM];
internal ShieldControl ShieldControl => (ShieldControl)_commandExecutors[Command.SHE];
internal float Energy => TotalEnergy - ShieldControl.ShieldEnergy;
internal float TotalEnergy { get; private set; }
internal int DamagedSystemCount => _systems.Count(s => s.IsDamaged);
internal IEnumerable<Subsystem> Systems => _systems;
internal PhotonTubes PhotonTubes => (PhotonTubes)_commandExecutors[Command.TOR];
internal bool IsDocked => _quadrant.EnterpriseIsNextToStarbase;
internal bool IsStranded => TotalEnergy < 10 || Energy < 10 && ShieldControl.IsDamaged;
internal Enterprise Add(Subsystem system)
{
_systems.Add(system);
_commandExecutors[system.Command] = system;
return this;
}
internal void StartIn(Quadrant quadrant)
{
_quadrant = quadrant;
quadrant.Display(Strings.StartText);
}
private string GetCondition() =>
IsDocked switch
{
true => "Docked",
false when _quadrant.HasKlingons => "*Red*",
false when Energy / _maxEnergy < 0.1f => "Yellow",
false => "Green"
};
internal CommandResult Execute(Command command)
{
if (command == Command.XXX) { return CommandResult.GameOver; }
return _commandExecutors[command].ExecuteCommand(_quadrant);
}
internal void Refuel() => TotalEnergy = _maxEnergy;
public override string ToString() => "<*>";
internal void UseEnergy(float amountUsed)
{
TotalEnergy -= amountUsed;
}
internal CommandResult TakeHit(Coordinates sector, int hitStrength)
{
_io.WriteLine($"{hitStrength} unit hit on Enterprise from sector {sector}");
ShieldControl.AbsorbHit(hitStrength);
if (ShieldControl.ShieldEnergy <= 0)
{
_io.WriteLine(Strings.Destroyed);
return CommandResult.GameOver;
}
_io.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.NextFloat() > 0.6 || hitShieldRatio <= 0.02f)
{
return;
}
var system = _systems[_random.Next1To8Inclusive() - 1];
system.TakeDamage(hitShieldRatio + 0.5f * _random.NextFloat());
_io.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())
{
_io.WriteLine("Damage Control report:");
foreach (var systemName in repairedSystems)
{
_io.WriteLine($" {systemName} repair completed.");
}
}
}
internal void VaryConditionOfRandomSystem()
{
if (_random.NextFloat() > 0.2f) { return; }
var system = _systems[_random.Next1To8Inclusive() - 1];
_io.Write($"Damage Control report: {system.Name} ");
if (_random.NextFloat() >= 0.6)
{
system.Repair(_random.NextFloat() * 3 + 1);
_io.WriteLine("state of repair improved");
}
else
{
system.TakeDamage(_random.NextFloat() * 5 + 1);
_io.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, _io);
}
_quadrant.SetEnterpriseSector(sector);
SectorCoordinates = sector;
TotalEnergy -= distance + 10;
if (Energy < 0)
{
_io.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))
{
_io.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)
{
_io.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,43 +1,43 @@
using Games.Common.Randomness;
using SuperStarTrek.Commands;
using SuperStarTrek.Space;
namespace SuperStarTrek.Objects
namespace SuperStarTrek.Objects;
internal class Klingon
{
internal class Klingon
private readonly IRandom _random;
internal Klingon(Coordinates sector, IRandom random)
{
private readonly Random _random;
internal Klingon(Coordinates sector, Random random)
{
Sector = sector;
_random = random;
Energy = _random.GetFloat(100, 300);
}
internal float Energy { get; private set; }
internal Coordinates Sector { get; private set; }
public override string ToString() => "+K+";
internal 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;
Sector = sector;
_random = random;
Energy = _random.NextFloat(100, 300);
}
internal float Energy { get; private set; }
internal Coordinates Sector { get; private set; }
public override string ToString() => "+K+";
internal CommandResult FireOn(Enterprise enterprise)
{
var attackStrength = _random.NextFloat();
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,6 @@
namespace SuperStarTrek.Objects
namespace SuperStarTrek.Objects;
internal class Star
{
internal class Star
{
public override string ToString() => " * ";
}
public override string ToString() => " * ";
}

View File

@@ -1,45 +1,44 @@
using Games.Common.IO;
using Games.Common.Randomness;
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
namespace SuperStarTrek.Objects
namespace SuperStarTrek.Objects;
internal class Starbase
{
internal class Starbase
private readonly IReadWrite _io;
private readonly float _repairDelay;
internal Starbase(Coordinates sector, IRandom random, IReadWrite io)
{
private readonly Input _input;
private readonly Output _output;
private readonly float _repairDelay;
internal Starbase(Coordinates sector, Random random, Input input, Output output)
{
Sector = sector;
_repairDelay = random.GetFloat() * 0.5f;
_input = input;
_output = output;
}
internal Coordinates Sector { get; }
public override string ToString() => ">!<";
internal bool TryRepair(Enterprise enterprise, out float repairTime)
{
repairTime = enterprise.DamagedSystemCount * 0.1f + _repairDelay;
if (repairTime >= 1) { repairTime = 0.9f; }
_output.Write(Strings.RepairEstimate, repairTime);
if (_input.GetYesNo(Strings.RepairPrompt, Input.YesNoMode.TrueOnY))
{
foreach (var system in enterprise.Systems)
{
system.Repair();
}
return true;
}
repairTime = 0;
return false;
}
internal void ProtectEnterprise() => _output.WriteLine(Strings.Protected);
Sector = sector;
_repairDelay = random.NextFloat(0.5f);
_io = io;
}
internal Coordinates Sector { get; }
public override string ToString() => ">!<";
internal bool TryRepair(Enterprise enterprise, out float repairTime)
{
repairTime = enterprise.DamagedSystemCount * 0.1f + _repairDelay;
if (repairTime >= 1) { repairTime = 0.9f; }
_io.Write(Strings.RepairEstimate, repairTime);
if (_io.GetYesNo(Strings.RepairPrompt, IReadWriteExtensions.YesNoMode.TrueOnY))
{
foreach (var system in enterprise.Systems)
{
system.Repair();
}
return true;
}
repairTime = 0;
return false;
}
internal void ProtectEnterprise() => _io.WriteLine(Strings.Protected);
}

View File

@@ -1,40 +0,0 @@
using System;
namespace SuperStarTrek
{
internal class Output
{
internal Output Write(string text)
{
Console.Write(text);
return this;
}
internal Output Write(string format, params object[] args)
{
Console.Write(format, args);
return this;
}
internal Output WriteLine(string text = "")
{
Console.WriteLine(text);
return this;
}
internal Output NextLine()
{
Console.WriteLine();
return this;
}
internal Output Prompt(string text = "")
{
Console.Write($"{text}? ");
return this;
}
}
}

View File

@@ -23,24 +23,18 @@
// **** CONVERTED TO MICROSOFT C# 2/20/21 BY ANDREW COOPER
// ****
namespace SuperStarTrek
using Games.Common.IO;
using Games.Common.Randomness;
using SuperStarTrek;
var io = new ConsoleIO();
var random = new RandomNumberGenerator();
var game = new Game(io, random);
game.DoIntroduction();
do
{
internal class Program
{
static void Main()
{
var output = new Output();
var input = new Input(output);
var random = new Random();
var game = new Game(output, input, random);
game.DoIntroduction();
do
{
game.Play();
} while (game.Replay());
}
}
}
game.Play();
} while (game.Replay());

View File

@@ -1,25 +0,0 @@
using SuperStarTrek.Space;
namespace SuperStarTrek
{
internal class Random
{
private readonly System.Random _random = new();
internal Coordinates GetCoordinate() => new Coordinates(Get1To8Inclusive() - 1, Get1To8Inclusive() - 1);
// Duplicates the algorithm used in the original code to get an integer value from 1 to 8, inclusive:
// 475 DEF FNR(R)=INT(RND(R)*7.98+1.01)
// Returns a value from 1 to 8, inclusive.
// Note there's a slight bias away from the extreme values, 1 and 8.
internal int Get1To8Inclusive() => (int)(GetFloat() * 7.98 + 1.01);
internal int GetInt(int inclusiveMinValue, int exclusiveMaxValue) =>
_random.Next(inclusiveMinValue, exclusiveMaxValue);
internal float GetFloat() => (float)_random.NextDouble();
internal float GetFloat(float inclusiveMinValue, float exclusiveMaxValue)
=> GetFloat() * (exclusiveMaxValue - inclusiveMinValue) + inclusiveMinValue;
}
}

View File

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

View File

@@ -16,3 +16,9 @@
'----------------'
THE USS ENTERPRISE --- NCC-1701

View File

@@ -104,3 +104,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

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

View File

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

View File

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

View File

@@ -3,3 +3,4 @@ Your orders are as follows:
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

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

View File

@@ -5,4 +5,4 @@
Canopus Aldebaran
Altair Regulus
Sagittarius Arcturus
Pollux Spica
Pollux Spica

View File

@@ -1,3 +1,3 @@
That does it, Captain!! You are hereby relieved of command
and sentenced to 99 stardates at hard labor on Cygnus 12!!
and sentenced to 99 stardates at hard labor on Cygnus 12!!

View File

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

View File

@@ -2,4 +2,4 @@
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'
let him step forward and enter 'Aye'

View File

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

View File

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

View File

@@ -17,3 +17,11 @@
* *
* *
*************************************

View File

@@ -1,70 +1,69 @@
using System;
using SuperStarTrek.Utils;
namespace SuperStarTrek.Space
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
{
// 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
internal Coordinates(int x, int y)
{
internal Coordinates(int x, int y)
{
X = Validated(x, nameof(x));
Y = Validated(y, nameof(y));
X = Validated(x, nameof(x));
Y = Validated(y, nameof(y));
RegionIndex = (X << 1) + (Y >> 2);
SubRegionIndex = Y % 4;
RegionIndex = (X << 1) + (Y >> 2);
SubRegionIndex = Y % 4;
}
internal int X { get; }
internal int Y { get; }
internal int RegionIndex { get; }
internal int SubRegionIndex { get; }
private static 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;
}
internal int X { get; }
coordinates = default;
return false;
internal int Y { get; }
static int Round(float value) => (int)Math.Round(value, MidpointRounding.AwayFromZero);
}
internal int RegionIndex { get; }
internal (float Direction, float Distance) GetDirectionAndDistanceTo(Coordinates destination) =>
DirectionAndDistance.From(this).To(destination);
internal int SubRegionIndex { get; }
private static 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;
}
internal float GetDistanceTo(Coordinates destination)
{
var (_, distance) = GetDirectionAndDistanceTo(destination);
return distance;
}
}

View File

@@ -1,98 +1,97 @@
using System;
using System.Collections.Generic;
namespace SuperStarTrek.Space
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
{
// 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[]
{
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)
};
internal Course(float direction)
{
if (direction < 1 || direction > 9)
{
(0, 1),
(-1, 1),
(-1, 0),
(-1, -1),
(0, -1),
(1, -1),
(1, 0),
(1, 1),
(0, 1)
};
internal Course(float 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;
throw new ArgumentOutOfRangeException(
nameof(direction),
direction,
"Must be between 1 and 9, inclusive.");
}
internal float DeltaX { get; }
var cardinalDirection = (int)(direction - 1) % 8;
var fractionalDirection = direction - (int)direction;
internal float DeltaY { get; }
var baseCardinal = cardinals[cardinalDirection];
var nextCardinal = cardinals[cardinalDirection + 1];
internal IEnumerable<Coordinates> GetSectorsFrom(Coordinates start)
DeltaX = baseCardinal.DeltaX + (nextCardinal.DeltaX - baseCardinal.DeltaX) * fractionalDirection;
DeltaY = baseCardinal.DeltaY + (nextCardinal.DeltaY - baseCardinal.DeltaY) * fractionalDirection;
}
internal float DeltaX { get; }
internal float DeltaY { get; }
internal IEnumerable<Coordinates> GetSectorsFrom(Coordinates start)
{
(float x, float y) = start;
while(true)
{
(float x, float y) = start;
x += DeltaX;
y += DeltaY;
while(true)
if (!Coordinates.TryCreate(x, y, out var coordinates))
{
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 static (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 < 0)
{
newQuadrant -= 1;
newSector += 8;
yield break;
}
return newQuadrant switch
{
< 0 => (false, 0, 0),
> 7 => (false, 7, 7),
_ => (true, newQuadrant, newSector)
};
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 static (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 < 0)
{
newQuadrant -= 1;
newSector += 8;
}
return newQuadrant switch
{
< 0 => (false, 0, 0),
> 7 => (false, 7, 7),
_ => (true, newQuadrant, newSector)
};
}
}

View File

@@ -1,64 +1,64 @@
using System.Collections.Generic;
using System.Linq;
using Games.Common.Randomness;
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;
static Galaxy()
{
private static readonly string[] _regionNames;
private static readonly string[] _subRegionIdentifiers;
private readonly QuadrantInfo[][] _quadrants;
_regionNames = Strings.RegionNames.Split(new[] { ' ', '\n' }, RemoveEmptyEntries | TrimEntries);
_subRegionIdentifiers = new[] { "I", "II", "III", "IV" };
}
static Galaxy()
{
_regionNames = Strings.RegionNames.Split(new[] { ' ', '\n' }, RemoveEmptyEntries | TrimEntries);
_subRegionIdentifiers = new[] { "I", "II", "III", "IV" };
}
internal Galaxy(Random random)
{
_quadrants = Enumerable
internal Galaxy(IRandom random)
{
_quadrants = Enumerable
.Range(0, 8)
.Select(x => Enumerable
.Range(0, 8)
.Select(x => Enumerable
.Range(0, 8)
.Select(y => new Coordinates(x, y))
.Select(c => QuadrantInfo.Create(c, GetQuadrantName(c), random))
.ToArray())
.ToArray();
.Select(y => new Coordinates(x, y))
.Select(c => QuadrantInfo.Create(c, GetQuadrantName(c), random))
.ToArray())
.ToArray();
if (StarbaseCount == 0)
if (StarbaseCount == 0)
{
var randomQuadrant = this[random.NextCoordinate()];
randomQuadrant.AddStarbase();
if (randomQuadrant.KlingonCount < 2)
{
var randomQuadrant = this[random.GetCoordinate()];
randomQuadrant.AddStarbase();
if (randomQuadrant.KlingonCount < 2)
{
randomQuadrant.AddKlingon();
}
randomQuadrant.AddKlingon();
}
}
internal QuadrantInfo this[Coordinates coordinate] => _quadrants[coordinate.X][coordinate.Y];
internal int KlingonCount => _quadrants.SelectMany(q => q).Sum(q => q.KlingonCount);
internal int StarbaseCount => _quadrants.SelectMany(q => q).Count(q => q.HasStarbase);
internal IEnumerable<IEnumerable<QuadrantInfo>> Quadrants => _quadrants;
private static string GetQuadrantName(Coordinates coordinates) =>
$"{_regionNames[coordinates.RegionIndex]} {_subRegionIdentifiers[coordinates.SubRegionIndex]}";
internal 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]);
}
internal QuadrantInfo this[Coordinates coordinate] => _quadrants[coordinate.X][coordinate.Y];
internal int KlingonCount => _quadrants.SelectMany(q => q).Sum(q => q.KlingonCount);
internal int StarbaseCount => _quadrants.SelectMany(q => q).Count(q => q.HasStarbase);
internal IEnumerable<IEnumerable<QuadrantInfo>> Quadrants => _quadrants;
private static string GetQuadrantName(Coordinates coordinates) =>
$"{_regionNames[coordinates.RegionIndex]} {_subRegionIdentifiers[coordinates.SubRegionIndex]}";
internal 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,192 +1,192 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Games.Common.IO;
using Games.Common.Randomness;
using SuperStarTrek.Commands;
using SuperStarTrek.Objects;
using SuperStarTrek.Resources;
namespace SuperStarTrek.Space
namespace SuperStarTrek.Space;
internal class Quadrant
{
internal class Quadrant
private readonly QuadrantInfo _info;
private readonly IRandom _random;
private readonly Dictionary<Coordinates, object> _sectors;
private readonly Enterprise _enterprise;
private readonly IReadWrite _io;
private bool _entered = false;
internal Quadrant(
QuadrantInfo info,
Enterprise enterprise,
IRandom random,
Galaxy galaxy,
IReadWrite io)
{
private readonly QuadrantInfo _info;
private readonly Random _random;
private readonly Dictionary<Coordinates, object> _sectors;
private readonly Enterprise _enterprise;
private readonly Output _output;
private bool _entered = false;
_info = info;
_random = random;
_io = io;
Galaxy = galaxy;
internal Quadrant(
QuadrantInfo info,
Enterprise enterprise,
Random random,
Galaxy galaxy,
Input input,
Output output)
info.MarkAsKnown();
_sectors = new() { [enterprise.SectorCoordinates] = _enterprise = enterprise };
PositionObject(sector => new Klingon(sector, _random), _info.KlingonCount);
if (_info.HasStarbase)
{
_info = info;
_random = random;
_output = output;
Galaxy = galaxy;
Starbase = PositionObject(sector => new Starbase(sector, _random, io));
}
PositionObject(_ => new Star(), _info.StarCount);
}
info.MarkAsKnown();
_sectors = new() { [enterprise.SectorCoordinates] = _enterprise = enterprise };
PositionObject(sector => new Klingon(sector, _random), _info.KlingonCount);
if (_info.HasStarbase)
{
Starbase = PositionObject(sector => new Starbase(sector, _random, input, output));
}
PositionObject(_ => new Star(), _info.StarCount);
internal Coordinates Coordinates => _info.Coordinates;
internal bool HasKlingons => _info.KlingonCount > 0;
internal int KlingonCount => _info.KlingonCount;
internal bool HasStarbase => _info.HasStarbase;
internal Starbase Starbase { get; }
internal Galaxy Galaxy { get; }
internal bool EnterpriseIsNextToStarbase =>
_info.HasStarbase &&
Math.Abs(_enterprise.SectorCoordinates.X - Starbase.Sector.X) <= 1 &&
Math.Abs(_enterprise.SectorCoordinates.Y - Starbase.Sector.Y) <= 1;
internal IEnumerable<Klingon> Klingons => _sectors.Values.OfType<Klingon>();
public override string ToString() => _info.Name;
private T PositionObject<T>(Func<Coordinates, T> objectFactory)
{
var sector = GetRandomEmptySector();
_sectors[sector] = objectFactory.Invoke(sector);
return (T)_sectors[sector];
}
private void PositionObject(Func<Coordinates, object> objectFactory, int count)
{
for (int i = 0; i < count; i++)
{
PositionObject(objectFactory);
}
}
internal void Display(string textFormat)
{
if (!_entered)
{
_io.Write(textFormat, this);
_entered = true;
}
internal Coordinates Coordinates => _info.Coordinates;
internal bool HasKlingons => _info.KlingonCount > 0;
internal int KlingonCount => _info.KlingonCount;
internal bool HasStarbase => _info.HasStarbase;
internal Starbase Starbase { get; }
internal Galaxy Galaxy { get; }
internal bool EnterpriseIsNextToStarbase =>
_info.HasStarbase &&
Math.Abs(_enterprise.SectorCoordinates.X - Starbase.Sector.X) <= 1 &&
Math.Abs(_enterprise.SectorCoordinates.Y - Starbase.Sector.Y) <= 1;
internal IEnumerable<Klingon> Klingons => _sectors.Values.OfType<Klingon>();
public override string ToString() => _info.Name;
private T PositionObject<T>(Func<Coordinates, T> objectFactory)
if (_info.KlingonCount > 0)
{
var sector = GetRandomEmptySector();
_sectors[sector] = objectFactory.Invoke(sector);
return (T)_sectors[sector];
_io.Write(Strings.CombatArea);
if (_enterprise.ShieldControl.ShieldEnergy <= 200) { _io.Write(Strings.LowShields); }
}
private void PositionObject(Func<Coordinates, object> objectFactory, int count)
_enterprise.Execute(Command.SRS);
}
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))
{
for (int i = 0; i < count; i++)
{
PositionObject(objectFactory);
}
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 void Display(string textFormat)
{
if (!_entered)
{
_output.Write(textFormat, this);
_entered = true;
}
if (_info.KlingonCount > 0)
{
_output.Write(Strings.CombatArea);
if (_enterprise.ShieldControl.ShieldEnergy <= 200) { _output.Write(Strings.LowShields); }
}
_enterprise.Execute(Command.SRS);
}
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)
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);
_info.RemoveKlingon();
return "*** Klingon destroyed ***";
_sectors[newSector] = klingon;
klingon.MoveTo(newSector);
}
internal CommandResult KlingonsMoveAndFire()
return KlingonsFireOnEnterprise();
}
internal CommandResult KlingonsFireOnEnterprise()
{
if (EnterpriseIsNextToStarbase && Klingons.Any())
{
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; }
}
Starbase.ProtectEnterprise();
return CommandResult.Ok;
}
private Coordinates GetRandomEmptySector()
foreach (var klingon in Klingons)
{
while (true)
var result = klingon.FireOn(_enterprise);
if (result.IsGameOver) { return result; }
}
return CommandResult.Ok;
}
private Coordinates GetRandomEmptySector()
{
while (true)
{
var sector = _random.NextCoordinate();
if (!_sectors.ContainsKey(sector))
{
var sector = _random.GetCoordinate();
if (!_sectors.ContainsKey(sector))
{
return sector;
}
return sector;
}
}
}
internal IEnumerable<string> GetDisplayLines() => Enumerable.Range(0, 8).Select(x => GetDisplayLine(x));
internal IEnumerable<string> GetDisplayLines() => Enumerable.Range(0, 8).Select(x => GetDisplayLine(x));
private string GetDisplayLine(int x) =>
string.Join(
" ",
Enumerable
.Range(0, 8)
.Select(y => new Coordinates(x, y))
.Select(c => _sectors.GetValueOrDefault(c))
.Select(o => o?.ToString() ?? " "));
private string GetDisplayLine(int x) =>
string.Join(
" ",
Enumerable
.Range(0, 8)
.Select(y => new Coordinates(x, y))
.Select(c => _sectors.GetValueOrDefault(c))
.Select(o => o?.ToString() ?? " "));
internal void SetEnterpriseSector(Coordinates sector)
{
_sectors.Remove(_enterprise.SectorCoordinates);
_sectors[sector] = _enterprise;
}
internal void SetEnterpriseSector(Coordinates sector)
{
_sectors.Remove(_enterprise.SectorCoordinates);
_sectors[sector] = _enterprise;
}
}

View File

@@ -1,65 +1,66 @@
namespace SuperStarTrek.Space
using Games.Common.Randomness;
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 bool _isKnown;
private QuadrantInfo(Coordinates coordinates, string name, int klingonCount, int starCount, bool hasStarbase)
{
Coordinates = coordinates;
Name = name;
KlingonCount = klingonCount;
StarCount = starCount;
HasStarbase = hasStarbase;
}
internal Coordinates Coordinates { get; }
internal string Name { get; }
internal int KlingonCount { get; private set; }
internal bool HasStarbase { get; private set; }
internal int StarCount { get; }
internal static QuadrantInfo Create(Coordinates coordinates, string name, Random random)
{
var klingonCount = random.GetFloat() switch
{
> 0.98f => 3,
> 0.95f => 2,
> 0.80f => 1,
_ => 0
};
var hasStarbase = random.GetFloat() > 0.96f;
var starCount = random.Get1To8Inclusive();
return new QuadrantInfo(coordinates, name, klingonCount, starCount, hasStarbase);
}
internal void AddKlingon() => KlingonCount += 1;
internal void AddStarbase() => HasStarbase = true;
internal void MarkAsKnown() => _isKnown = true;
internal string Scan()
{
_isKnown = true;
return ToString();
}
public override string ToString() => _isKnown ? $"{KlingonCount}{(HasStarbase ? 1 : 0)}{StarCount}" : "***";
internal void RemoveKlingon()
{
if (KlingonCount > 0)
{
KlingonCount -= 1;
}
}
internal void RemoveStarbase() => HasStarbase = false;
Coordinates = coordinates;
Name = name;
KlingonCount = klingonCount;
StarCount = starCount;
HasStarbase = hasStarbase;
}
internal Coordinates Coordinates { get; }
internal string Name { get; }
internal int KlingonCount { get; private set; }
internal bool HasStarbase { get; private set; }
internal int StarCount { get; }
internal static QuadrantInfo Create(Coordinates coordinates, string name, IRandom random)
{
var klingonCount = random.NextFloat() switch
{
> 0.98f => 3,
> 0.95f => 2,
> 0.80f => 1,
_ => 0
};
var hasStarbase = random.NextFloat() > 0.96f;
var starCount = random.Next1To8Inclusive();
return new QuadrantInfo(coordinates, name, klingonCount, starCount, hasStarbase);
}
internal void AddKlingon() => KlingonCount += 1;
internal void AddStarbase() => HasStarbase = true;
internal void MarkAsKnown() => _isKnown = true;
internal string Scan()
{
_isKnown = true;
return ToString();
}
public override string ToString() => _isKnown ? $"{KlingonCount}{(HasStarbase ? 1 : 0)}{StarCount}" : "***";
internal void RemoveKlingon()
{
if (KlingonCount > 0)
{
KlingonCount -= 1;
}
}
internal void RemoveStarbase() => HasStarbase = false;
}

View File

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

View File

@@ -2,11 +2,15 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\*.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\00_Common\dotnet\Games.Common\Games.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,19 +1,19 @@
using Games.Common.IO;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems.ComputerFunctions
namespace SuperStarTrek.Systems.ComputerFunctions;
internal abstract class ComputerFunction
{
internal abstract class ComputerFunction
protected ComputerFunction(string description, IReadWrite io)
{
protected ComputerFunction(string description, Output output)
{
Description = description;
Output = output;
}
internal string Description { get; }
protected Output Output { get; }
internal abstract void Execute(Quadrant quadrant);
Description = description;
IO = io;
}
internal string Description { get; }
protected IReadWrite IO { get; }
internal abstract void Execute(Quadrant quadrant);
}

View File

@@ -1,21 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Games.Common.IO;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems.ComputerFunctions
namespace SuperStarTrek.Systems.ComputerFunctions;
internal class CumulativeGalacticRecord : GalacticReport
{
internal class CumulativeGalacticRecord : GalacticReport
internal CumulativeGalacticRecord(IReadWrite io, Galaxy galaxy)
: base("Cumulative galactic record", io, galaxy)
{
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));
}
protected override void WriteHeader(Quadrant quadrant)
{
IO.WriteLine();
IO.WriteLine($"Computer record of galaxy for quadrant {quadrant.Coordinates}");
IO.WriteLine();
}
protected override IEnumerable<string> GetRowData() =>
Galaxy.Quadrants.Select(row => " " + string.Join(" ", row));
}

View File

@@ -1,30 +1,30 @@
using Games.Common.IO;
using SuperStarTrek.Objects;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems.ComputerFunctions
namespace SuperStarTrek.Systems.ComputerFunctions;
internal class DirectionDistanceCalculator : NavigationCalculator
{
internal class DirectionDistanceCalculator : NavigationCalculator
private readonly Enterprise _enterprise;
private readonly IReadWrite _io;
internal DirectionDistanceCalculator(Enterprise enterprise, IReadWrite io)
: base("Direction/distance calculator", io)
{
private readonly Enterprise _enterprise;
private readonly Input _input;
_enterprise = enterprise;
_io = io;
}
internal DirectionDistanceCalculator(Enterprise enterprise, Output output, Input input)
: base("Direction/distance calculator", output)
{
_enterprise = enterprise;
_input = input;
}
internal override void Execute(Quadrant quadrant)
{
IO.WriteLine("Direction/distance calculator:");
IO.Write($"You are at quadrant {_enterprise.QuadrantCoordinates}");
IO.WriteLine($" sector {_enterprise.SectorCoordinates}");
IO.WriteLine("Please enter");
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"));
}
WriteDirectionAndDistance(
_io.GetCoordinates(" Initial coordinates"),
_io.GetCoordinates(" Final coordinates"));
}
}

View File

@@ -1,34 +1,34 @@
using System.Collections.Generic;
using System.Linq;
using Games.Common.IO;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems.ComputerFunctions
namespace SuperStarTrek.Systems.ComputerFunctions;
internal abstract class GalacticReport : ComputerFunction
{
internal abstract class GalacticReport : ComputerFunction
internal GalacticReport(string description, IReadWrite io, Galaxy galaxy)
: base(description, io)
{
internal 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);
IO.WriteLine(" 1 2 3 4 5 6 7 8");
IO.WriteLine(" ----- ----- ----- ----- ----- ----- ----- -----");
foreach (var (row, index) in GetRowData().Select((r, i) => (r, i)))
{
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(" ----- ----- ----- ----- ----- ----- ----- -----");
}
IO.WriteLine($" {index+1} {row}");
IO.WriteLine(" ----- ----- ----- ----- ----- ----- ----- -----");
}
}
}

View File

@@ -1,21 +1,21 @@
using System.Collections.Generic;
using System.Linq;
using Games.Common.IO;
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems.ComputerFunctions
namespace SuperStarTrek.Systems.ComputerFunctions;
internal class GalaxyRegionMap : GalacticReport
{
internal class GalaxyRegionMap : GalacticReport
internal GalaxyRegionMap(IReadWrite io, Galaxy galaxy)
: base("Galaxy 'region name' map", io, galaxy)
{
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'));
}
protected override void WriteHeader(Quadrant quadrant) =>
IO.WriteLine(" The Galaxy");
protected override IEnumerable<string> GetRowData() =>
Strings.RegionNames.Split('\n').Select(n => n.TrimEnd('\r'));
}

View File

@@ -1,29 +1,31 @@
using Games.Common.IO;
using SuperStarTrek.Space;
using SuperStarTrek.Utils;
namespace SuperStarTrek.Systems.ComputerFunctions
namespace SuperStarTrek.Systems.ComputerFunctions;
internal abstract class NavigationCalculator : ComputerFunction
{
internal abstract class NavigationCalculator : ComputerFunction
protected NavigationCalculator(string description, IReadWrite io)
: base(description, io)
{
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(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);
}
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}");
private void Write(float direction, float distance)
{
IO.WriteLine($"Direction = {direction}");
IO.WriteLine($"Distance = {distance}");
}
}

View File

@@ -1,30 +1,30 @@
using Games.Common.IO;
using SuperStarTrek.Objects;
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems.ComputerFunctions
namespace SuperStarTrek.Systems.ComputerFunctions;
internal class StarbaseDataCalculator : NavigationCalculator
{
internal class StarbaseDataCalculator : NavigationCalculator
private readonly Enterprise _enterprise;
internal StarbaseDataCalculator(Enterprise enterprise, IReadWrite io)
: base("Starbase nav data", io)
{
private readonly Enterprise _enterprise;
_enterprise = enterprise;
}
internal StarbaseDataCalculator(Enterprise enterprise, Output output)
: base("Starbase nav data", output)
internal override void Execute(Quadrant quadrant)
{
if (!quadrant.HasStarbase)
{
_enterprise = enterprise;
IO.WriteLine(Strings.NoStarbase);
return;
}
internal override void Execute(Quadrant quadrant)
{
if (!quadrant.HasStarbase)
{
Output.WriteLine(Strings.NoStarbase);
return;
}
IO.WriteLine("From Enterprise to Starbase:");
Output.WriteLine("From Enterprise to Starbase:");
WriteDirectionAndDistance(_enterprise.SectorCoordinates, quadrant.Starbase.Sector);
}
WriteDirectionAndDistance(_enterprise.SectorCoordinates, quadrant.Starbase.Sector);
}
}

View File

@@ -1,41 +1,43 @@
using Games.Common.IO;
using SuperStarTrek.Commands;
using SuperStarTrek.Objects;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems.ComputerFunctions
namespace SuperStarTrek.Systems.ComputerFunctions;
internal class StatusReport : ComputerFunction
{
internal class StatusReport : ComputerFunction
private readonly Game _game;
private readonly Galaxy _galaxy;
private readonly Enterprise _enterprise;
internal StatusReport(Game game, Galaxy galaxy, Enterprise enterprise, IReadWrite io)
: base("Status report", io)
{
private readonly Game _game;
private readonly Galaxy _galaxy;
private readonly Enterprise _enterprise;
_game = game;
_galaxy = galaxy;
_enterprise = enterprise;
}
internal StatusReport(Game game, Galaxy galaxy, Enterprise enterprise, Output output)
: base("Status report", output)
internal override void Execute(Quadrant quadrant)
{
IO.WriteLine(" Status report:");
IO.Write("Klingon".Pluralize(_galaxy.KlingonCount));
IO.WriteLine($" left: {_galaxy.KlingonCount}");
IO.WriteLine($"Mission must be completed in {_game.StardatesRemaining:0.#} stardates.");
if (_galaxy.StarbaseCount > 0)
{
_game = game;
_galaxy = galaxy;
_enterprise = enterprise;
IO.Write($"The Federation is maintaining {_galaxy.StarbaseCount} ");
IO.Write("starbase".Pluralize(_galaxy.StarbaseCount));
IO.WriteLine(" in the galaxy.");
}
else
{
IO.WriteLine("Your stupidity has left you on your own in");
IO.WriteLine(" the galaxy -- you have no starbases left!");
}
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);
}
_enterprise.Execute(Command.DAM);
}
}

View File

@@ -1,33 +1,33 @@
using Games.Common.IO;
using SuperStarTrek.Objects;
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems.ComputerFunctions
{
internal class TorpedoDataCalculator : NavigationCalculator
{
private readonly Enterprise _enterprise;
namespace SuperStarTrek.Systems.ComputerFunctions;
internal TorpedoDataCalculator(Enterprise enterprise, Output output)
: base("Photon torpedo data", output)
internal class TorpedoDataCalculator : NavigationCalculator
{
private readonly Enterprise _enterprise;
internal TorpedoDataCalculator(Enterprise enterprise, IReadWrite io)
: base("Photon torpedo data", io)
{
_enterprise = enterprise;
}
internal override void Execute(Quadrant quadrant)
{
if (!quadrant.HasKlingons)
{
_enterprise = enterprise;
IO.WriteLine(Strings.NoEnemyShips);
return;
}
internal override void Execute(Quadrant quadrant)
IO.WriteLine("From Enterprise to Klingon battle cruiser".Pluralize(quadrant.KlingonCount));
foreach (var klingon in quadrant.Klingons)
{
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);
}
WriteDirectionAndDistance(_enterprise.SectorCoordinates, klingon.Sector);
}
}
}

View File

@@ -1,54 +1,55 @@
using Games.Common.IO;
using SuperStarTrek.Commands;
using SuperStarTrek.Objects;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems
namespace SuperStarTrek.Systems;
internal class DamageControl : Subsystem
{
internal class DamageControl : Subsystem
private readonly Enterprise _enterprise;
private readonly IReadWrite _io;
internal DamageControl(Enterprise enterprise, IReadWrite io)
: base("Damage Control", Command.DAM, io)
{
private readonly Enterprise _enterprise;
private readonly Output _output;
_enterprise = enterprise;
_io = io;
}
internal DamageControl(Enterprise enterprise, Output output)
: base("Damage Control", Command.DAM, output)
protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
{
if (IsDamaged)
{
_enterprise = enterprise;
_output = output;
_io.WriteLine("Damage Control report not available");
}
else
{
_io.WriteLine();
WriteDamageReport();
}
protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
if (_enterprise.DamagedSystemCount > 0 && _enterprise.IsDocked)
{
if (IsDamaged)
if (quadrant.Starbase.TryRepair(_enterprise, out var repairTime))
{
_output.WriteLine("Damage Control report not available");
}
else
{
_output.NextLine();
WriteDamageReport();
return CommandResult.Elapsed(repairTime);
}
if (_enterprise.DamagedSystemCount > 0 && _enterprise.IsDocked)
{
if (quadrant.Starbase.TryRepair(_enterprise, out var repairTime))
{
WriteDamageReport();
return CommandResult.Elapsed(repairTime);
}
}
return CommandResult.Ok;
}
internal void WriteDamageReport()
return CommandResult.Ok;
}
internal void WriteDamageReport()
{
_io.WriteLine();
_io.WriteLine("Device State of Repair");
foreach (var system in _enterprise.Systems)
{
_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();
_io.Write(system.Name.PadRight(25));
_io.WriteLine((int)(system.Condition * 100) * 0.01F);
}
_io.WriteLine();
}
}

View File

@@ -1,46 +1,44 @@
using Games.Common.IO;
using SuperStarTrek.Commands;
using SuperStarTrek.Space;
using SuperStarTrek.Systems.ComputerFunctions;
namespace SuperStarTrek.Systems
namespace SuperStarTrek.Systems;
internal class LibraryComputer : Subsystem
{
internal class LibraryComputer : Subsystem
private readonly IReadWrite _io;
private readonly ComputerFunction[] _functions;
internal LibraryComputer(IReadWrite io, params ComputerFunction[] functions)
: base("Library-Computer", Command.COM, io)
{
private readonly Output _output;
private readonly Input _input;
private readonly ComputerFunction[] _functions;
_io = io;
_functions = functions;
}
internal LibraryComputer(Output output, Input input, params ComputerFunction[] functions)
: base("Library-Computer", Command.COM, output)
protected override bool CanExecuteCommand() => IsOperational("Computer disabled");
protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
{
var index = GetFunctionIndex();
_io.WriteLine();
_functions[index].Execute(quadrant);
return CommandResult.Ok;
}
private int GetFunctionIndex()
{
while (true)
{
_output = output;
_input = input;
_functions = functions;
}
var index = (int)_io.ReadNumber("Computer active and waiting command");
if (index >= 0 && index <= 5) { return index; }
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)
for (int i = 0; i < _functions.Length; i++)
{
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}");
}
_io.WriteLine($" {i} = {_functions[i].Description}");
}
}
}

View File

@@ -1,34 +1,35 @@
using System.Linq;
using Games.Common.IO;
using SuperStarTrek.Commands;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems
namespace SuperStarTrek.Systems;
internal class LongRangeSensors : Subsystem
{
internal class LongRangeSensors : Subsystem
private readonly Galaxy _galaxy;
private readonly IReadWrite _io;
internal LongRangeSensors(Galaxy galaxy, IReadWrite io)
: base("Long Range Sensors", Command.LRS, io)
{
private readonly Galaxy _galaxy;
private readonly Output _output;
_galaxy = galaxy;
_io = io;
}
internal LongRangeSensors(Galaxy galaxy, Output output)
: base("Long Range Sensors", Command.LRS, output)
protected override bool CanExecuteCommand() => IsOperational("{name} are inoperable");
protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
{
_io.WriteLine($"Long range scan for quadrant {quadrant.Coordinates}");
_io.WriteLine("-------------------");
foreach (var quadrants in _galaxy.GetNeighborhood(quadrant))
{
_galaxy = galaxy;
_output = output;
_io.WriteLine(": " + string.Join(" : ", quadrants.Select(q => q?.Scan() ?? "***")) + " :");
_io.WriteLine("-------------------");
}
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;
}
return CommandResult.Ok;
}
}

View File

@@ -1,97 +1,96 @@
using System.Linq;
using Games.Common.IO;
using Games.Common.Randomness;
using SuperStarTrek.Commands;
using SuperStarTrek.Objects;
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems
namespace SuperStarTrek.Systems;
internal class PhaserControl : Subsystem
{
internal class PhaserControl : Subsystem
private readonly Enterprise _enterprise;
private readonly IReadWrite _io;
private readonly IRandom _random;
internal PhaserControl(Enterprise enterprise, IReadWrite io, IRandom random)
: base("Phaser Control", Command.PHA, io)
{
private readonly Enterprise _enterprise;
private readonly Output _output;
private readonly Input _input;
private readonly Random _random;
_enterprise = enterprise;
_io = io;
_random = random;
}
internal PhaserControl(Enterprise enterprise, Output output, Input input, Random random)
: base("Phaser Control", Command.PHA, output)
protected override bool CanExecuteCommand() => IsOperational("Phasers inoperative");
protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
{
if (!quadrant.HasKlingons)
{
_enterprise = enterprise;
_output = output;
_input = input;
_random = random;
_io.WriteLine(Strings.NoEnemyShips);
return CommandResult.Ok;
}
protected override bool CanExecuteCommand() => IsOperational("Phasers inoperative");
protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
if (_enterprise.Computer.IsDamaged)
{
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();
_io.WriteLine("Computer failure hampers accuracy");
}
private float GetPhaserStrength()
{
while (true)
{
_output.WriteLine($"Energy available = {_enterprise.Energy} units");
var phaserStrength = _input.GetNumber("Number of units to fire");
_io.Write($"Phasers locked on target; ");
if (phaserStrength <= _enterprise.Energy) { return phaserStrength; }
}
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);
}
private float GetPerTargetPhaserStrength(float phaserStrength, int targetCount)
{
if (_enterprise.Computer.IsDamaged)
{
phaserStrength *= _random.GetFloat();
}
return quadrant.KlingonsFireOnEnterprise();
}
return phaserStrength / targetCount;
private float GetPhaserStrength()
{
while (true)
{
_io.WriteLine($"Energy available = {_enterprise.Energy} units");
var phaserStrength = _io.ReadNumber("Number of units to fire");
if (phaserStrength <= _enterprise.Energy) { return phaserStrength; }
}
}
private float GetPerTargetPhaserStrength(float phaserStrength, int targetCount)
{
if (_enterprise.Computer.IsDamaged)
{
phaserStrength *= _random.NextFloat();
}
private void ResolveHitOn(Klingon klingon, float perEnemyStrength, Quadrant quadrant)
{
var distance = _enterprise.SectorCoordinates.GetDistanceTo(klingon.Sector);
var hitStrength = (int)(perEnemyStrength / distance * (2 + _random.GetFloat()));
return phaserStrength / targetCount;
}
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}");
}
private void ResolveHitOn(Klingon klingon, float perEnemyStrength, Quadrant quadrant)
{
var distance = _enterprise.SectorCoordinates.GetDistanceTo(klingon.Sector);
var hitStrength = (int)(perEnemyStrength / distance * (2 + _random.NextFloat()));
if (klingon.TakeHit(hitStrength))
{
_io.WriteLine($"{hitStrength} unit hit on Klingon at sector {klingon.Sector}");
_io.WriteLine(
klingon.Energy <= 0
? quadrant.Remove(klingon)
: $" (sensors show {klingon.Energy} units remaining)");
}
else
{
_io.WriteLine($"Sensors show no damage to enemy at {klingon.Sector}");
}
}
}

View File

@@ -1,66 +1,64 @@
using Games.Common.IO;
using SuperStarTrek.Commands;
using SuperStarTrek.Objects;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems
namespace SuperStarTrek.Systems;
internal class PhotonTubes : Subsystem
{
internal class PhotonTubes : Subsystem
private readonly int _tubeCount;
private readonly Enterprise _enterprise;
private readonly IReadWrite _io;
internal PhotonTubes(int tubeCount, Enterprise enterprise, IReadWrite io)
: base("Photon Tubes", Command.TOR, io)
{
private readonly int _tubeCount;
private readonly Enterprise _enterprise;
private readonly Output _output;
private readonly Input _input;
internal PhotonTubes(int tubeCount, Enterprise enterprise, Output output, Input input)
: base("Photon Tubes", Command.TOR, output)
{
TorpedoCount = _tubeCount = tubeCount;
_enterprise = enterprise;
_output = output;
_input = input;
}
internal 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;
TorpedoCount = _tubeCount = tubeCount;
_enterprise = enterprise;
_io = io;
}
internal int TorpedoCount { get; private set; }
protected override bool CanExecuteCommand() => HasTorpedoes() && IsOperational("{name} are not operational");
private bool HasTorpedoes()
{
if (TorpedoCount > 0) { return true; }
_io.WriteLine("All photon torpedoes expended");
return false;
}
protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
{
if (!_io.TryReadCourse("Photon torpedo course", "Ensign Chekov", out var course))
{
return CommandResult.Ok;
}
TorpedoCount -= 1;
var isHit = false;
_io.WriteLine("Torpedo track:");
foreach (var sector in course.GetSectorsFrom(_enterprise.SectorCoordinates))
{
_io.WriteLine($" {sector}");
if (quadrant.TorpedoCollisionAt(sector, out var message, out var gameOver))
{
_io.WriteLine(message);
isHit = true;
if (gameOver) { return CommandResult.GameOver; }
break;
}
}
if (!isHit) { _io.WriteLine("Torpedo missed!"); }
return quadrant.KlingonsFireOnEnterprise();
}
internal void ReplenishTorpedoes() => TorpedoCount = _tubeCount;
}

View File

@@ -1,59 +1,57 @@
using Games.Common.IO;
using SuperStarTrek.Commands;
using SuperStarTrek.Objects;
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems
namespace SuperStarTrek.Systems;
internal class ShieldControl : Subsystem
{
internal class ShieldControl : Subsystem
private readonly Enterprise _enterprise;
private readonly IReadWrite _io;
internal ShieldControl(Enterprise enterprise, IReadWrite io)
: base("Shield Control", Command.SHE, io)
{
private readonly Enterprise _enterprise;
private readonly Output _output;
private readonly Input _input;
internal ShieldControl(Enterprise enterprise, Output output, Input input)
: base("Shield Control", Command.SHE, output)
{
_enterprise = enterprise;
_output = output;
_input = input;
}
internal float ShieldEnergy { get; set; }
protected override bool CanExecuteCommand() => IsOperational("{name} inoperable");
protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
{
_output.WriteLine($"Energy available = {_enterprise.TotalEnergy}");
var requested = _input.GetNumber($"Number of units to shields");
if (Validate(requested))
{
ShieldEnergy = requested;
_output.Write(Strings.ShieldsSet, requested);
}
else
{
_output.WriteLine("<SHIELDS UNCHANGED>");
}
return CommandResult.Ok;
}
private bool Validate(float requested)
{
if (requested > _enterprise.TotalEnergy)
{
_output.WriteLine("Shield Control reports, 'This is not the Federation Treasury.'");
return false;
}
return requested >= 0 && requested != ShieldEnergy;
}
internal void AbsorbHit(int hitStrength) => ShieldEnergy -= hitStrength;
internal void DropShields() => ShieldEnergy = 0;
_enterprise = enterprise;
_io = io;
}
internal float ShieldEnergy { get; set; }
protected override bool CanExecuteCommand() => IsOperational("{name} inoperable");
protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
{
_io.WriteLine($"Energy available = {_enterprise.TotalEnergy}");
var requested = _io.ReadNumber($"Number of units to shields");
if (Validate(requested))
{
ShieldEnergy = requested;
_io.Write(Strings.ShieldsSet, requested);
}
else
{
_io.WriteLine("<SHIELDS UNCHANGED>");
}
return CommandResult.Ok;
}
private bool Validate(float requested)
{
if (requested > _enterprise.TotalEnergy)
{
_io.WriteLine("Shield Control reports, 'This is not the Federation Treasury.'");
return false;
}
return requested >= 0 && requested != ShieldEnergy;
}
internal void AbsorbHit(int hitStrength) => ShieldEnergy -= hitStrength;
internal void DropShields() => ShieldEnergy = 0;
}

View File

@@ -2,61 +2,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Games.Common.IO;
using SuperStarTrek.Commands;
using SuperStarTrek.Objects;
using SuperStarTrek.Resources;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems
namespace SuperStarTrek.Systems;
internal class ShortRangeSensors : Subsystem
{
internal class ShortRangeSensors : Subsystem
private readonly Enterprise _enterprise;
private readonly Galaxy _galaxy;
private readonly Game _game;
private readonly IReadWrite _io;
internal ShortRangeSensors(Enterprise enterprise, Galaxy galaxy, Game game, IReadWrite io)
: base("Short Range Sensors", Command.SRS, io)
{
private readonly Enterprise _enterprise;
private readonly Galaxy _galaxy;
private readonly Game _game;
private readonly Output _output;
_enterprise = enterprise;
_galaxy = galaxy;
_game = game;
_io = io;
}
internal ShortRangeSensors(Enterprise enterprise, Galaxy galaxy, Game game, Output output)
: base("Short Range Sensors", Command.SRS, output)
protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
{
if (_enterprise.IsDocked)
{
_enterprise = enterprise;
_galaxy = galaxy;
_game = game;
_output = output;
_io.WriteLine(Strings.ShieldsDropped);
}
protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
if (Condition < 0)
{
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("---------------------------------");
return CommandResult.Ok;
_io.WriteLine(Strings.ShortRangeSensorsOut);
}
internal IEnumerable<string> GetStatusLines()
{
yield return $"Stardate {_game.Stardate}";
yield return $"Condition {_enterprise.Condition}";
yield return $"Quadrant {_enterprise.QuadrantCoordinates}";
yield return $"Sector {_enterprise.SectorCoordinates}";
yield return $"Photon torpedoes {_enterprise.PhotonTubes.TorpedoCount}";
yield return $"Total energy {Math.Ceiling(_enterprise.TotalEnergy)}";
yield return $"Shields {(int)_enterprise.ShieldControl.ShieldEnergy}";
yield return $"Klingons remaining {_galaxy.KlingonCount}";
}
_io.WriteLine("---------------------------------");
quadrant.GetDisplayLines()
.Zip(GetStatusLines(), (sectors, status) => $" {sectors} {status}")
.ToList()
.ForEach(l => _io.WriteLine(l));
_io.WriteLine("---------------------------------");
return CommandResult.Ok;
}
internal IEnumerable<string> GetStatusLines()
{
yield return $"Stardate {_game.Stardate}";
yield return $"Condition {_enterprise.Condition}";
yield return $"Quadrant {_enterprise.QuadrantCoordinates}";
yield return $"Sector {_enterprise.SectorCoordinates}";
yield return $"Photon torpedoes {_enterprise.PhotonTubes.TorpedoCount}";
yield return $"Total energy {Math.Ceiling(_enterprise.TotalEnergy)}";
yield return $"Shields {(int)_enterprise.ShieldControl.ShieldEnergy}";
yield return $"Klingons remaining {_galaxy.KlingonCount}";
}
}

View File

@@ -1,68 +1,68 @@
using Games.Common.IO;
using SuperStarTrek.Commands;
using SuperStarTrek.Space;
namespace SuperStarTrek.Systems
namespace SuperStarTrek.Systems;
internal abstract class Subsystem
{
internal abstract class Subsystem
private readonly IReadWrite _io;
protected Subsystem(string name, Command command, IReadWrite io)
{
private readonly Output _output;
protected Subsystem(string name, Command command, Output output)
{
Name = name;
Command = command;
Condition = 0;
_output = output;
}
internal string Name { get; }
internal float Condition { get; private set; }
internal bool IsDamaged => Condition < 0;
internal Command Command { get; }
protected virtual bool CanExecuteCommand() => true;
protected bool IsOperational(string notOperationalMessage)
{
if (IsDamaged)
{
_output.WriteLine(notOperationalMessage.Replace("{name}", Name));
return false;
}
return true;
}
internal CommandResult ExecuteCommand(Quadrant quadrant)
=> CanExecuteCommand() ? ExecuteCommandCore(quadrant) : CommandResult.Ok;
protected abstract CommandResult ExecuteCommandCore(Quadrant quadrant);
internal virtual void Repair()
{
if (IsDamaged)
{
Condition = 0;
}
}
internal 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;
Name = name;
Command = command;
Condition = 0;
_io = io;
}
internal string Name { get; }
internal float Condition { get; private set; }
internal bool IsDamaged => Condition < 0;
internal Command Command { get; }
protected virtual bool CanExecuteCommand() => true;
protected bool IsOperational(string notOperationalMessage)
{
if (IsDamaged)
{
_io.WriteLine(notOperationalMessage.Replace("{name}", Name));
return false;
}
return true;
}
internal CommandResult ExecuteCommand(Quadrant quadrant)
=> CanExecuteCommand() ? ExecuteCommandCore(quadrant) : CommandResult.Ok;
protected abstract CommandResult ExecuteCommandCore(Quadrant quadrant);
internal virtual void Repair()
{
if (IsDamaged)
{
Condition = 0;
}
}
internal 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

@@ -1,4 +1,5 @@
using System;
using Games.Common.IO;
using SuperStarTrek.Commands;
using SuperStarTrek.Objects;
using SuperStarTrek.Resources;
@@ -9,20 +10,18 @@ namespace SuperStarTrek.Systems
internal class WarpEngines : Subsystem
{
private readonly Enterprise _enterprise;
private readonly Output _output;
private readonly Input _input;
private readonly IReadWrite _io;
internal WarpEngines(Enterprise enterprise, Output output, Input input)
: base("Warp Engines", Command.NAV, output)
internal WarpEngines(Enterprise enterprise, IReadWrite io)
: base("Warp Engines", Command.NAV, io)
{
_enterprise = enterprise;
_output = output;
_input = input;
_io = io;
}
protected override CommandResult ExecuteCommandCore(Quadrant quadrant)
{
if (_input.TryGetCourse("Course", " Lt. Sulu", out var course) &&
if (_io.TryReadCourse("Course", " Lt. Sulu", out var course) &&
TryGetWarpFactor(out var warpFactor) &&
TryGetDistanceToMove(warpFactor, out var distanceToMove))
{
@@ -51,12 +50,12 @@ namespace SuperStarTrek.Systems
private bool TryGetWarpFactor(out float warpFactor)
{
var maximumWarp = IsDamaged ? 0.2f : 8;
if (_input.TryGetNumber("Warp Factor", 0, maximumWarp, out warpFactor))
if (_io.TryReadNumberInRange("Warp Factor", 0, maximumWarp, out warpFactor))
{
return warpFactor > 0;
}
_output.WriteLine(
_io.WriteLine(
IsDamaged && warpFactor > maximumWarp
? "Warp engines are damaged. Maximum speed = warp 0.2"
: $" Chief Engineer Scott reports, 'The engines won't take warp {warpFactor} !'");
@@ -69,14 +68,14 @@ namespace SuperStarTrek.Systems
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} !'");
_io.WriteLine("Engineering reports, 'Insufficient energy available");
_io.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.");
_io.Write($"Deflector control room acknowledges {_enterprise.ShieldControl.ShieldEnergy} ");
_io.WriteLine("units of energy");
_io.WriteLine(" presently deployed to shields.");
}
return false;