전함!
2003 년 (17 살 때)에 전함 AI 코딩 경쟁에 참가했습니다. 토너먼트를 잃어 버렸지 만 많은 재미가 있었고 많은 것을 배웠습니다.
이제 최고의 전함 AI를 찾아이 경쟁을 부활시키고 싶습니다.
다음은 현재 Bitbucket에서 호스팅되는 프레임 워크 입니다.
우승자는 +450의 평판을 얻습니다! 대회는 2009 년 11 월 17 일에 시작됩니다 . 17 일 0시 이후의 항목이나 편집 내용은 허용되지 않습니다. (중부 표준시) 응모작을 일찍 제출하여 기회를 놓치지 마십시오!
이 목표 를 유지하려면 경쟁의 정신을 따르십시오.
게임의 규칙:
- 게임은 10×10 그리드에서 재생됩니다.
- 각 선수는 5 척의 배 (길이 2, 3, 3, 4, 5)를 그리드에 배치합니다.
- 선박은 겹칠 수 없지만 인접 할 수 있습니다.
- 그런 다음 선수들은 상대편에서 차례대로 한 번의 발사를한다.
- 게임의 변형으로 발리 당 생존 한 배당 하나씩 여러 샷을 발사 할 수 있습니다.
- 상대방은 샷이 가라 앉거나 부딪 치면 경쟁자에게 알립니다.
- 한 플레이어의 모든 함선이 침몰되면 게임 플레이가 종료됩니다.
경쟁 규칙 :
- 경쟁의 정신은 최고의 전함 알고리즘을 찾는 것입니다.
- 경쟁의 정신에 반하는 것으로 여겨지는 것은 실격의 근거가됩니다.
- 상대방을 방해하는 것은 경쟁의 정신에 위배됩니다.
- 멀티 스레딩은 다음 제한 사항에 따라 사용될 수 있습니다.
- 당신의 차례가 아닌 동안 하나 이상의 스레드가 실행될 수 있습니다. (다수의 스레드가 “일시 중단”상태 일 수 있습니다).
- “정상”이외의 우선 순위로 스레드를 실행할 수 없습니다.
- 위의 두 가지 제한 사항을 감안할 때, 턴 동안 최소 3 개의 전용 CPU 코어가 보장됩니다.
- 게임당 1 초의 CPU 시간 제한은 기본 스레드의 각 경쟁자에게 할당됩니다.
- 시간이 없으면 현재 게임에서 패배합니다.
- 처리되지 않은 예외는 현재 게임에서 패배합니다.
- 네트워크 액세스 및 디스크 액세스는 허용되지만 시간 제한은 상당히 금지되어 있습니다. 그러나 시간 변형을 완화하기 위해 몇 가지 설정 및 분해 방법이 추가되었습니다.
- 코드는 스택 오버플로에 대한 답변 또는 너무 큰 경우 링크로 게시해야합니다.
- 항목의 최대 총 크기 (비 압축)는 1MB입니다.
- 공식적으로 .Net 2.0 / 3.5는 유일한 프레임 워크 요구 사항입니다.
- 항목은 IBattleshipOpponent 인터페이스를 구현해야합니다.
채점 :
- 101 개 게임 중 최고의 51 개 게임이 경기의 승자입니다.
- 모든 선수는 라운드 로빈 스타일로 서로 경기를합니다.
- 그런 다음 경쟁사 중 최고 절반이 이중 제거 토너먼트를 플레이하여 승자를 결정합니다. (실제로 절반보다 크거나 같은 2의 가장 작은 거듭 제곱)
- 토너먼트에 TournamentApi 프레임 워크를 사용할 것 입니다.
- 결과가 여기에 게시됩니다.
- 두 개 이상의 출품작을 제출하면 최고 점수를받은 출품작 만 이중 심사를받을 수 있습니다.
행운을 빕니다! 즐기세요!
편집 1 : 함수 에서 오류를 발견 한 Freed 에게
감사드립니다 . 수정되었습니다. 업데이트 된 버전의 프레임 워크를 다운로드하십시오.Ship.IsValid
편집 2 :
디스크에 통계를 유지하는 데 상당한 관심이 있었기 때문에 필자는 필요한 기능을 제공 해야하는 시간이 설정되지 않은 설정 및 해제 이벤트를 몇 가지 추가했습니다. 이것은 반 파괴적인 변화 입니다. 즉, 인터페이스가 기능을 추가하도록 수정되었지만 본문이 필요하지 않습니다. 업데이트 된 버전의 프레임 워크를 다운로드하십시오.
편집 3 :
버그 수정 1 : GameWon
와 GameLost
한 번에 아웃의 경우 전화 받고 있었다.
버그 수정 2 : 엔진이 모든 게임의 시간을 초과했다면 경쟁은 끝나지 않을 것입니다.
업데이트 된 버전의 프레임 워크를 다운로드하십시오.
편집 4 :
토너먼트 결과 :
답변
경기당 더 많은 게임을하기 위해 모션을 두 번째로합니다. 50 게임을하는 것은 동전을 뒤집는 것입니다. 테스트 알고리즘을 합리적으로 구별하려면 1000 게임을해야했습니다.
Dreadnought 1.2를 다운로드하십시오 .
전략 :
-
적중 수가 0보다 큰 선박의 모든 가능한 위치를 추적하십시오. 목록은 ~ 30K보다 커지지 않으므로 모든 선박의 가능한 모든 위치 목록과 달리 정확하게 유지할 수 있습니다 (매우 큽니다).
-
GetShot 알고리즘에는 두 가지 부분이 있습니다. 하나는 임의 샷을 생성하고 다른 하나는 이미 적중 한 선박의 침몰을 시도합니다. 모든 적중 함이 침몰 할 수있는 위치 (위 목록에서)가있을 경우 무작위 샷을합니다. 그렇지 않으면, 우리는 가능한 가장 많은 위치 (가중치)를 제거 할 수있는 촬영 장소를 선택하여 선박 침몰을 완료하려고합니다.
-
무작위 샷의 경우, 위치가 겹치지 않은 배송되지 않은 선박 중 하나의 가능성을 기반으로 가장 적합한 위치를 계산하십시오.
-
적을 통계적으로 발사하기 어려운 위치에 선박을 배치하는 적응 알고리즘.
-
상대가 통계적으로 배를 배치 할 가능성이 더 높은 위치에서 촬영하는 것을 선호하는 적응 알고리즘.
-
배는 주로 서로 접촉하지 않습니다.
답변
여기 내 항목이 있습니다! (가장 순진한 해결책)
“임의의 1.1”
namespace Battleship
{
using System;
using System.Collections.ObjectModel;
using System.Drawing;
public class RandomOpponent : IBattleshipOpponent
{
public string Name { get { return "Random"; } }
public Version Version { get { return this.version; } }
Random rand = new Random();
Version version = new Version(1, 1);
Size gameSize;
public void NewGame(Size size, TimeSpan timeSpan)
{
this.gameSize = size;
}
public void PlaceShips(ReadOnlyCollection<Ship> ships)
{
foreach (Ship s in ships)
{
s.Place(
new Point(
rand.Next(this.gameSize.Width),
rand.Next(this.gameSize.Height)),
(ShipOrientation)rand.Next(2));
}
}
public Point GetShot()
{
return new Point(
rand.Next(this.gameSize.Width),
rand.Next(this.gameSize.Height));
}
public void NewMatch(string opponent) { }
public void OpponentShot(Point shot) { }
public void ShotHit(Point shot, bool sunk) { }
public void ShotMiss(Point shot) { }
public void GameWon() { }
public void GameLost() { }
public void MatchOver() { }
}
}
답변
사람들이 상대하는 상대는 다음과 같습니다.
고정 지오메트리에서 영감을 얻은 전략을 사용하는 대신, 특정 미 탐사 공간이 배를 보유하고있는 근본적인 확률 을 추정하는 것이 흥미로울 것이라고 생각했습니다 .
이를 위해 현재의 세계관에 맞는 가능한 모든 선박 구성을 탐색 한 다음 해당 구성을 기반으로 확률을 계산합니다. 나무를 탐험하는 것처럼 생각할 수 있습니다.
가능한 전함 상태의 확장 http://natekohl.net/media/battleship-tree.png
세계에 대해 알고있는 것과 같은 나무의 모든 잎을 고려한 후 (예를 들어, 선박이 겹칠 수없고, 적중 한 모든 정사각형은 선박 등이어야 함) 각 미 탐사 위치에서 선박이 얼마나 자주 발생하는지 계산하여 그 가능성을 추정 할 수 있습니다. 배가 앉아있다
이것은 핫스팟이 선박을 포함 할 가능성이 높은 히트 맵으로 시각화 할 수 있습니다.
각각의 미 탐사 위치에 대한 확률 열지도 http://natekohl.net/media/battleship-probs.png
이 전함 경쟁에서 내가 좋아하는 것 중 하나는 위의 트리가 이런 종류의 알고리즘을 무차별화할 수있을 정도로 작다는 것입니다. 5 대의 선박 각각에 대해 ~ 150 개의 가능한 위치가 있다면, 그것은 150 5 = 750 억 가능성입니다. 그리고 그 숫자는 특히 배 전체를 제거 할 수 있다면 더 작아집니다.
위에 링크 된 상대는 전체 트리를 탐색하지 않습니다. 750 억은 여전히 1 초도 채 걸리지 않습니다. 그러나 몇 가지 휴리스틱의 도움으로 이러한 확률을 추정하려고 시도합니다.
답변
본격적인 답변은 아니지만 일반적인 코드로 실제 답변을 어지럽히는 것은 거의 없습니다. 따라서 오픈 소스의 정신으로 일부 확장 / 일반 클래스를 제시합니다. 이것을 사용하면 네임 스페이스를 변경하거나 모든 것을 하나의 dll로 컴파일하려고하면 작동하지 않습니다.
BoardView를 사용하면 주석이 달린 보드를 쉽게 사용할 수 있습니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.IO;
namespace Battleship.ShuggyCoUk
{
public enum Compass
{
North,East,South,West
}
class Cell<T>
{
private readonly BoardView<T> view;
public readonly int X;
public readonly int Y;
public T Data;
public double Bias { get; set; }
public Cell(BoardView<T> view, int x, int y)
{
this.view = view; this.X = x; this.Y = y; this.Bias = 1.0;
}
public Point Location
{
get { return new Point(X, Y); }
}
public IEnumerable<U> FoldAll<U>(U acc, Func<Cell<T>, U, U> trip)
{
return new[] { Compass.North, Compass.East, Compass.South, Compass.West }
.Select(x => FoldLine(x, acc, trip));
}
public U FoldLine<U>(Compass direction, U acc, Func<Cell<T>, U, U> trip)
{
var cell = this;
while (true)
{
switch (direction)
{
case Compass.North:
cell = cell.North; break;
case Compass.East:
cell = cell.East; break;
case Compass.South:
cell = cell.South; break;
case Compass.West:
cell = cell.West; break;
}
if (cell == null)
return acc;
acc = trip(cell, acc);
}
}
public Cell<T> North
{
get { return view.SafeLookup(X, Y - 1); }
}
public Cell<T> South
{
get { return view.SafeLookup(X, Y + 1); }
}
public Cell<T> East
{
get { return view.SafeLookup(X+1, Y); }
}
public Cell<T> West
{
get { return view.SafeLookup(X-1, Y); }
}
public IEnumerable<Cell<T>> Neighbours()
{
if (North != null)
yield return North;
if (South != null)
yield return South;
if (East != null)
yield return East;
if (West != null)
yield return West;
}
}
class BoardView<T> : IEnumerable<Cell<T>>
{
public readonly Size Size;
private readonly int Columns;
private readonly int Rows;
private Cell<T>[] history;
public BoardView(Size size)
{
this.Size = size;
Columns = size.Width;
Rows = size.Height;
this.history = new Cell<T>[Columns * Rows];
for (int y = 0; y < Rows; y++)
{
for (int x = 0; x < Rows; x++)
history[x + y * Columns] = new Cell<T>(this, x, y);
}
}
public T this[int x, int y]
{
get { return history[x + y * Columns].Data; }
set { history[x + y * Columns].Data = value; }
}
public T this[Point p]
{
get { return history[SafeCalc(p.X, p.Y, true)].Data; }
set { this.history[SafeCalc(p.X, p.Y, true)].Data = value; }
}
private int SafeCalc(int x, int y, bool throwIfIllegal)
{
if (x < 0 || y < 0 || x >= Columns || y >= Rows)
{ if (throwIfIllegal)
throw new ArgumentOutOfRangeException("["+x+","+y+"]");
else
return -1;
}
return x + y * Columns;
}
public void Set(T data)
{
foreach (var cell in this.history)
cell.Data = data;
}
public Cell<T> SafeLookup(int x, int y)
{
int index = SafeCalc(x, y, false);
if (index < 0)
return null;
return history[index];
}
#region IEnumerable<Cell<T>> Members
public IEnumerator<Cell<T>> GetEnumerator()
{
foreach (var cell in this.history)
yield return cell;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
public BoardView<U> Transform<U>(Func<T, U> transform)
{
var result = new BoardView<U>(new Size(Columns, Rows));
for (int y = 0; y < Rows; y++)
{
for (int x = 0; x < Columns; x++)
{
result[x,y] = transform(this[x, y]);
}
}
return result;
}
public void WriteAsGrid(TextWriter w)
{
WriteAsGrid(w, "{0}");
}
public void WriteAsGrid(TextWriter w, string format)
{
WriteAsGrid(w, x => string.Format(format, x.Data));
}
public void WriteAsGrid(TextWriter w, Func<Cell<T>,string> perCell)
{
for (int y = 0; y < Rows; y++)
{
for (int x = 0; x < Columns; x++)
{
if (x != 0)
w.Write(",");
w.Write(perCell(this.SafeLookup(x, y)));
}
w.WriteLine();
}
}
#endregion
}
}
일부 확장,이 중 일부는 기본 프레임 워크에서 기능을 복제하지만 실제로 사용자가 수행해야합니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Collections.ObjectModel;
namespace Battleship.ShuggyCoUk
{
public static class Extensions
{
public static bool IsIn(this Point p, Size size)
{
return p.X >= 0 && p.Y >= 0 && p.X < size.Width && p.Y < size.Height;
}
public static bool IsLegal(this Ship ship,
IEnumerable<Ship> ships,
Size board,
Point location,
ShipOrientation direction)
{
var temp = new Ship(ship.Length);
temp.Place(location, direction);
if (!temp.GetAllLocations().All(p => p.IsIn(board)))
return false;
return ships.Where(s => s.IsPlaced).All(s => !s.ConflictsWith(temp));
}
public static bool IsTouching(this Point a, Point b)
{
return (a.X == b.X - 1 || a.X == b.X + 1) &&
(a.Y == b.Y - 1 || a.Y == b.Y + 1);
}
public static bool IsTouching(this Ship ship,
IEnumerable<Ship> ships,
Point location,
ShipOrientation direction)
{
var temp = new Ship(ship.Length);
temp.Place(location, direction);
var occupied = new HashSet<Point>(ships
.Where(s => s.IsPlaced)
.SelectMany(s => s.GetAllLocations()));
if (temp.GetAllLocations().Any(p => occupied.Any(b => b.IsTouching(p))))
return true;
return false;
}
public static ReadOnlyCollection<Ship> MakeShips(params int[] lengths)
{
return new System.Collections.ObjectModel.ReadOnlyCollection<Ship>(
lengths.Select(l => new Ship(l)).ToList());
}
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Rand rand)
{
T[] elements = source.ToArray();
// Note i > 0 to avoid final pointless iteration
for (int i = elements.Length - 1; i > 0; i--)
{
// Swap element "i" with a random earlier element it (or itself)
int swapIndex = rand.Next(i + 1);
T tmp = elements[i];
elements[i] = elements[swapIndex];
elements[swapIndex] = tmp;
}
// Lazily yield (avoiding aliasing issues etc)
foreach (T element in elements)
{
yield return element;
}
}
public static T RandomOrDefault<T>(this IEnumerable<T> things, Rand rand)
{
int count = things.Count();
if (count == 0)
return default(T);
return things.ElementAt(rand.Next(count));
}
}
}
내가 많이 사용하는 것.
enum OpponentsBoardState
{
Unknown = 0,
Miss,
MustBeEmpty,
Hit,
}
무작위 화. 안전하지만 테스트 가능하며 테스트에 유용합니다.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
namespace Battleship.ShuggyCoUk
{
public class Rand
{
Random r;
public Rand()
{
var rand = System.Security.Cryptography.RandomNumberGenerator.Create();
byte[] b = new byte[4];
rand.GetBytes(b);
r = new Random(BitConverter.ToInt32(b, 0));
}
public int Next(int maxValue)
{
return r.Next(maxValue);
}
public double NextDouble(double maxValue)
{
return r.NextDouble() * maxValue;
}
public T Pick<T>(IEnumerable<T> things)
{
return things.ElementAt(Next(things.Count()));
}
public T PickBias<T>(Func<T, double> bias, IEnumerable<T> things)
{
double d = NextDouble(things.Sum(x => bias(x)));
foreach (var x in things)
{
if (d < bias(x))
return x;
d -= bias(x);
}
throw new InvalidOperationException("fell off the end!");
}
}
}
답변
지금은 본격적인 알고리즘을 작성할 시간이 없지만 생각은 다음과 같습니다. 상대방이 무작위로 배를 배치하면 배치 확률이 (5.5,5.5)를 중심으로 한 단순한 분포가 아닐까요? 예를 들어, x 차원에서 전함 (5 단위 길이)의 배치 가능성은 다음과 같습니다.
x 1 2 3 4 5 6 7 8 9 10
P(x) 2 4 6 8 10 10 8 6 4 2
y에 대해서도 동일한 계산이 유효합니다. 다른 함선은 분포가 가파르 지 않지만 가장 좋은 추측은 여전히 중심입니다. 그 후, 수학적 접근 방식은 중심에서 대각선 (아마도 평균 선박의 길이, 17/5)을 천천히 방출합니다. 전의:
...........
....x.x....
.....x.....
....x.x....
...........
분명히 임의의 무작위성이 아이디어에 추가되어야 할 것이지만, 나는 순수하게 수학적으로 그것이 갈 길이라고 생각합니다.
답변
그다지 세련된 것은 없지만 여기에 내가 생각해 낸 것입니다. 그것은 무작위 상대의 99.9 %를 이겼다. 누군가 이와 같은 작은 도전이 있다면 흥미로울 것입니다.
namespace Battleship
{
using System;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Collections.Generic;
using System.Linq;
public class AgentSmith : IBattleshipOpponent
{
public string Name { get { return "Agent Smith"; } }
public Version Version { get { return this.version; } }
private Random rand = new Random();
private Version version = new Version(2, 1);
private Size gameSize;
private enum Direction { Up, Down, Left, Right }
private int MissCount;
private Point?[] EndPoints = new Point?[2];
private LinkedList<Point> HitShots = new LinkedList<Point>();
private LinkedList<Point> Shots = new LinkedList<Point>();
private List<Point> PatternShots = new List<Point>();
private Direction ShotDirection = Direction.Up;
private void NullOutTarget()
{
EndPoints = new Point?[2];
MissCount = 0;
}
private void SetupPattern()
{
for (int y = 0; y < gameSize.Height; y++)
for (int x = 0; x < gameSize.Width; x++)
if ((x + y) % 2 == 0) PatternShots.Add(new Point(x, y));
}
private bool InvalidShot(Point p)
{
bool InvalidShot = (Shots.Where(s => s.X == p.X && s.Y == p.Y).Any());
if (p.X < 0 | p.Y<0) InvalidShot = true;
if (p.X >= gameSize.Width | p.Y >= gameSize.Height) InvalidShot = true;
return InvalidShot;
}
private Point FireDirectedShot(Direction? direction, Point p)
{
ShotDirection = (Direction)direction;
switch (ShotDirection)
{
case Direction.Up: p.Y--; break;
case Direction.Down: p.Y++; break;
case Direction.Left: p.X--; break;
case Direction.Right: p.X++; break;
}
return p;
}
private Point FireAroundPoint(Point p)
{
if (!InvalidShot(FireDirectedShot(ShotDirection,p)))
return FireDirectedShot(ShotDirection, p);
Point testShot = FireDirectedShot(Direction.Left, p);
if (InvalidShot(testShot)) { testShot = FireDirectedShot(Direction.Right, p); }
if (InvalidShot(testShot)) { testShot = FireDirectedShot(Direction.Up, p); }
if (InvalidShot(testShot)) { testShot = FireDirectedShot(Direction.Down, p); }
return testShot;
}
private Point FireRandomShot()
{
Point p;
do
{
if (PatternShots.Count > 0)
PatternShots.Remove(p = PatternShots[rand.Next(PatternShots.Count)]);
else do
{
p = FireAroundPoint(HitShots.First());
if (InvalidShot(p)) HitShots.RemoveFirst();
} while (InvalidShot(p) & HitShots.Count > 0);
}
while (InvalidShot(p));
return p;
}
private Point FireTargettedShot()
{
Point p;
do
{
p = FireAroundPoint(new Point(EndPoints[1].Value.X, EndPoints[1].Value.Y));
if (InvalidShot(p) & EndPoints[1] != EndPoints[0])
EndPoints[1] = EndPoints[0];
else if (InvalidShot(p)) NullOutTarget();
} while (InvalidShot(p) & EndPoints[1] != null);
if (InvalidShot(p)) p = FireRandomShot();
return p;
}
private void ResetVars()
{
Shots.Clear();
HitShots.Clear();
PatternShots.Clear();
MissCount = 0;
}
public void NewGame(Size size, TimeSpan timeSpan)
{
gameSize = size;
ResetVars();
SetupPattern();
}
public void PlaceShips(ReadOnlyCollection<Ship> ships)
{
foreach (Ship s in ships)
s.Place(new Point(rand.Next(this.gameSize.Width), rand.Next(this.gameSize.Height)), (ShipOrientation)rand.Next(2));
}
public Point GetShot()
{
if (EndPoints[1] != null) Shots.AddLast(FireTargettedShot());
else Shots.AddLast(FireRandomShot());
return Shots.Last();
}
public void ShotHit(Point shot, bool sunk)
{
HitShots.AddLast(shot);
MissCount = 0;
EndPoints[1] = shot;
if (EndPoints[0] == null) EndPoints[0] = shot;
if (sunk) NullOutTarget();
}
public void ShotMiss(Point shot)
{
if (++MissCount == 6) NullOutTarget();
}
public void GameWon() { }
public void GameLost() { }
public void NewMatch(string opponent) { }
public void OpponentShot(Point shot) { }
public void MatchOver() { }
}
}
여기에 최소한의 공간을 차지하고 읽을 수 있도록 약간 압축되었습니다.
답변
경쟁 엔진에 대한 의견 :
새로운 게임 파라미터 :
IBattleshipOpponent :: NewGame이 사전 게임 설정 용이며 보드 크기를 사용하는 경우, 선박 및 해당 크기 목록도 가져와야합니다. 가변 선박 구성을 허용하지 않고 가변 보드 크기를 허용하는 것은 의미가 없습니다.
선박은 밀봉되어 있습니다 :
클래스 함선이 봉인 된 이유는 없습니다. 다른 기본 사항 중에서 Ships에 이름이 있기를 원하므로 ( “You sunk my {0}”, ship.Name); . 다른 확장 기능도 염두에두고 Ship은 상속 가능해야한다고 생각합니다.
시간 제한 :
토너먼트 규칙에는 1 초의 시간 제한이 적합하지만 완전히 디버깅에 혼란을줍니다. 전함 경쟁은 개발 / 디버깅을 돕기 위해 시간 위반을 무시할 수있는 쉬운 설정이 있어야합니다. 또한 얼마나 많은 시간이 사용되는지 더 정확하게 볼 수 있도록 System.Diagnostics.Process :: UserProcessorTime / Privileged ProcessorTime / TotalProcessorTime을 조사하는 것이 좋습니다.
침몰선 :
현재 API는 상대방의 함선을 침몰했을 때 알려줍니다.
ShotHit(Point shot, bool sunk);
그러나 당신이 침몰 한 배는 아닙니다 ! 나는 당신이 “전함 침몰했습니다!”라고 선언해야하는 인간-전함 규칙의 일부라고 생각합니다. (또는 구축함 또는 하위 등).
AI가 서로 맞대고있는 선박을 플러시하려고 할 때 특히 중요합니다. 다음과 같이 API 변경을 요청하고 싶습니다.
ShotHit(Point shot, Ship ship);
만약 배가 널이 아니라면 , 그 샷은 가라 앉는 샷 이었음을 의미하며, 당신은 당신이 어떤 선박을 침몰했는지, 그리고 얼마나 오래 걸 렸는지 알고 있습니다. 샷이 싱킹되지 않은 샷인 경우 ship은 null이며 추가 정보가 없습니다.