Merge pull request #1 from amjadkofahi/pr/860

This commit is contained in:
amjadkofahi
2023-05-25 02:18:13 +03:00
committed by GitHub
40 changed files with 819 additions and 3 deletions

View File

@@ -0,0 +1,45 @@
namespace Salvo;
internal record struct Coordinate(int Value)
{
public const int MinValue = 1;
public const int MaxValue = 10;
public static IEnumerable<Coordinate> Range => Enumerable.Range(1, 10).Select(v => new Coordinate(v));
public bool IsInRange => Value is >= MinValue and <= MaxValue;
public static Coordinate Create(float value) => new((int)value);
public static bool TryCreateValid(float value, out Coordinate coordinate)
{
coordinate = default;
if (value != (int)value) { return false; }
var result = Create(value);
if (result.IsInRange)
{
coordinate = result;
return true;
}
return false;
}
public Coordinate BringIntoRange(IRandom random)
=> Value switch
{
< MinValue => new(MinValue + (int)random.NextFloat(2.5F)),
> MaxValue => new(MaxValue - (int)random.NextFloat(2.5F)),
_ => this
};
public static implicit operator Coordinate(float value) => Create(value);
public static implicit operator int(Coordinate coordinate) => coordinate.Value;
public static Coordinate operator +(Coordinate coordinate, int offset) => new(coordinate.Value + offset);
public static int operator -(Coordinate a, Coordinate b) => a.Value - b.Value;
public override string ToString() => $" {Value} ";
}

View File

@@ -0,0 +1,27 @@
namespace Games.Common.IO;
internal static class IOExtensions
{
internal static Position ReadPosition(this IReadWrite io) => Position.Create(io.Read2Numbers(""));
internal static Position ReadValidPosition(this IReadWrite io)
{
while (true)
{
if (Position.TryCreateValid(io.Read2Numbers(""), out var position))
{
return position;
}
io.Write(Streams.Illegal);
}
}
internal static IEnumerable<Position> ReadPositions(this IReadWrite io, string shipName, int shipSize)
{
io.WriteLine(shipName);
for (var i = 0; i < shipSize; i++)
{
yield return io.ReadPosition();
}
}
}

View File

@@ -0,0 +1,32 @@
namespace Games.Common.Randomness;
internal static class RandomExtensions
{
internal static (Position, Offset) NextShipPosition(this IRandom random)
{
var startX = random.NextCoordinate();
var startY = random.NextCoordinate();
var deltaY = random.NextOffset();
var deltaX = random.NextOffset();
return (new(startX, startY), new(deltaX, deltaY));
}
private static Coordinate NextCoordinate(this IRandom random)
=> random.Next(Coordinate.MinValue, Coordinate.MaxValue + 1);
private static int NextOffset(this IRandom random) => random.Next(-1, 2);
internal static (Position, Offset) GetRandomShipPositionInRange(this IRandom random, int shipSize)
{
while (true)
{
var (start, delta) = random.NextShipPosition();
var shipSizeLessOne = shipSize - 1;
var end = start + delta * shipSizeLessOne;
if (delta != 0 && end.IsInRange)
{
return (start, delta);
}
}
}
}

66
77_Salvo/csharp/Fleet.cs Normal file
View File

@@ -0,0 +1,66 @@
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
namespace Salvo;
internal class Fleet
{
private readonly List<Ship> _ships;
internal Fleet(IReadWrite io)
{
io.WriteLine(Prompts.Coordinates);
_ships = new()
{
new Battleship(io),
new Cruiser(io),
new Destroyer("A", io),
new Destroyer("B", io)
};
}
internal Fleet(IRandom random)
{
_ships = new();
while (true)
{
_ships.Add(new Battleship(random));
if (TryPositionShip(() => new Cruiser(random)) &&
TryPositionShip(() => new Destroyer("A", random)) &&
TryPositionShip(() => new Destroyer("B", random)))
{
return;
}
_ships.Clear();
}
bool TryPositionShip(Func<Ship> shipFactory)
{
var shipGenerationAttempts = 0;
while (true)
{
var ship = shipFactory.Invoke();
shipGenerationAttempts++;
if (shipGenerationAttempts > 25) { return false; }
if (_ships.Min(ship.DistanceTo) >= 3.59)
{
_ships.Add(ship);
return true;
}
}
}
}
internal IEnumerable<Ship> Ships => _ships.AsEnumerable();
internal void ReceiveShots(IEnumerable<Position> shots, Action<Ship> reportHit)
{
foreach (var position in shots)
{
var ship = _ships.FirstOrDefault(s => s.IsHit(position));
if (ship == null) { continue; }
if (ship.IsDestroyed) { _ships.Remove(ship); }
reportHit(ship);
}
}
}

29
77_Salvo/csharp/Game.cs Normal file
View File

@@ -0,0 +1,29 @@
namespace Salvo;
internal class Game
{
private readonly IReadWrite _io;
private readonly IRandom _random;
public Game(IReadWrite io, IRandom random)
{
_io = io;
_random = random;
}
internal void Play()
{
_io.Write(Streams.Title);
var turnHandler = new TurnHandler(_io, _random);
_io.WriteLine();
Winner? winner;
do
{
winner = turnHandler.PlayTurn();
} while (winner == null);
_io.Write(winner == Winner.Computer ? Streams.IWon : Streams.YouWon);
}
}

25
77_Salvo/csharp/Offset.cs Normal file
View File

@@ -0,0 +1,25 @@
namespace Salvo;
internal record struct Offset(int X, int Y)
{
public static readonly Offset Zero = 0;
public static Offset operator *(Offset offset, int scale) => new(offset.X * scale, offset.Y * scale);
public static implicit operator Offset(int value) => new(value, value);
public static IEnumerable<Offset> Units
{
get
{
for (int x = -1; x <= 1; x++)
{
for (int y = -1; y <= 1; y++)
{
var offset = new Offset(x, y);
if (offset != Zero) { yield return offset; }
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
namespace Salvo;
internal record struct Position(Coordinate X, Coordinate Y)
{
public bool IsInRange => X.IsInRange && Y.IsInRange;
public bool IsOnDiagonal => X == Y;
public static Position Create((float X, float Y) coordinates) => new(coordinates.X, coordinates.Y);
public static bool TryCreateValid((float X, float Y) coordinates, out Position position)
{
if (Coordinate.TryCreateValid(coordinates.X, out var x) && Coordinate.TryCreateValid(coordinates.Y, out var y))
{
position = new(x, y);
return true;
}
position = default;
return false;
}
public static IEnumerable<Position> All
=> Coordinate.Range.SelectMany(x => Coordinate.Range.Select(y => new Position(x, y)));
public IEnumerable<Position> Neighbours
{
get
{
foreach (var offset in Offset.Units)
{
var neighbour = this + offset;
if (neighbour.IsInRange) { yield return neighbour; }
}
}
}
internal float DistanceTo(Position other)
{
var (deltaX, deltaY) = (X - other.X, Y - other.Y);
return (float)Math.Sqrt(deltaX * deltaX + deltaY * deltaY);
}
internal Position BringIntoRange(IRandom random)
=> IsInRange ? this : new(X.BringIntoRange(random), Y.BringIntoRange(random));
public static Position operator +(Position position, Offset offset)
=> new(position.X + offset.X, position.Y + offset.Y);
public static implicit operator Position(int value) => new(value, value);
public override string ToString() => $"{X}{Y}";
}

View File

@@ -0,0 +1,9 @@
global using System;
global using Games.Common.IO;
global using Games.Common.Randomness;
global using Salvo;
global using Salvo.Ships;
global using static Salvo.Resources.Resource;
//new Game(new ConsoleIO(), new RandomNumberGenerator()).Play();
new Game(new ConsoleIO(), new DataRandom()).Play();

View File

@@ -0,0 +1 @@
Enter coordinates for...

View File

@@ -0,0 +1 @@
I have more shots than blank squares.

View File

@@ -0,0 +1 @@
I have {0} shots.

View File

@@ -0,0 +1 @@
I hit your {0}

View File

@@ -0,0 +1 @@
I have won.

View File

@@ -0,0 +1 @@
Illegal, enter again.

View File

@@ -0,0 +1,49 @@
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Salvo.Resources;
internal static class Resource
{
internal static class Streams
{
public static Stream Title => GetStream();
public static Stream YouHaveMoreShotsThanSquares => GetStream();
public static Stream YouWon => GetStream();
public static Stream IHaveMoreShotsThanSquares => GetStream();
public static Stream IWon => GetStream();
public static Stream Illegal => GetStream();
}
internal static class Strings
{
public static string WhereAreYourShips => GetString();
public static string YouHaveShots(int number) => Format(number);
public static string IHaveShots(int number) => Format(number);
public static string YouHit(string shipName) => Format(shipName);
public static string IHit(string shipName) => Format(shipName);
public static string ShotBefore(int turnNumber) => Format(turnNumber);
public static string Turn(int number) => Format(number);
}
internal static class Prompts
{
public static string Coordinates => GetString();
public static string Start => GetString();
public static string SeeShots => GetString();
}
private static string Format<T>(T value, [CallerMemberName] string? name = null)
=> string.Format(GetString(name), value);
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($"{typeof(Resource).Namespace}.{name}.txt")
?? throw new Exception($"Could not find embedded resource stream '{name}'.");
}

View File

@@ -0,0 +1 @@
Do you want to see my shots

View File

@@ -0,0 +1 @@
You shot there before on turn {0}

View File

@@ -0,0 +1 @@
Do you want to start

View File

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

View File

@@ -0,0 +1,2 @@
Turn {0}

View File

@@ -0,0 +1 @@
Where are your ships?

View File

@@ -0,0 +1 @@
You have more shots than there are blank squares.

View File

@@ -0,0 +1 @@
You have {0} shots.

View File

@@ -0,0 +1 @@
You hit my {0}.

View File

@@ -0,0 +1 @@
You have won.

View File

@@ -2,8 +2,16 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<LangVersion>10</LangVersion>
<LangVersion>11</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources/*.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\00_Common\dotnet\Games.Common\Games.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,17 @@
namespace Salvo.Ships;
internal sealed class Battleship : Ship
{
internal Battleship(IReadWrite io)
: base(io)
{
}
internal Battleship(IRandom random)
: base(random)
{
}
internal override int Shots => 3;
internal override int Size => 5;
}

View File

@@ -0,0 +1,17 @@
namespace Salvo.Ships;
internal sealed class Cruiser : Ship
{
internal Cruiser(IReadWrite io)
: base(io)
{
}
internal Cruiser(IRandom random)
: base(random)
{
}
internal override int Shots => 2;
internal override int Size => 3;
}

View File

@@ -0,0 +1,17 @@
namespace Salvo.Ships;
internal sealed class Destroyer : Ship
{
internal Destroyer(string nameIndex, IReadWrite io)
: base(io, $"<{nameIndex}>")
{
}
internal Destroyer(string nameIndex, IRandom random)
: base(random, $"<{nameIndex}>")
{
}
internal override int Shots => 1;
internal override int Size => 2;
}

View File

@@ -0,0 +1,37 @@
namespace Salvo.Ships;
internal abstract class Ship
{
private readonly List<Position> _positions = new();
protected Ship(IReadWrite io, string? nameSuffix = null)
{
Name = GetType().Name + nameSuffix;
_positions = io.ReadPositions(Name, Size).ToList();
}
protected Ship(IRandom random, string? nameSuffix = null)
{
Name = GetType().Name + nameSuffix;
var (start, delta) = random.GetRandomShipPositionInRange(Size);
for (var i = 0; i < Size; i++)
{
_positions.Add(start + delta * i);
}
}
internal string Name { get; }
internal abstract int Shots { get; }
internal abstract int Size { get; }
internal bool IsDamaged => _positions.Count > 0 && _positions.Count < Size;
internal bool IsDestroyed => _positions.Count == 0;
internal bool IsHit(Position position) => _positions.Remove(position);
internal float DistanceTo(Ship other)
=> _positions.SelectMany(a => other._positions.Select(b => a.DistanceTo(b))).Min();
public override string ToString()
=> string.Join(Environment.NewLine, _positions.Select(p => p.ToString()).Prepend(Name));
}

View File

@@ -0,0 +1,33 @@
namespace Salvo.Targetting;
internal class ComputerShotSelector : ShotSelector
{
private readonly KnownHitsShotSelectionStrategy _knownHitsStrategy;
private readonly SearchPatternShotSelectionStrategy _searchPatternStrategy;
private readonly IReadWrite _io;
private readonly bool _showShots;
internal ComputerShotSelector(Fleet source, IRandom random, IReadWrite io)
: base(source)
{
_knownHitsStrategy = new KnownHitsShotSelectionStrategy(this);
_searchPatternStrategy = new SearchPatternShotSelectionStrategy(this, random);
_io = io;
_showShots = io.ReadString(Prompts.SeeShots).Equals("yes", StringComparison.InvariantCultureIgnoreCase);
}
protected override IEnumerable<Position> GetShots()
{
var shots = GetSelectionStrategy().GetShots(NumberOfShots).ToArray();
if (_showShots)
{
_io.WriteLine(string.Join(Environment.NewLine, shots));
}
return shots;
}
internal void RecordHit(Ship ship, int turn) => _knownHitsStrategy.RecordHit(ship, turn);
private ShotSelectionStrategy GetSelectionStrategy()
=> _knownHitsStrategy.KnowsOfDamagedShips ? _knownHitsStrategy : _searchPatternStrategy;
}

View File

@@ -0,0 +1,34 @@
namespace Salvo.Targetting;
internal class HumanShotSelector : ShotSelector
{
private readonly IReadWrite _io;
internal HumanShotSelector(Fleet source, IReadWrite io)
: base(source)
{
_io = io;
}
protected override IEnumerable<Position> GetShots()
{
var shots = new Position[NumberOfShots];
for (var i = 0; i < shots.Length; i++)
{
while (true)
{
var position = _io.ReadValidPosition();
if (WasSelectedPreviously(position, out var turnTargeted))
{
_io.WriteLine($"YOU SHOT THERE BEFORE ON TURN {turnTargeted}");
continue;
}
shots[i] = position;
break;
}
}
return shots;
}
}

View File

@@ -0,0 +1,71 @@
namespace Salvo.Targetting;
internal class KnownHitsShotSelectionStrategy : ShotSelectionStrategy
{
private readonly List<(int Turn, Ship Ship)> _damagedShips = new();
internal KnownHitsShotSelectionStrategy(ShotSelector shotSelector)
: base(shotSelector)
{
}
internal bool KnowsOfDamagedShips => _damagedShips.Any();
internal override IEnumerable<Position> GetShots(int numberOfShots)
{
var tempGrid = Position.All.ToDictionary(x => x, _ => 0);
var shots = Enumerable.Range(1, numberOfShots).Select(x => new Position(x, x)).ToArray();
foreach (var (hitTurn, ship) in _damagedShips)
{
foreach (var position in Position.All)
{
if (WasSelectedPreviously(position))
{
tempGrid[position]=-10000000;
continue;
}
foreach (var neighbour in position.Neighbours)
{
if (WasSelectedPreviously(neighbour, out var turn) && turn == hitTurn)
{
tempGrid[position] += hitTurn + 10 - position.Y * ship.Shots;
}
}
}
}
foreach (var position in Position.All)
{
var Q9=0;
for (var i = 0; i < numberOfShots; i++)
{
if (tempGrid[shots[i]] < tempGrid[shots[Q9]])
{
Q9 = i;
}
}
if (position.X <= numberOfShots && position.IsOnDiagonal) { continue; }
if (tempGrid[position]<tempGrid[shots[Q9]]) { continue; }
if (!shots.Contains(position))
{
shots[Q9] = position;
}
}
return shots;
}
internal void RecordHit(Ship ship, int turn)
{
if (ship.IsDestroyed)
{
_damagedShips.RemoveAll(x => x.Ship == ship);
}
else
{
_damagedShips.Add((turn, ship));
}
}
}

View File

@@ -0,0 +1,22 @@
using System.Collections.Immutable;
namespace Salvo.Targetting;
internal class SearchPattern
{
private static readonly ImmutableArray<Offset> _offsets =
ImmutableArray.Create<Offset>(new(1, 1), new(-1, 1), new(1, -3), new(1, 1), new(0, 2), new(-1, 1));
private int _nextIndex;
internal bool TryGetOffset(out Offset offset)
{
offset = default;
if (_nextIndex >= _offsets.Length) { return false; }
offset = _offsets[_nextIndex++];
return true;
}
internal void Reset() => _nextIndex = 0;
}

View File

@@ -0,0 +1,55 @@
namespace Salvo.Targetting;
internal class SearchPatternShotSelectionStrategy : ShotSelectionStrategy
{
private const int MaxSearchPatternAttempts = 100;
private readonly IRandom _random;
private readonly SearchPattern _searchPattern = new();
private readonly List<Position> _shots = new();
internal SearchPatternShotSelectionStrategy(ShotSelector shotSelector, IRandom random)
: base(shotSelector)
{
_random = random;
}
internal override IEnumerable<Position> GetShots(int numberOfShots)
{
_shots.Clear();
while(_shots.Count < numberOfShots)
{
var (seed, _) = _random.NextShipPosition();
SearchFrom(numberOfShots, seed);
}
return _shots;
}
private void SearchFrom(int numberOfShots, Position candidateShot)
{
var attemptsLeft = MaxSearchPatternAttempts;
while (true)
{
_searchPattern.Reset();
if (attemptsLeft-- == 0) { return; }
candidateShot = candidateShot.BringIntoRange(_random);
if (FindValidShots(numberOfShots, ref candidateShot)) { return; }
}
}
private bool FindValidShots(int numberOfShots, ref Position candidateShot)
{
while (true)
{
if (IsValidShot(candidateShot))
{
_shots.Add(candidateShot);
if (_shots.Count == numberOfShots) { return true; }
}
if (!_searchPattern.TryGetOffset(out var offset)) { return false; }
candidateShot += offset;
}
}
private bool IsValidShot(Position candidate)
=> candidate.IsInRange && !WasSelectedPreviously(candidate) && !_shots.Contains(candidate);
}

View File

@@ -0,0 +1,17 @@
namespace Salvo.Targetting;
internal abstract class ShotSelectionStrategy
{
private readonly ShotSelector _shotSelector;
protected ShotSelectionStrategy(ShotSelector shotSelector)
{
_shotSelector = shotSelector;
}
internal abstract IEnumerable<Position> GetShots(int numberOfShots);
protected bool WasSelectedPreviously(Position position) => _shotSelector.WasSelectedPreviously(position);
protected bool WasSelectedPreviously(Position position, out int turn)
=> _shotSelector.WasSelectedPreviously(position, out turn);
}

View File

@@ -0,0 +1,31 @@
namespace Salvo.Targetting;
internal abstract class ShotSelector
{
private readonly Fleet _source;
private readonly Dictionary<Position, int> _previousShots = new();
internal ShotSelector(Fleet source)
{
_source = source;
}
internal int NumberOfShots => _source.Ships.Sum(s => s.Shots);
internal bool CanTargetAllRemainingSquares => NumberOfShots >= 100 - _previousShots.Count;
internal bool WasSelectedPreviously(Position position) => _previousShots.ContainsKey(position);
internal bool WasSelectedPreviously(Position position, out int turn)
=> _previousShots.TryGetValue(position, out turn);
internal IEnumerable<Position> GetShots(int turnNumber)
{
foreach (var shot in GetShots())
{
_previousShots.Add(shot, turnNumber);
yield return shot;
}
}
protected abstract IEnumerable<Position> GetShots();
}

View File

@@ -0,0 +1,92 @@
using Salvo.Targetting;
namespace Salvo;
internal class TurnHandler
{
private readonly IReadWrite _io;
private readonly Fleet _humanFleet;
private readonly Fleet _computerFleet;
private readonly bool _humanStarts;
private readonly HumanShotSelector _humanShotSelector;
private readonly ComputerShotSelector _computerShotSelector;
private readonly Func<Winner?> _turnAction;
private int _turnNumber;
public TurnHandler(IReadWrite io, IRandom random)
{
_io = io;
_computerFleet = new Fleet(random);
_humanFleet = new Fleet(io);
_turnAction = AskWhoStarts()
? () => PlayHumanTurn() ?? PlayComputerTurn()
: () => PlayComputerTurn() ?? PlayHumanTurn();
_humanShotSelector = new HumanShotSelector(_humanFleet, io);
_computerShotSelector = new ComputerShotSelector(_computerFleet, random, io);
}
public Winner? PlayTurn()
{
_io.Write(Strings.Turn(++_turnNumber));
return _turnAction.Invoke();
}
private bool AskWhoStarts()
{
while (true)
{
var startResponse = _io.ReadString(Prompts.Start);
if (startResponse.Equals(Strings.WhereAreYourShips, StringComparison.InvariantCultureIgnoreCase))
{
foreach (var ship in _computerFleet.Ships)
{
_io.WriteLine(ship);
}
}
else
{
return startResponse.Equals("yes", StringComparison.InvariantCultureIgnoreCase);
}
}
}
private Winner? PlayComputerTurn()
{
var numberOfShots = _computerShotSelector.NumberOfShots;
_io.Write(Strings.IHaveShots(numberOfShots));
if (numberOfShots == 0) { return Winner.Human; }
if (_computerShotSelector.CanTargetAllRemainingSquares)
{
_io.Write(Streams.IHaveMoreShotsThanSquares);
return Winner.Computer;
}
_humanFleet.ReceiveShots(
_computerShotSelector.GetShots(_turnNumber),
ship =>
{
_io.Write(Strings.IHit(ship.Name));
_computerShotSelector.RecordHit(ship, _turnNumber);
});
return null;
}
private Winner? PlayHumanTurn()
{
var numberOfShots = _humanShotSelector.NumberOfShots;
_io.Write(Strings.YouHaveShots(numberOfShots));
if (numberOfShots == 0) { return Winner.Computer; }
if (_humanShotSelector.CanTargetAllRemainingSquares)
{
_io.WriteLine(Streams.YouHaveMoreShotsThanSquares);
return Winner.Human;
}
_computerFleet.ReceiveShots(
_humanShotSelector.GetShots(_turnNumber),
ship => _io.Write(Strings.YouHit(ship.Name)));
return null;
}
}

View File

@@ -0,0 +1,7 @@
namespace Salvo;
internal enum Winner
{
Human,
Computer
}

View File

@@ -1,3 +1,6 @@
Original source downloaded [from Vintage Basic](http://www.vintage-basic.net/games.html)
Original source downloaded [from Vintage Basic][def]
Conversion to [JavaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Shells)
Conversion to [****J****avaScript](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Shells)
[def]: http://www.vintage-basic.net/games.html