diff --git a/77_Salvo/csharp/Coordinate.cs b/77_Salvo/csharp/Coordinate.cs new file mode 100644 index 00000000..4cd2c433 --- /dev/null +++ b/77_Salvo/csharp/Coordinate.cs @@ -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 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} "; +} diff --git a/77_Salvo/csharp/Extensions/IOExtensions.cs b/77_Salvo/csharp/Extensions/IOExtensions.cs new file mode 100644 index 00000000..6d021deb --- /dev/null +++ b/77_Salvo/csharp/Extensions/IOExtensions.cs @@ -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 ReadPositions(this IReadWrite io, string shipName, int shipSize) + { + io.WriteLine(shipName); + for (var i = 0; i < shipSize; i++) + { + yield return io.ReadPosition(); + } + } +} diff --git a/77_Salvo/csharp/Extensions/RandomExtensions.cs b/77_Salvo/csharp/Extensions/RandomExtensions.cs new file mode 100644 index 00000000..e83ab265 --- /dev/null +++ b/77_Salvo/csharp/Extensions/RandomExtensions.cs @@ -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); + } + } + } +} diff --git a/77_Salvo/csharp/Fleet.cs b/77_Salvo/csharp/Fleet.cs new file mode 100644 index 00000000..5f267225 --- /dev/null +++ b/77_Salvo/csharp/Fleet.cs @@ -0,0 +1,66 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; + +namespace Salvo; + +internal class Fleet +{ + private readonly List _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 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 Ships => _ships.AsEnumerable(); + + internal void ReceiveShots(IEnumerable shots, Action 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); + } + } +} diff --git a/77_Salvo/csharp/Game.cs b/77_Salvo/csharp/Game.cs new file mode 100644 index 00000000..a0223d8b --- /dev/null +++ b/77_Salvo/csharp/Game.cs @@ -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); + } +} diff --git a/77_Salvo/csharp/Offset.cs b/77_Salvo/csharp/Offset.cs new file mode 100644 index 00000000..3c873f10 --- /dev/null +++ b/77_Salvo/csharp/Offset.cs @@ -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 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; } + } + } + } + } +} diff --git a/77_Salvo/csharp/Position.cs b/77_Salvo/csharp/Position.cs new file mode 100644 index 00000000..3bafe217 --- /dev/null +++ b/77_Salvo/csharp/Position.cs @@ -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 All + => Coordinate.Range.SelectMany(x => Coordinate.Range.Select(y => new Position(x, y))); + + public IEnumerable 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}"; +} diff --git a/77_Salvo/csharp/Program.cs b/77_Salvo/csharp/Program.cs new file mode 100644 index 00000000..5dde4d05 --- /dev/null +++ b/77_Salvo/csharp/Program.cs @@ -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(); diff --git a/77_Salvo/csharp/Resources/Coordinates.txt b/77_Salvo/csharp/Resources/Coordinates.txt new file mode 100644 index 00000000..387d7a6b --- /dev/null +++ b/77_Salvo/csharp/Resources/Coordinates.txt @@ -0,0 +1 @@ +Enter coordinates for... diff --git a/77_Salvo/csharp/Resources/IHaveMoreShotsThanSquares.txt b/77_Salvo/csharp/Resources/IHaveMoreShotsThanSquares.txt new file mode 100644 index 00000000..a6f37110 --- /dev/null +++ b/77_Salvo/csharp/Resources/IHaveMoreShotsThanSquares.txt @@ -0,0 +1 @@ +I have more shots than blank squares. diff --git a/77_Salvo/csharp/Resources/IHaveShots.txt b/77_Salvo/csharp/Resources/IHaveShots.txt new file mode 100644 index 00000000..9157e1a7 --- /dev/null +++ b/77_Salvo/csharp/Resources/IHaveShots.txt @@ -0,0 +1 @@ +I have {0} shots. diff --git a/77_Salvo/csharp/Resources/IHit.txt b/77_Salvo/csharp/Resources/IHit.txt new file mode 100644 index 00000000..3b43216c --- /dev/null +++ b/77_Salvo/csharp/Resources/IHit.txt @@ -0,0 +1 @@ +I hit your {0} diff --git a/77_Salvo/csharp/Resources/IWon.txt b/77_Salvo/csharp/Resources/IWon.txt new file mode 100644 index 00000000..b3f50634 --- /dev/null +++ b/77_Salvo/csharp/Resources/IWon.txt @@ -0,0 +1 @@ +I have won. diff --git a/77_Salvo/csharp/Resources/Illegal.txt b/77_Salvo/csharp/Resources/Illegal.txt new file mode 100644 index 00000000..6a71787a --- /dev/null +++ b/77_Salvo/csharp/Resources/Illegal.txt @@ -0,0 +1 @@ +Illegal, enter again. diff --git a/77_Salvo/csharp/Resources/Resource.cs b/77_Salvo/csharp/Resources/Resource.cs new file mode 100644 index 00000000..8be52fdb --- /dev/null +++ b/77_Salvo/csharp/Resources/Resource.cs @@ -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 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}'."); +} \ No newline at end of file diff --git a/77_Salvo/csharp/Resources/SeeShots.txt b/77_Salvo/csharp/Resources/SeeShots.txt new file mode 100644 index 00000000..7f608b0b --- /dev/null +++ b/77_Salvo/csharp/Resources/SeeShots.txt @@ -0,0 +1 @@ +Do you want to see my shots \ No newline at end of file diff --git a/77_Salvo/csharp/Resources/ShotBefore.txt b/77_Salvo/csharp/Resources/ShotBefore.txt new file mode 100644 index 00000000..18e90fcb --- /dev/null +++ b/77_Salvo/csharp/Resources/ShotBefore.txt @@ -0,0 +1 @@ +You shot there before on turn {0} diff --git a/77_Salvo/csharp/Resources/Start.txt b/77_Salvo/csharp/Resources/Start.txt new file mode 100644 index 00000000..283a0fbb --- /dev/null +++ b/77_Salvo/csharp/Resources/Start.txt @@ -0,0 +1 @@ +Do you want to start \ No newline at end of file diff --git a/77_Salvo/csharp/Resources/Title.txt b/77_Salvo/csharp/Resources/Title.txt new file mode 100644 index 00000000..6b2ba9b3 --- /dev/null +++ b/77_Salvo/csharp/Resources/Title.txt @@ -0,0 +1,5 @@ + Salvo + Creative Computing Morristown, New Jersey + + + diff --git a/77_Salvo/csharp/Resources/Turn.txt b/77_Salvo/csharp/Resources/Turn.txt new file mode 100644 index 00000000..14bf316e --- /dev/null +++ b/77_Salvo/csharp/Resources/Turn.txt @@ -0,0 +1,2 @@ + +Turn {0} diff --git a/77_Salvo/csharp/Resources/WhereAreYourShips.txt b/77_Salvo/csharp/Resources/WhereAreYourShips.txt new file mode 100644 index 00000000..e8391a46 --- /dev/null +++ b/77_Salvo/csharp/Resources/WhereAreYourShips.txt @@ -0,0 +1 @@ +Where are your ships? \ No newline at end of file diff --git a/77_Salvo/csharp/Resources/YouHaveMoreShotsThanSquares.txt b/77_Salvo/csharp/Resources/YouHaveMoreShotsThanSquares.txt new file mode 100644 index 00000000..04d58f92 --- /dev/null +++ b/77_Salvo/csharp/Resources/YouHaveMoreShotsThanSquares.txt @@ -0,0 +1 @@ +You have more shots than there are blank squares. diff --git a/77_Salvo/csharp/Resources/YouHaveShots.txt b/77_Salvo/csharp/Resources/YouHaveShots.txt new file mode 100644 index 00000000..43abeda8 --- /dev/null +++ b/77_Salvo/csharp/Resources/YouHaveShots.txt @@ -0,0 +1 @@ +You have {0} shots. diff --git a/77_Salvo/csharp/Resources/YouHit.txt b/77_Salvo/csharp/Resources/YouHit.txt new file mode 100644 index 00000000..b5b5d761 --- /dev/null +++ b/77_Salvo/csharp/Resources/YouHit.txt @@ -0,0 +1 @@ +You hit my {0}. diff --git a/77_Salvo/csharp/Resources/YouWon.txt b/77_Salvo/csharp/Resources/YouWon.txt new file mode 100644 index 00000000..1f71343b --- /dev/null +++ b/77_Salvo/csharp/Resources/YouWon.txt @@ -0,0 +1 @@ +You have won. diff --git a/77_Salvo/csharp/Salvo.csproj b/77_Salvo/csharp/Salvo.csproj index d3fe4757..51470ec9 100644 --- a/77_Salvo/csharp/Salvo.csproj +++ b/77_Salvo/csharp/Salvo.csproj @@ -2,8 +2,16 @@ Exe net6.0 - 10 + 11 enable enable + + + + + + + + diff --git a/77_Salvo/csharp/Ships/Battleship.cs b/77_Salvo/csharp/Ships/Battleship.cs new file mode 100644 index 00000000..4f864eb6 --- /dev/null +++ b/77_Salvo/csharp/Ships/Battleship.cs @@ -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; +} diff --git a/77_Salvo/csharp/Ships/Cruiser.cs b/77_Salvo/csharp/Ships/Cruiser.cs new file mode 100644 index 00000000..d004f24c --- /dev/null +++ b/77_Salvo/csharp/Ships/Cruiser.cs @@ -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; +} diff --git a/77_Salvo/csharp/Ships/Destroyer.cs b/77_Salvo/csharp/Ships/Destroyer.cs new file mode 100644 index 00000000..6523395d --- /dev/null +++ b/77_Salvo/csharp/Ships/Destroyer.cs @@ -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; +} diff --git a/77_Salvo/csharp/Ships/Ship.cs b/77_Salvo/csharp/Ships/Ship.cs new file mode 100644 index 00000000..ee204d3f --- /dev/null +++ b/77_Salvo/csharp/Ships/Ship.cs @@ -0,0 +1,37 @@ +namespace Salvo.Ships; + +internal abstract class Ship +{ + private readonly List _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)); +} diff --git a/77_Salvo/csharp/Targetting/ComputerShotSelector.cs b/77_Salvo/csharp/Targetting/ComputerShotSelector.cs new file mode 100644 index 00000000..721f50c5 --- /dev/null +++ b/77_Salvo/csharp/Targetting/ComputerShotSelector.cs @@ -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 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; +} diff --git a/77_Salvo/csharp/Targetting/HumanShotSelector.cs b/77_Salvo/csharp/Targetting/HumanShotSelector.cs new file mode 100644 index 00000000..ea49bf12 --- /dev/null +++ b/77_Salvo/csharp/Targetting/HumanShotSelector.cs @@ -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 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; + } +} diff --git a/77_Salvo/csharp/Targetting/KnownHitsShotSelectionStrategy.cs b/77_Salvo/csharp/Targetting/KnownHitsShotSelectionStrategy.cs new file mode 100644 index 00000000..5e0b9d2c --- /dev/null +++ b/77_Salvo/csharp/Targetting/KnownHitsShotSelectionStrategy.cs @@ -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 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] x.Ship == ship); + } + else + { + _damagedShips.Add((turn, ship)); + } + } +} diff --git a/77_Salvo/csharp/Targetting/SearchPattern.cs b/77_Salvo/csharp/Targetting/SearchPattern.cs new file mode 100644 index 00000000..ac2071c8 --- /dev/null +++ b/77_Salvo/csharp/Targetting/SearchPattern.cs @@ -0,0 +1,22 @@ +using System.Collections.Immutable; + +namespace Salvo.Targetting; + +internal class SearchPattern +{ + private static readonly ImmutableArray _offsets = + ImmutableArray.Create(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; +} \ No newline at end of file diff --git a/77_Salvo/csharp/Targetting/SearchPatternShotSelector.cs b/77_Salvo/csharp/Targetting/SearchPatternShotSelector.cs new file mode 100644 index 00000000..1eae88f8 --- /dev/null +++ b/77_Salvo/csharp/Targetting/SearchPatternShotSelector.cs @@ -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 _shots = new(); + + internal SearchPatternShotSelectionStrategy(ShotSelector shotSelector, IRandom random) + : base(shotSelector) + { + _random = random; + } + + internal override IEnumerable 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); +} \ No newline at end of file diff --git a/77_Salvo/csharp/Targetting/ShotSelectionStrategy.cs b/77_Salvo/csharp/Targetting/ShotSelectionStrategy.cs new file mode 100644 index 00000000..b4cf14b1 --- /dev/null +++ b/77_Salvo/csharp/Targetting/ShotSelectionStrategy.cs @@ -0,0 +1,17 @@ +namespace Salvo.Targetting; + +internal abstract class ShotSelectionStrategy +{ + private readonly ShotSelector _shotSelector; + protected ShotSelectionStrategy(ShotSelector shotSelector) + { + _shotSelector = shotSelector; + } + + internal abstract IEnumerable GetShots(int numberOfShots); + + protected bool WasSelectedPreviously(Position position) => _shotSelector.WasSelectedPreviously(position); + + protected bool WasSelectedPreviously(Position position, out int turn) + => _shotSelector.WasSelectedPreviously(position, out turn); +} diff --git a/77_Salvo/csharp/Targetting/ShotSelector.cs b/77_Salvo/csharp/Targetting/ShotSelector.cs new file mode 100644 index 00000000..695dbc42 --- /dev/null +++ b/77_Salvo/csharp/Targetting/ShotSelector.cs @@ -0,0 +1,31 @@ +namespace Salvo.Targetting; + +internal abstract class ShotSelector +{ + private readonly Fleet _source; + private readonly Dictionary _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 GetShots(int turnNumber) + { + foreach (var shot in GetShots()) + { + _previousShots.Add(shot, turnNumber); + yield return shot; + } + } + + protected abstract IEnumerable GetShots(); +} diff --git a/77_Salvo/csharp/TurnHandler.cs b/77_Salvo/csharp/TurnHandler.cs new file mode 100644 index 00000000..afde9613 --- /dev/null +++ b/77_Salvo/csharp/TurnHandler.cs @@ -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 _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; + } +} diff --git a/77_Salvo/csharp/Winner.cs b/77_Salvo/csharp/Winner.cs new file mode 100644 index 00000000..a5a73eb4 --- /dev/null +++ b/77_Salvo/csharp/Winner.cs @@ -0,0 +1,7 @@ +namespace Salvo; + +internal enum Winner +{ + Human, + Computer +}