mirror of
https://github.com/coding-horror/basic-computer-games.git
synced 2026-02-01 09:44:48 -08:00
Merge pull request #1 from amjadkofahi/pr/860
This commit is contained in:
45
77_Salvo/csharp/Coordinate.cs
Normal file
45
77_Salvo/csharp/Coordinate.cs
Normal 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} ";
|
||||
}
|
||||
27
77_Salvo/csharp/Extensions/IOExtensions.cs
Normal file
27
77_Salvo/csharp/Extensions/IOExtensions.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
77_Salvo/csharp/Extensions/RandomExtensions.cs
Normal file
32
77_Salvo/csharp/Extensions/RandomExtensions.cs
Normal 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
66
77_Salvo/csharp/Fleet.cs
Normal 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
29
77_Salvo/csharp/Game.cs
Normal 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
25
77_Salvo/csharp/Offset.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
52
77_Salvo/csharp/Position.cs
Normal file
52
77_Salvo/csharp/Position.cs
Normal 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}";
|
||||
}
|
||||
9
77_Salvo/csharp/Program.cs
Normal file
9
77_Salvo/csharp/Program.cs
Normal 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();
|
||||
1
77_Salvo/csharp/Resources/Coordinates.txt
Normal file
1
77_Salvo/csharp/Resources/Coordinates.txt
Normal file
@@ -0,0 +1 @@
|
||||
Enter coordinates for...
|
||||
1
77_Salvo/csharp/Resources/IHaveMoreShotsThanSquares.txt
Normal file
1
77_Salvo/csharp/Resources/IHaveMoreShotsThanSquares.txt
Normal file
@@ -0,0 +1 @@
|
||||
I have more shots than blank squares.
|
||||
1
77_Salvo/csharp/Resources/IHaveShots.txt
Normal file
1
77_Salvo/csharp/Resources/IHaveShots.txt
Normal file
@@ -0,0 +1 @@
|
||||
I have {0} shots.
|
||||
1
77_Salvo/csharp/Resources/IHit.txt
Normal file
1
77_Salvo/csharp/Resources/IHit.txt
Normal file
@@ -0,0 +1 @@
|
||||
I hit your {0}
|
||||
1
77_Salvo/csharp/Resources/IWon.txt
Normal file
1
77_Salvo/csharp/Resources/IWon.txt
Normal file
@@ -0,0 +1 @@
|
||||
I have won.
|
||||
1
77_Salvo/csharp/Resources/Illegal.txt
Normal file
1
77_Salvo/csharp/Resources/Illegal.txt
Normal file
@@ -0,0 +1 @@
|
||||
Illegal, enter again.
|
||||
49
77_Salvo/csharp/Resources/Resource.cs
Normal file
49
77_Salvo/csharp/Resources/Resource.cs
Normal 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}'.");
|
||||
}
|
||||
1
77_Salvo/csharp/Resources/SeeShots.txt
Normal file
1
77_Salvo/csharp/Resources/SeeShots.txt
Normal file
@@ -0,0 +1 @@
|
||||
Do you want to see my shots
|
||||
1
77_Salvo/csharp/Resources/ShotBefore.txt
Normal file
1
77_Salvo/csharp/Resources/ShotBefore.txt
Normal file
@@ -0,0 +1 @@
|
||||
You shot there before on turn {0}
|
||||
1
77_Salvo/csharp/Resources/Start.txt
Normal file
1
77_Salvo/csharp/Resources/Start.txt
Normal file
@@ -0,0 +1 @@
|
||||
Do you want to start
|
||||
5
77_Salvo/csharp/Resources/Title.txt
Normal file
5
77_Salvo/csharp/Resources/Title.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
Salvo
|
||||
Creative Computing Morristown, New Jersey
|
||||
|
||||
|
||||
|
||||
2
77_Salvo/csharp/Resources/Turn.txt
Normal file
2
77_Salvo/csharp/Resources/Turn.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
|
||||
Turn {0}
|
||||
1
77_Salvo/csharp/Resources/WhereAreYourShips.txt
Normal file
1
77_Salvo/csharp/Resources/WhereAreYourShips.txt
Normal file
@@ -0,0 +1 @@
|
||||
Where are your ships?
|
||||
@@ -0,0 +1 @@
|
||||
You have more shots than there are blank squares.
|
||||
1
77_Salvo/csharp/Resources/YouHaveShots.txt
Normal file
1
77_Salvo/csharp/Resources/YouHaveShots.txt
Normal file
@@ -0,0 +1 @@
|
||||
You have {0} shots.
|
||||
1
77_Salvo/csharp/Resources/YouHit.txt
Normal file
1
77_Salvo/csharp/Resources/YouHit.txt
Normal file
@@ -0,0 +1 @@
|
||||
You hit my {0}.
|
||||
1
77_Salvo/csharp/Resources/YouWon.txt
Normal file
1
77_Salvo/csharp/Resources/YouWon.txt
Normal file
@@ -0,0 +1 @@
|
||||
You have won.
|
||||
@@ -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>
|
||||
|
||||
17
77_Salvo/csharp/Ships/Battleship.cs
Normal file
17
77_Salvo/csharp/Ships/Battleship.cs
Normal 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;
|
||||
}
|
||||
17
77_Salvo/csharp/Ships/Cruiser.cs
Normal file
17
77_Salvo/csharp/Ships/Cruiser.cs
Normal 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;
|
||||
}
|
||||
17
77_Salvo/csharp/Ships/Destroyer.cs
Normal file
17
77_Salvo/csharp/Ships/Destroyer.cs
Normal 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;
|
||||
}
|
||||
37
77_Salvo/csharp/Ships/Ship.cs
Normal file
37
77_Salvo/csharp/Ships/Ship.cs
Normal 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));
|
||||
}
|
||||
33
77_Salvo/csharp/Targetting/ComputerShotSelector.cs
Normal file
33
77_Salvo/csharp/Targetting/ComputerShotSelector.cs
Normal 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;
|
||||
}
|
||||
34
77_Salvo/csharp/Targetting/HumanShotSelector.cs
Normal file
34
77_Salvo/csharp/Targetting/HumanShotSelector.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
71
77_Salvo/csharp/Targetting/KnownHitsShotSelectionStrategy.cs
Normal file
71
77_Salvo/csharp/Targetting/KnownHitsShotSelectionStrategy.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
22
77_Salvo/csharp/Targetting/SearchPattern.cs
Normal file
22
77_Salvo/csharp/Targetting/SearchPattern.cs
Normal 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;
|
||||
}
|
||||
55
77_Salvo/csharp/Targetting/SearchPatternShotSelector.cs
Normal file
55
77_Salvo/csharp/Targetting/SearchPatternShotSelector.cs
Normal 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);
|
||||
}
|
||||
17
77_Salvo/csharp/Targetting/ShotSelectionStrategy.cs
Normal file
17
77_Salvo/csharp/Targetting/ShotSelectionStrategy.cs
Normal 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);
|
||||
}
|
||||
31
77_Salvo/csharp/Targetting/ShotSelector.cs
Normal file
31
77_Salvo/csharp/Targetting/ShotSelector.cs
Normal 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();
|
||||
}
|
||||
92
77_Salvo/csharp/TurnHandler.cs
Normal file
92
77_Salvo/csharp/TurnHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
7
77_Salvo/csharp/Winner.cs
Normal file
7
77_Salvo/csharp/Winner.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Salvo;
|
||||
|
||||
internal enum Winner
|
||||
{
|
||||
Human,
|
||||
Computer
|
||||
}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user