Jaka jest najlepsza sztuczna inteligencja pancernika?


315

Okręt wojenny!

W 2003 roku (kiedy miałem 17 lat) brałem udział w konkursie kodowania AI pancernika . Mimo że przegrałem ten turniej, dobrze się bawiłem i wiele się z niego nauczyłem.

Teraz chciałbym wskrzesić tę konkurencję w poszukiwaniu najlepszego pancernika AI.

Oto struktura, teraz hostowana na Bitbucket .

Zwycięzca otrzyma +450 reputacji! Konkurs odbędzie się 17 listopada 2009 roku . Żadne wpisy ani zmiany późniejsze niż godzina zero w dniu 17 nie będą akceptowane. (Centralny czas standardowy) Prześlij swoje wpisy wcześniej, aby nie przegapić okazji!

Aby zachować ten CEL , postępuj zgodnie z duchem konkurencji.

Zasady gry:

  1. Gra rozgrywana jest na siatce 10x10.
  2. Każdy zawodnik umieści każdy z 5 statków (o długości 2, 3, 3, 4, 5) na swojej planszy.
  3. Żadne statki nie mogą się pokrywać, ale mogą znajdować się w sąsiedztwie.
  4. Następnie zawodnicy wykonują kolejno strzały w kierunku przeciwnika.
    • Odmiana gry pozwala na oddanie wielu strzałów na salwę, po jednym na każdy ocalały statek.
  5. Przeciwnik powiadomi zawodnika, jeśli strzał opadnie, trafi lub nie trafi.
  6. Gra kończy się, gdy wszystkie statki jednego gracza zostaną zatopione.

Zasady konkursu:

  1. Celem zawodów jest znalezienie najlepszego algorytmu pancernika.
  2. Wszystko, co zostanie uznane za niezgodne z duchem konkurencji, będzie podstawą do dyskwalifikacji.
  3. Ingerowanie w przeciwnika jest sprzeczne z duchem konkurencji.
  4. Wielowątkowość może być używana z następującymi ograniczeniami:
    • Nie może być uruchomiony więcej niż jeden wątek, gdy nie jest twoja kolej. (Chociaż dowolna liczba wątków może być w stanie „zawieszonym”).
    • Żaden wątek nie może mieć priorytetu innego niż „Normalny”.
    • Biorąc pod uwagę powyższe dwa ograniczenia, podczas swojej tury będziesz mieć zagwarantowane co najmniej 3 dedykowane rdzenie procesora.
  5. Każdemu zawodnikowi w głównym wątku przydzielany jest limit 1 sekundy czasu procesora na grę.
  6. Upływ czasu powoduje utratę bieżącej gry.
  7. Jakikolwiek nieobsługiwany wyjątek spowoduje utratę bieżącej gry.
  8. Dostęp do sieci i dostęp do dysku jest dozwolony, ale ograniczenia czasowe mogą być dość wygórowane. Jednak dodano kilka metod konfiguracji i usuwania, aby złagodzić obciążenie czasowe.
  9. Kod powinien zostać wysłany w przypadku przepełnienia stosu jako odpowiedź lub, jeśli jest zbyt duży, połączony.
  10. Maksymalny całkowity rozmiar (nieskompresowany) pozycji wynosi 1 MB.
  11. Oficjalnie, .Net 2.0 / 3.5 jest jedynym wymaganiem ramowym.
  12. Wpis musi implementować interfejs IBattleshipOpponent.

Punktacja:

  1. Najlepsze 51 gier na 101 gier jest zwycięzcą meczu.
  2. Wszyscy zawodnicy będą grać w dopasowanym do siebie stylu round-robin.
  3. Najlepsza połowa zawodników rozegra turniej z podwójną eliminacją, aby wyłonić zwycięzcę. (Właściwie najmniejsza moc dwóch, która jest większa lub równa połowie).
  4. Będę używał frameworku TournamentApi do turnieju.
  5. Wyniki zostaną opublikowane tutaj.
  6. Jeśli prześlesz więcej niż jedno zgłoszenie, tylko twój najlepszy wynik uzyska kwalifikację do podwójnej eliminacji.

Powodzenia! Baw się dobrze!


EDYCJA 1:
Podziękowania dla Freeda , który znalazł błąd w Ship.IsValidfunkcji. Zostało to naprawione. Pobierz zaktualizowaną wersję frameworka.

EDYCJA 2:
Ponieważ istnieje duże zainteresowanie utrzymywaniem statystyk na dysku i takie, dodałem kilka nieokreślonych w czasie zdarzeń konfigurowania i usuwania, które powinny zapewnić wymaganą funkcjonalność. To zmiana na wpół przełomowa . Innymi słowy: interfejs został zmodyfikowany w celu dodania funkcji, ale nie jest dla nich wymagana treść. Pobierz zaktualizowaną wersję frameworka.

EDYCJA 3:
Poprawka błędu 1: GameWoni GameLostbyły wywoływane tylko w przypadku przekroczenia limitu czasu.
Poprawka 2: Jeśli silnik przekroczył limit czasu każdej gry, konkurencja nigdy się nie skończy.
Pobierz zaktualizowaną wersję frameworka.

EDYCJA 4:
Wyniki turnieju:


Jeśli pozycja wymaga dużej bazy danych, czy można się z nią połączyć przez sieć? To znaczy. czy wpis może nawiązywać połączenia internetowe?
Remus Rusanu

czy istnieje limit wielkości wpisów?
Jherico

8
@Steven: Ponadto skonsultowałem się z Jeffem Atwoodem, aby sprawdzić, czy jest to właściwe. Oto jego odpowiedź: twitter.com/codinghorror/status/5203185621
John Gietzen

1
Dodałbym również taht, ponieważ nieunikniony losowy składnik tych 50 gier nie wystarczy, aby dokładnie rozróżnić bardzo dobre implementacje. Sądzę, że 501 lub więcej może być konieczne do rozsądnego spojrzenia na to, co jest lepsze.
ShuggyCoUk

1
„Spokojny” przeciwnik, który odmawia umieszczenia statków, powoduje zawieszenie się konkurencji. Nie jestem pewien, jak bardzo zależy ci na ludziach, które robią takie głupie rzeczy. :)
Joe

Odpowiedzi:


56

Popieram ruch, aby zrobić o wiele więcej gier na mecz. Wykonanie 50 gier to po prostu rzut monetą. Musiałem zrobić 1000 gier, aby uzyskać rozsądne rozróżnienie między algorytmami testowymi.

Pobierz Dreadnought 1.2 .

Strategie:

  • śledzić wszystkie możliwe pozycje dla statków, które mają> 0 trafień. Lista nigdy nie jest większa niż ~ 30 000, więc można ją dokładnie przechowywać, w przeciwieństwie do listy wszystkich możliwych pozycji dla wszystkich statków (która jest bardzo duża).

  • Algorytm GetShot składa się z dwóch części, z których jedna generuje losowe strzały, a druga próbuje dokończyć zatopienie już trafionego statku. Wykonujemy losowe strzały, jeśli istnieje możliwa pozycja (z powyższej listy), w której zatopione są wszystkie trafione statki. W przeciwnym razie staramy się zakończyć zatapianie statku, wybierając miejsce, w którym strzelamy, w którym eliminuje się najwięcej możliwych pozycji (ważonych).

  • W przypadku losowych ujęć oblicz najlepszą lokalizację do strzelania na podstawie prawdopodobieństwa, że ​​jeden z niezatopionych statków pokryje się z daną lokalizacją.

  • algorytm adaptacyjny, który umieszcza statki w miejscach, w których przeciwnik statystycznie rzadziej strzela.

  • algorytm adaptacyjny, który woli strzelać w miejsca, w których przeciwnik statystycznie ma większe szanse na umieszczenie swoich statków.

  • statki w większości nie dotykają się.


na mojej maszynie testowej (netbook Celeron ULV) ten kod konsekwentnie traci limit czasu. Kiedy pozwalam, żeby zabierało to cały czas, chce, żeby biczował Prosto (około 90% wskaźnika sukcesu). Jeśli polegasz w dużej mierze na specyfikacji maszyny, na której będziesz biegał, aby osiągnąć ci limity czasowe, możesz chcieć dać sobie trochę
spokoju

Ciekawe ... Działa dobrze na automacie turniejowym. Jednak „idealny” silnik dostosowałby się do tego, ile czasu już spędził.
John Gietzen

35

Oto mój wpis! (Najbardziej naiwne możliwe rozwiązanie)

„Losowo 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() { }
    }
}

52
Właściwie ta odpowiedź jest miła, ponieważ pokazuje w bardzo zwięzłej formie API, które musisz wdrożyć, aby konkurować ... :)
dicroce

1
Kiedy budowałem podobny projekt na mojej uczelni w klasie Algorytmy, używałem losowej logiki z przeplotem przy podejmowaniu niektórych decyzji. Czasami było dobrze!
Nathan Taylor

2
To może próbować umieścić nakładające się statki, prawda?

6
Tak, ale silnik tego nie zezwoli. Następnie powie AI, aby umieściła je ponownie, ale tym razem mocniejszym głosem. (Widziany przez pop ax \ cmp ax, 1 \ je stern)
John Gietzen

5
Ważna uwaga dla każdego, kto, podobnie jak ja, pomyślał, że może łatwo to pokonać, pamiętając wcześniej umieszczone ujęcia i nie powtarzając. Framework zignoruje powtórzenia i da ci kolejną szansę, o ile twój całkowity czas jest mniejszy niż limit. Moim zdaniem jest to kiepskie, jeśli ktoś zepsuje swoje algo, powinien zostać ukarany ...
ShuggyCoUk

22

Oto przeciwnik, z którym ludzie mogą grać:

Zamiast stosować strategię inspirowaną stałą geometrią, pomyślałem, że byłoby interesujące oszacować podstawowe prawdopodobieństwo, że jakaś konkretna niezbadana przestrzeń posiada statek.

Aby zrobić to dobrze, należy zbadać wszystkie możliwe konfiguracje statków, które pasują do twojego obecnego poglądu na świat, a następnie obliczyć prawdopodobieństwa na podstawie tych konfiguracji. Możesz pomyśleć o tym jak o eksploracji drzewa:

rozszerzenie możliwych stanów pancernika http://natekohl.net/media/battleship-tree.png

Po rozważeniu wszystkich liści tego drzewa, które prześlizgują się przez to, co wiesz o świecie (np. Statki nie mogą się pokrywać, wszystkie trafione pola muszą być statkami itp.) , Możesz policzyć, jak często statki pojawiają się na każdej niezbadanej pozycji, aby oszacować prawdopodobieństwo, że statek tam siedzi.

Można to wizualizować jako mapę cieplną, na której bardziej prawdopodobne jest, że w hot spotach znajdą się statki:

mapa termiczna prawdopodobieństw dla każdej niezbadanej pozycji http://natekohl.net/media/battleship-probs.png

Jedną z rzeczy, które podoba mi się w tym konkursie pancerników, jest to, że drzewo powyżej jest prawie tak małe, że można użyć siły tego rodzaju algorytmu. Jeśli istnieje ~ 150 możliwych pozycji dla każdego z 5 statków, to 150 5 = 75 miliardów możliwości. Liczba ta maleje, zwłaszcza jeśli możesz wyeliminować całe statki.

Przeciwnik, z którym się połączyłem, nie eksploruje całego drzewa; 75 miliardów wciąż jest zbyt duże, aby dostać się w niecałą sekundę. Próbuje jednak oszacować te prawdopodobieństwa za pomocą kilku heurystyk.


Do tej pory pokonujesz nasze jedyne inne pełne rozwiązanie o około 67,7% do 32,3% :)
John Gietzen

2
Jestem zdecydowanie ciekawy, jak „podejście prawdopodobieństwa” wypada w porównaniu z „podejściem geometrycznym”. Zauważyłem, że ten przeciwnik prawdopodobieństwa faktycznie wykonuje ruchy zgodne z geometrycznymi wzorami omówionymi w innych odpowiedziach. Możliwe, że użycie geometrii jest równie dobre i znacznie szybsze. :)
Nate Kohl

12

Nie jest to pełna odpowiedź, ale wydaje się, że nie ma sensu zaśmiecać prawdziwych odpowiedzi wspólnym kodem. W ten sposób przedstawiam niektóre rozszerzenia / klasy ogólne w duchu open source. Jeśli ich używasz, proszę zmień przestrzeń nazw lub próba skompilowania wszystkiego w jedną bibliotekę DLL nie zadziała.

BoardView pozwala łatwo pracować z tablicą z adnotacjami.

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
    }
}

Niektóre rozszerzenia, niektóre z tych funkcji duplikują się w głównym frameworku, ale naprawdę powinieneś to zrobić.

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));
        }
    }
}

Coś, co często używam.

enum OpponentsBoardState
{
    Unknown = 0,
    Miss,
    MustBeEmpty,        
    Hit,
}

Randomizacja Bezpieczny, ale testowalny, przydatny do testowania.

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!");
        }
    }
}

10

Nie mam teraz czasu, aby napisać pełnoprawny algorytm, ale oto jedna myśl: jeśli twój przeciwnik losowo rozmieścił statki, czy prawdopodobieństwo umieszczenia nie byłoby prostym rozkładem ześrodkowanym na (5.5,5.5)? Na przykład, możliwości umieszczenia pancernika (o długości 5 jednostek) w wymiarze x są tutaj:

x    1 2 3 4 5  6  7 8 9 10
P(x) 2 4 6 8 10 10 8 6 4 2

Te same obliczenia byłyby ważne dla y. Inne statki nie miałyby tak dużej dystrybucji, ale twoje najlepsze przypuszczenia wciąż są w centrum. Następnie podejście matematyczne powoli promieniowałoby po przekątnej (być może o długości przeciętnego statku 17/5) poza centrum. Dawny:

...........
....x.x....
.....x.....
....x.x....
...........

Oczywiście do tego pomysłu trzeba dodać trochę przypadkowości, ale myślę, że to czysto matematyczne podejście.


Tak, rzeczywiście by to zrobili. Mój stary silnik to zrekompensował.
John Gietzen

1
Tam, skąd pochodzę, powoli promieniujące przekątne poza centrum uważa się za oszustwo .
bzlm

Jeśli uważa się to za oszustwo, istnieje dość łatwe przeciwdziałanie. Unikaj (x, y) gdzie x = y. :)
ine

5
Myślę, że miał na myśli liczenie kart? Co moim zdaniem nie jest oszustwem.
John Gietzen

10

Nic tak wyszukanego, jak tylko to, co wymyśliłem. Pokonuje losowego przeciwnika 99,9% czasu. Byłbym zainteresowany, gdyby ktoś miał jakieś inne małe wyzwania, takie jak ta, to dobra zabawa.

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() { }
    }
}

Lekko skondensowany, aby zajął tutaj minimalną przestrzeń i nadal był czytelny.


6

Kilka komentarzy na temat silnika konkursowego:

Parametry NewGame:

Jeśli IBattleshipOpponent :: NewGame jest przeznaczony do przygotowania przed meczem i przyjmuje rozmiar planszy, powinien również wziąć listę statków i ich odpowiednich rozmiarów. Nie ma sensu zezwalać na zmienną wielkość planszy bez uwzględnienia różnych konfiguracji statku.

Statki są zaplombowane:

Nie widzę powodu, dla którego statek klasy jest zaplombowany. Oprócz innych podstawowych rzeczy chciałbym, aby Statki miały Nazwę, dzięki czemu mogę wysyłać takie wiadomości, jak („Zatopiłeś moje {0}”, ship.Name); . Mam też na myśli inne rozszerzenia, więc uważam, że Ship powinien być dziedziczony.

Limity czasowe:

Chociaż ograniczenie czasowe do 1 sekundy ma sens w regule turniejowej, to całkowicie przeszkadza w debugowaniu. BattleshipCompetition powinno mieć łatwe ustawienie do ignorowania przekroczeń czasu, aby pomóc w rozwoju / debugowaniu. Sugerowałbym również zbadanie System.Diagnostics.Process :: UserProcessorTime / Privileged ProcessorTime / TotalProcessorTime, aby uzyskać dokładniejszy obraz tego, ile czasu jest używane.

Zatopione statki:

Aktualny interfejs API informuje cię, kiedy zatopiłeś statek przeciwnika:

ShotHit(Point shot, bool sunk);

ale nie który statek zatonąłeś! Uważam, że częścią zasad pancernika jest to, że musisz ogłosić „Zatopiłeś mój pancernik!” (lub niszczyciel lub sub, itp.).

Jest to szczególnie ważne, gdy AI próbuje wypłukać statki, które stykają się ze sobą. Chciałbym poprosić o zmianę interfejsu API na:

ShotHit(Point shot, Ship ship);

Jeśli statek ma wartość inną niż zero, oznacza to, że strzał był tonący, a ty wiesz, który statek zatonąłeś i jak długo to trwało. Jeśli strzał nie był tonący, statek jest zerowy i nie masz żadnych dalszych informacji.


Prześlij próbki kodu, jeśli uważasz, że czas można dokładniej ustalić. Nie chcę teraz zbytnio zmieniać zasad.
John Gietzen

Rozmiary statków są również przekazywane podczas PlaceShips (), która jest uruchamiana dokładnie raz na grę i może być również wykorzystana jako faza przygotowania. Prosimy o odpieczętowanie statku do własnych testów, ale planuję użyć zapieczętowanego do turnieju.
John Gietzen

BŁĄD: @John Gietzen: Ustaliłem, że PlaceShips NIE są uruchamiane dokładnie raz na grę (jak powiedziałeś). Jeśli gracz umieszcza swoje statki niepoprawnie (jak to często robi RandomOpponent), PlaceShips jest wywoływany wielokrotnie, bez ingerowania w NewGame.
abelenky

5
Zawsze uważałem za strategię umieszczenie dwóch statków w konfiguracji L, aby mój przeciwnik pomyślał, że zatonął pancernik, podczas gdy w rzeczywistości tak nie było. Nigdy nie miałem wrażenia, że ​​musiałeś zadeklarować, która łódź została zatopiona.
Josh Smeaton,

3
@DJ: Przestrzegam oryginalnych zasad pisanych kartkami. Pamiętaj, że Hasbro to firma zajmująca się zabawkami i że ta gra jest wcześniejsza niż Hasbro.
John Gietzen

5

Zaktualizowano CrossFire. Wiem, że nie może konkurować z Farnsworth lub Dreadnought, ale jest o wiele szybszy niż ten drugi i jest łatwy do grania na wypadek, gdyby ktoś chciał spróbować. Zależy to od obecnego stanu moich bibliotek, tutaj zawartych, aby ułatwić obsługę.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Drawing;
using System.IO;
using System.Collections.ObjectModel;

namespace Battleship.ShuggyCoUk
{
    public class Simple : IBattleshipOpponent
    {
        BoardView<OpponentsBoardState> opponentsBoard = new BoardView<OpponentsBoardState>(new Size(10,10));
        Rand rand = new Rand();
        int gridOddEven;
        Size size;

        public string Name { get { return "Simple"; } }

        public Version Version { get { return new Version(2, 1); }}

        public void NewMatch(string opponent) {}

        public void NewGame(System.Drawing.Size size, TimeSpan timeSpan)
        {
            this.size = size;
            this.opponentsBoard = new BoardView<OpponentsBoardState>(size);
            this.gridOddEven = rand.Pick(new[] { 0, 1 });
        }

        public void PlaceShips(System.Collections.ObjectModel.ReadOnlyCollection<Ship> ships)
        {
            BoardView<bool> board = new BoardView<bool>(size);
            var AllOrientations = new[] {
                ShipOrientation.Horizontal,
                ShipOrientation.Vertical };

            foreach (var ship in ships)
            {
                int avoidTouching = 3;
                while (!ship.IsPlaced)
                {
                    var l = rand.Pick(board.Select(c => c.Location));
                    var o = rand.Pick(AllOrientations);
                    if (ship.IsLegal(ships, size, l, o))
                    {
                        if (ship.IsTouching(ships, l, o)&& --avoidTouching > 0)
                            continue;
                        ship.Place(l, o);
                    }
                }
            }
        }
        protected virtual Point PickWhenNoTargets()
        {
            return rand.PickBias(x => x.Bias,
                opponentsBoard
                // nothing 1 in size
                .Where(c => (c.Location.X + c.Location.Y) % 2 == gridOddEven)
                .Where(c => c.Data == OpponentsBoardState.Unknown))
                .Location;
        }

        private int SumLine(Cell<OpponentsBoardState> c, int acc)
        {
            if (acc >= 0)
                return acc;
            if (c.Data == OpponentsBoardState.Hit)
                return acc - 1;
            return -acc;
        }

        public System.Drawing.Point GetShot()
        {
            var targets = opponentsBoard
                .Where(c => c.Data == OpponentsBoardState.Hit)
                .SelectMany(c => c.Neighbours())
                .Where(c => c.Data == OpponentsBoardState.Unknown)
                .ToList();
            if (targets.Count > 1)
            {
                var lines = targets.Where(
                    x => x.FoldAll(-1, SumLine).Select(r => Math.Abs(r) - 1).Max() > 1).ToList();
                if (lines.Count > 0)
                    targets = lines;
            }
            var target = targets.RandomOrDefault(rand);
            if (target == null)
                return PickWhenNoTargets();
            return target.Location;
        }

        public void OpponentShot(System.Drawing.Point shot)
        {
        }

        public void ShotHit(Point shot, bool sunk)
        {
            opponentsBoard[shot] = OpponentsBoardState.Hit;
            Debug(shot, sunk);
        }

        public void ShotMiss(Point shot)
        {
            opponentsBoard[shot] = OpponentsBoardState.Miss;
            Debug(shot, false);
        }

        public const bool DebugEnabled = false;

        public void Debug(Point shot, bool sunk)
        {
            if (!DebugEnabled)
                return;
            opponentsBoard.WriteAsGrid(
                Console.Out,
                x =>
                {
                    string t;
                    switch (x.Data)
                    {
                        case OpponentsBoardState.Unknown:
                            return " ";
                        case OpponentsBoardState.Miss:
                            t = "m";
                            break;
                        case OpponentsBoardState.MustBeEmpty:
                            t = "/";
                            break;
                        case OpponentsBoardState.Hit:
                            t = "x";
                            break;
                        default:
                            t = "?";
                            break;
                    }
                    if (x.Location == shot)
                        t = t.ToUpper();
                    return t;
                });
            if (sunk)
                Console.WriteLine("sunk!");
            Console.ReadLine();
        }

        public void GameWon()
        {
        }

        public void GameLost()
        {
        }

        public void MatchOver()
        {
        }

        #region Library code
        enum OpponentsBoardState
        {
            Unknown = 0,
            Miss,
            MustBeEmpty,
            Hit,
        }

        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
        }

        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!");
            }
        }
        #endregion
    }

    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, Battleship.ShuggyCoUk.Simple.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, Battleship.ShuggyCoUk.Simple.Rand rand)
        {
            int count = things.Count();
            if (count == 0)
                return default(T);
            return things.ElementAt(rand.Next(count));
        }
    }

}


5

Chodzi o to, co najlepsze, co mogłem zebrać w wolnym czasie, czyli o nieistnieniu. Trwają statystyki statystyk meczowych i meczowych, kiedy ustawiłem główną funkcję do zapętlania i ciągłego uruchamiania BattleshipCompetition, dopóki nie naciśniełem klawisza.

namespace Battleship
{
    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Linq;

    public class BP7 : IBattleshipOpponent
    {
        public string Name { get { return "BP7"; } }
        public Version Version { get { return this.version; } }

        Random rand = new Random();
        Version version = new Version(0, 7);
        Size gameSize;
        List<Point> scanShots;
        List<NextShot> nextShots;
        int wins, losses;
        int totalWins = 0;
        int totalLosses = 0;
        int maxWins = 0;
        int maxLosses = 0;
        int matchWins = 0;
        int matchLosses = 0;

        public enum Direction { VERTICAL = -1, UNKNOWN = 0, HORIZONTAL = 1 };
        Direction hitDirection, lastShotDirection;

        enum ShotResult { UNKNOWN, MISS, HIT };
        ShotResult[,] board;

        public struct NextShot
        {
            public Point point;
            public Direction direction;
            public NextShot(Point p, Direction d)
            {
                point = p;
                direction = d;
            }
        }

        public struct ScanShot
        {
            public Point point;
            public int openSpaces;
            public ScanShot(Point p, int o)
            {
                point = p;
                openSpaces = o;
            }
        }

        public void NewGame(Size size, TimeSpan timeSpan)
        {
            this.gameSize = size;
            scanShots = new List<Point>();
            nextShots = new List<NextShot>();
            fillScanShots();
            hitDirection = Direction.UNKNOWN;
            board = new ShotResult[size.Width, size.Height];
        }

        private void fillScanShots()
        {
            int x;
            for (x = 0; x < gameSize.Width - 1; x++)
            {
                scanShots.Add(new Point(x, x));
            }

            if (gameSize.Width == 10)
            {
                for (x = 0; x < 3; x++)
                {
                    scanShots.Add(new Point(9 - x, x));
                    scanShots.Add(new Point(x, 9 - x));
                }
            }
        }

        public void PlaceShips(System.Collections.ObjectModel.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()
        {
            Point shot;

            if (this.nextShots.Count > 0)
            {
                if (hitDirection != Direction.UNKNOWN)
                {
                    if (hitDirection == Direction.HORIZONTAL)
                    {
                        this.nextShots = this.nextShots.OrderByDescending(x => x.direction).ToList();
                    }
                    else
                    {
                        this.nextShots = this.nextShots.OrderBy(x => x.direction).ToList();
                    }
                }

                shot = this.nextShots.First().point;
                lastShotDirection = this.nextShots.First().direction;
                this.nextShots.RemoveAt(0);
                return shot;
            }

            List<ScanShot> scanShots = new List<ScanShot>();
            for (int x = 0; x < gameSize.Width; x++)
            {
                for (int y = 0; y < gameSize.Height; y++)
                {
                    if (board[x, y] == ShotResult.UNKNOWN)
                    {
                        scanShots.Add(new ScanShot(new Point(x, y), OpenSpaces(x, y)));
                    }
                }
            }
            scanShots = scanShots.OrderByDescending(x => x.openSpaces).ToList();
            int maxOpenSpaces = scanShots.FirstOrDefault().openSpaces;

            List<ScanShot> scanShots2 = new List<ScanShot>();
            scanShots2 = scanShots.Where(x => x.openSpaces == maxOpenSpaces).ToList();
            shot = scanShots2[rand.Next(scanShots2.Count())].point;

            return shot;
        }

        int OpenSpaces(int x, int y)
        {
            int ctr = 0;
            Point p;

            // spaces to the left
            p = new Point(x - 1, y);
            while (p.X >= 0 && board[p.X, p.Y] == ShotResult.UNKNOWN)
            {
                ctr++;
                p.X--;
            }

            // spaces to the right
            p = new Point(x + 1, y);
            while (p.X < gameSize.Width && board[p.X, p.Y] == ShotResult.UNKNOWN)
            {
                ctr++;
                p.X++;
            }

            // spaces to the top
            p = new Point(x, y - 1);
            while (p.Y >= 0 && board[p.X, p.Y] == ShotResult.UNKNOWN)
            {
                ctr++;
                p.Y--;
            }

            // spaces to the bottom
            p = new Point(x, y + 1);
            while (p.Y < gameSize.Height && board[p.X, p.Y] == ShotResult.UNKNOWN)
            {
                ctr++;
                p.Y++;
            }

            return ctr;
        }

        public void NewMatch(string opponenet)
        {
            wins = 0;
            losses = 0;
        }

        public void OpponentShot(Point shot) { }

        public void ShotHit(Point shot, bool sunk)
        {
            board[shot.X, shot.Y] = ShotResult.HIT;

            if (!sunk)
            {
                hitDirection = lastShotDirection;
                if (shot.X != 0)
                {
                    this.nextShots.Add(new NextShot(new Point(shot.X - 1, shot.Y), Direction.HORIZONTAL));
                }

                if (shot.Y != 0)
                {
                    this.nextShots.Add(new NextShot(new Point(shot.X, shot.Y - 1), Direction.VERTICAL));
                }

                if (shot.X != this.gameSize.Width - 1)
                {
                    this.nextShots.Add(new NextShot(new Point(shot.X + 1, shot.Y), Direction.HORIZONTAL));
                }

                if (shot.Y != this.gameSize.Height - 1)
                {
                    this.nextShots.Add(new NextShot(new Point(shot.X, shot.Y + 1), Direction.VERTICAL));
                }
            }
            else
            {
                hitDirection = Direction.UNKNOWN;
                this.nextShots.Clear();     // so now this works like gangbusters ?!?!?!?!?!?!?!?!?
            }
        }

        public void ShotMiss(Point shot)
        {
            board[shot.X, shot.Y] = ShotResult.MISS;
        }

        public void GameWon()
        {
            wins++;
        }

        public void GameLost()
        {
            losses++;
        }

        public void MatchOver()
        {
            if (wins > maxWins)
            {
                maxWins = wins;
            }

            if (losses > maxLosses)
            {
                maxLosses = losses;
            }

            totalWins += wins;
            totalLosses += losses;

            if (wins >= 51)
            {
                matchWins++;
            }
            else
            {
                matchLosses++;
            }
        }

        public void FinalStats()
        {
            Console.WriteLine("Games won: " + totalWins.ToString());
            Console.WriteLine("Games lost: " + totalLosses.ToString());
            Console.WriteLine("Game winning percentage: " + (totalWins * 1.0 / (totalWins + totalLosses)).ToString("P"));
            Console.WriteLine("Game losing percentage: " + (totalLosses * 1.0 / (totalWins + totalLosses)).ToString("P"));
            Console.WriteLine();
            Console.WriteLine("Matches won: " + matchWins.ToString());
            Console.WriteLine("Matches lost: " + matchLosses.ToString());
            Console.WriteLine("Match winning percentage: " + (matchWins * 1.0 / (matchWins + matchLosses)).ToString("P"));
            Console.WriteLine("Match losing percentage: " + (matchLosses * 1.0 / (matchWins + matchLosses)).ToString("P"));
            Console.WriteLine("Match games won high: " + maxWins.ToString());
            Console.WriteLine("Match games lost high: " + maxLosses.ToString());
            Console.WriteLine();
        }
    }
}

Ta logika jest najbliższa pokonaniu Dreadnought, wygrywając około 41% poszczególnych gier. (W rzeczywistości wygrał jeden mecz licząc od 52 do 49.) Co dziwne, ta klasa nie radzi sobie tak dobrze w porównaniu z FarnsworthOpponent jak wcześniejsza wersja, która była znacznie mniej zaawansowana.


5

Mój komputer jest obecnie naprawiany przez Dell, ale w tym miejscu byłem w zeszłym tygodniu:

namespace Battleship
{
    using System;
    using System.Collections.ObjectModel;
    using System.Drawing;
    using System.Collections.Generic;
    using System.Linq;

    public class BSKiller4 : OpponentExtended, IBattleshipOpponent
    {
        public string Name { get { return "BSKiller4"; } }
        public Version Version { get { return this.version; } }

        public bool showBoard = false;

        Random rand = new Random();
        Version version = new Version(0, 4);
        Size gameSize;

        List<Point> nextShots;
        Queue<Point> scanShots;

        char[,] board;

        private void printBoard()
        {
            Console.WriteLine();
            for (int y = 0; y < this.gameSize.Height; y++)
            {
                for (int x = 0; x < this.gameSize.Width; x++)
                {
                    Console.Write(this.board[x, y]);
                }
                Console.WriteLine();
            }
            Console.ReadKey();
        }

        public void NewGame(Size size, TimeSpan timeSpan)
        {
            this.gameSize = size;
            board = new char[size.Width, size.Height];
            this.nextShots = new List<Point>();
            this.scanShots = new Queue<Point>();
            fillScanShots();
            initializeBoard();
        }

        private void initializeBoard()
        {
            for (int y = 0; y < this.gameSize.Height; y++)
            {
                for (int x = 0; x < this.gameSize.Width; x++)
                {
                    this.board[x, y] = 'O';
                }
            }
        }

        private void fillScanShots()
        {
            int x, y;
            int num = gameSize.Width * gameSize.Height;
            for (int j = 0; j < 3; j++)
            {
                for (int i = j; i < num; i += 3)
                {
                    x = i % gameSize.Width;
                    y = i / gameSize.Height;
                    scanShots.Enqueue(new Point(x, y));
                }
            }
        }

        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 (showBoard) printBoard();
            Point shot;

            shot = findShotRun();
            if (shot.X != -1)
            {
                return shot;
            }

            if (this.nextShots.Count > 0)
            {
                shot = this.nextShots[0];
                this.nextShots.RemoveAt(0);
            }
            else
            {
                shot = this.scanShots.Dequeue();
            }

            return shot;
        }

        public void ShotHit(Point shot, bool sunk)
        {
            this.board[shot.X, shot.Y] = 'H';
            if (!sunk)
            {
                addToNextShots(new Point(shot.X - 1, shot.Y));
                addToNextShots(new Point(shot.X, shot.Y + 1));
                addToNextShots(new Point(shot.X + 1, shot.Y));
                addToNextShots(new Point(shot.X, shot.Y - 1));
            }
            else
            {
                this.nextShots.Clear();
            }
        }



        private Point findShotRun()
        {
            int run_forward_horizontal = 0;
            int run_backward_horizontal = 0;
            int run_forward_vertical = 0;
            int run_backward_vertical = 0;

            List<shotPossibilities> possible = new List<shotPossibilities>(5);

            // this only works if width = height for the board;
            for (int y = 0; y < this.gameSize.Height; y++)
            {
                for (int x = 0; x < this.gameSize.Width; x++)
                {
                    // forward horiz
                    if (this.board[x, y] == 'M')
                    {
                        run_forward_horizontal = 0;
                    }
                    else if (this.board[x, y] == 'O')
                    {
                        if (run_forward_horizontal >= 2)
                        {
                            possible.Add(
                                new shotPossibilities(
                                    run_forward_horizontal,
                                    new Point(x, y),
                                    true));
                        }
                        else
                        {
                            run_forward_horizontal = 0;
                        }
                    }
                    else
                    {
                        run_forward_horizontal++;
                    }

                    // forward vertical
                    if (this.board[y, x] == 'M')
                    {
                        run_forward_vertical = 0;
                    }
                    else if (this.board[y, x] == 'O')
                    {
                        if (run_forward_vertical >= 2)
                        {
                            possible.Add(
                                new shotPossibilities(
                                    run_forward_vertical,
                                    new Point(y, x),
                                    false));
                        }
                        else
                        {
                            run_forward_vertical = 0;
                        }
                    }
                    else
                    {
                        run_forward_vertical++;
                    }


                    // backward horiz
                    if (this.board[this.gameSize.Width - x - 1, y] == 'M')
                    {
                        run_backward_horizontal = 0;
                    }
                    else if (this.board[this.gameSize.Width - x - 1, y] == 'O')
                    {
                        if (run_backward_horizontal >= 2)
                        {
                            possible.Add(
                                new shotPossibilities(
                                    run_backward_horizontal,
                                    new Point(this.gameSize.Width - x - 1, y),
                                    true));
                        }
                        else
                        {
                            run_backward_horizontal = 0;
                        }
                    }
                    else
                    {
                        run_backward_horizontal++;
                    }


                    // backward vertical
                    if (this.board[y, this.gameSize.Height - x - 1] == 'M')
                    {
                        run_backward_vertical = 0;
                    }
                    else if (this.board[y, this.gameSize.Height - x - 1] == 'O')
                    {
                        if (run_backward_vertical >= 2)
                        {
                            possible.Add(
                                new shotPossibilities(
                                    run_backward_vertical,
                                    new Point(y, this.gameSize.Height - x - 1),
                                    false));
                        }
                        else
                        {
                            run_backward_vertical = 0;
                        }
                    }
                    else
                    {
                        run_backward_vertical++;
                    }

                }

                run_forward_horizontal = 0;
                run_backward_horizontal = 0;
                run_forward_vertical = 0;
                run_backward_vertical = 0;
            }
            Point shot;

            if (possible.Count > 0)
            {
                shotPossibilities shotp = possible.OrderByDescending(a => a.run).First();
                //this.nextShots.Clear();
                shot = shotp.shot;
                //if (shotp.isHorizontal)
                //{
                //    this.nextShots.RemoveAll(p => p.X != shot.X);
                //}
                //else
                //{
                //    this.nextShots.RemoveAll(p => p.Y != shot.Y);
                //}
            }
            else
            {
                shot = new Point(-1, -1);
            }

            return shot;
        }

        private void addToNextShots(Point p)
        {
            if (!this.nextShots.Contains(p) &&
                p.X >= 0 &&
                p.X < this.gameSize.Width &&
                p.Y >= 0 &&
                p.Y < this.gameSize.Height)
            {
                if (this.board[p.X, p.Y] == 'O')
                {
                    this.nextShots.Add(p);
                }
            }
        }

        public void GameWon()
        {
            this.GameWins++;
        }

        public void NewMatch(string opponent)
        {
            System.Threading.Thread.Sleep(5);
            this.rand = new Random(System.Environment.TickCount);
        }
        public void OpponentShot(Point shot) { }
        public void ShotMiss(Point shot)
        {
            this.board[shot.X, shot.Y] = 'M';
        }
        public void GameLost()
        {
            if (showBoard) Console.WriteLine("-----Game Over-----");
        }
        public void MatchOver() { }
    }


    public class OpponentExtended
    {
        public int GameWins { get; set; }
        public int MatchWins { get; set; }
        public OpponentExtended() { }
    }

    public class shotPossibilities
    {
        public shotPossibilities(int r, Point s, bool h)
        {
            this.run = r;
            this.shot = s;
            this.isHorizontal = h;
        }
        public int run { get; set; }
        public Point shot { get; set; }
        public bool isHorizontal { get; set; }
    }
}

2
Gratulacje na srebrze. Czy możesz opisać swój algorytm słowami? Warto byłoby wiedzieć.
Thomas Ahle

4

Jeśli brutalnie zmuszasz się do analizy, mechanika dostarczonego RandomOpponent może okazać się bardzo nieefektywna. Pozwala sobie na ponowne wybranie już wybranych lokalizacji i pozwala ramie zmusić ją do powtarzania, aż trafi w jedną, której jeszcze nie dotknął lub nie upłynie limit czasu na ruch.

Przeciwnik zachowuje się podobnie (efektywny rozkład miejsc jest taki sam), sam sprawdza poprawność psychiczną i zużywa tylko jedno generowanie liczb losowych na połączenie (amortyzowane).

To korzysta z klas w moich odpowiedziach na rozszerzenia / biblioteki i podam tylko kluczowe metody / stan.

Losowanie zostało usunięte z odpowiedzi Jona Skeeta tutaj

class WellBehavedRandomOpponent : IBattleShipOpponent
{
    Rand rand = new Rand();
    List<Point> guesses;
    int nextGuess = 0;

    public void PlaceShips(IEnumerable<Ship> ships)
    {
        BoardView<bool> board = new BoardView<bool>(BoardSize);
        var AllOrientations = new[] {
            ShipOrientation.Horizontal,
            ShipOrientation.Vertical };

        foreach (var ship in ships)
        {
            while (!ship.IsPlaced)
            {
                var l = rand.Pick(board.Select(c => c.Location));
                var o = rand.Pick(AllOrientations);
                if (ship.IsLegal(ships, BoardSize, l, o))
                    ship.Place(l, o);
            }
        }
    }

    public void NewGame(Size size, TimeSpan timeSpan)
    {
        var board = new BoardView<bool>(size);
        this.guesses = new List<Point>(
            board.Select(x => x.Location).Shuffle(rand));
        nextGuess = 0;
    }

    public System.Drawing.Point GetShot()
    {
        return guesses[nextGuess++];
    }

    // empty methods left out 
}

4

Nie będę mógł brać udziału, ale oto algorytm, który wdrożyłbym, gdybym miał czas:

Po pierwsze, kiedy wykryję trafienie, nie ścigam natychmiast reszty statku - buduję tabelę lokalizacji statków i zastanawiam się, czy trafiłem wszystkie pięć przynajmniej raz, zanim zacznę je całkowicie zatapiać. (Uwaga: jest to zła zasada dla wariantu wielokrotnego strzału - patrz komentarze)

  1. Uderz w środek (patrz ostatnia uwaga poniżej - „środek” to tylko wygoda opisu)
  2. Uderz w punkt 4 na prawo od centrum
  3. Uderz w punkt 1 w dół i jeden na prawo od centrum
  4. Traf w miejsce cztery po prawej stronie poprzedniego trafienia
  5. Kontynuuj według tego schematu (powinien skończyć się ukośnymi liniami oddzielonymi 3 spacjami wypełniającymi planszę). Powinno to trafić we wszystkie łodzie 4 i 5 długości oraz statystycznie dużą liczbę 3 i 2 łodzi.

  6. Zacznij losowo uderzać w miejsca między przekątnymi, aby złapać łodzie o długości 2 i 3, które nie zostały jeszcze zauważone.

Po wykryciu 5 trafień ustaliłbym, czy 5 trafień dotyczy oddzielnych łodzi. Jest to stosunkowo łatwe, wykonując kilka kolejnych strzałów w pobliżu miejsc, w których dwa trafienia znajdują się na tej samej linii poziomej lub pionowej i znajdują się w odległości 5 miejsc od siebie (mogą to być dwa trafienia na tej samej łodzi). Jeśli są to oddzielne łodzie, kontynuuj zatapianie wszystkich statków. Jeśli okaże się, że to ta sama łódź, kontynuuj wzorce napełniania powyżej, aż wszystkie 5 łodzi się znajdzie.

Ten algorytm jest prostym algorytmem wypełniania. Kluczowymi cechami są to, że nie marnuje czasu na zatapianie statków, o których wie, kiedy są jeszcze statki, których nie jest świadomy, i nie wykorzystuje nieefektywnego wzoru napełniania (tj. Całkowicie losowy wzór byłby marnotrawstwem).

Uwagi końcowe:

A) „Środek” to losowy punkt początkowy na planszy. Eliminuje to podstawową słabość tego algorytmu. B) Podczas gdy opis wskazuje rysowanie przekątnych bezpośrednio od początku, idealnie algorytm strzela tylko w „losowe” miejsca, które znajdują się wzdłuż tych przekątnych. Pomaga to zapobiec określeniu przez konkurenta czasu, przez który ich statki nie zostaną uderzone przewidywalnymi wzorami.

Opisuje to „idealny” algorytm polegający na tym, że wszystkie statki trafią pod (9x9) / 2 + 10 strzałów.

Można go jednak znacznie poprawić:

Po trafieniu statku określ jego rozmiar przed wykonaniem „wewnętrznych” linii ukośnych. Być może znalazłeś 2 statki, w którym to przypadku wewnętrzne przekątne można uprościć, aby szybciej znaleźć statki o rozmiarze 3.

Zidentyfikuj etapy gry i postępuj zgodnie z nimi. Ten algorytm może być dobry do pewnego momentu w grze, ale inne algorytmy mogą przynieść lepsze korzyści w ramach gry końcowej. Ponadto, jeśli drugi gracz jest bardzo bliski pokonania cię, inny algorytm może działać lepiej - na przykład algorytm wysokiego ryzyka może zawieść częściej, ale gdy działa, działa szybko i możesz pokonać przeciwnika, który jest bliżej wygranej niż ty .

Określ styl gry konkurenta - może dać ci wskazówki, w jaki sposób planują rozmieszczenie statków (tzn. Istnieje duża szansa, że ​​ich własny algorytm najszybciej zidentyfikuje sposób umieszczenia własnych statków - jeśli jedynym narzędziem, które masz, jest młotek, wszystko wygląda jak gwóźdź)

-Adam


Strategia czekania na zatopienie statków, aż wszystkie zostaną znalezione, zależy w dużej mierze od wariantu jednego strzału na turę. W wersji (liczba ocalałych statków) strzałów na turę korzystne jest zatapianie statków tak szybko, jak to możliwe, aby spowolnić przeciwnika.
Jason Owen,

4

Mój wpis

Nic nadzwyczajnego i nie miałem czasu na dodanie wszystkich dobrych pomysłów.

Ale wydaje się, że gra dość dobrze. Zobaczymy, jak to działa w konkurencji:

(umieść to w pliku Missouri.csi dodaj do projektu).

using System;
using System.Collections.ObjectModel;
using System.Drawing;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;

namespace Battleship
{
    // The Empire of Japan surrendered on the deck of the USS Missouri on Sept. 2, 1945
    public class USSMissouri : IBattleshipOpponent
    {
        public String  Name    { get { return name; } }
        public Version Version { get { return ver;  } }

#region IBattleship Interface
        // IBattleship::NewGame
        public void NewGame(Size gameSize, TimeSpan timeSpan)
        {
            size      = gameSize;
            shotBoard = new ShotBoard(size);
            attackVector = new Stack<Attack>();
        }

        // IBattleship::PlaceShips
        public void PlaceShips(ReadOnlyCollection<Ship> ships)
        {
            HunterBoard board;
            targetBoards = new List<HunterBoard>();
            shotBoard    = new ShotBoard(size);
            foreach (Ship s in ships)
            {
                board = new HunterBoard(this, size, s);
                targetBoards.Add(board);

                // REWRITE: to ensure valid board placement.
                s.Place(
                    new Point(
                        rand.Next(size.Width),
                        rand.Next(size.Height)),
                    (ShipOrientation)rand.Next(2));
            }
        }

        // IBattleship::GetShot
        public Point GetShot()
        {
            Point p = new Point();

            if (attackVector.Count() > 0)
            {
                p = ExtendShot();
                return p;
            }

            // Contemplate a shot at every-single point, and measure how effective it would be.
            Board potential = new Board(size);
            for(p.Y=0; p.Y<size.Height; ++p.Y)
            {
                for(p.X=0; p.X<size.Width; ++p.X)
                {
                    if (shotBoard.ShotAt(p))
                    {
                        potential[p] = 0;
                        continue;
                    }

                    foreach(HunterBoard b in targetBoards)
                    {
                        potential[p] += b.GetWeightAt(p);
                    }
                }
            }

            // Okay, we have the shot potential of the board.
            // Lets pick a weighted-random spot.
            Point shot;
            shot = potential.GetWeightedRandom(rand.NextDouble());

            shotBoard[shot] = Shot.Unresolved;

            return shot;
        }

        public Point ExtendShot()
        {
            // Lets consider North, South, East, and West of the current shot.
            // and measure the potential of each
            Attack attack = attackVector.Peek();

            Board potential = new Board(size);

            Point[] points = attack.GetNextTargets();
            foreach(Point p in points)
            {
                if (shotBoard.ShotAt(p))
                {
                    potential[p] = 0;
                    continue;
                }

                foreach(HunterBoard b in targetBoards)
                {
                    potential[p] += b.GetWeightAt(p);
                }
            }

            Point shot = potential.GetBestShot();
            shotBoard[shot] = Shot.Unresolved;
            return shot;
        }

        // IBattleship::NewMatch
        public void NewMatch(string opponent)
        {
        }
        public void OpponentShot(Point shot)
        {
        }
        public void ShotHit(Point shot, bool sunk)
        {
            shotBoard[shot] = Shot.Hit;

            if (!sunk)
            {
                if (attackVector.Count == 0) // This is a first hit, open an attackVector
                {   
                    attackVector.Push(new Attack(this, shot));
                }
                else
                {
                    attackVector.Peek().AddHit(shot);    // Add a hit to our current attack.
                }
            }

            // What if it is sunk?  Close the top attack, which we've been pursuing.
            if (sunk)
            {
                if (attackVector.Count > 0)
                {
                    attackVector.Pop();
                }
            }
        }
        public void ShotMiss(Point shot)
        {
            shotBoard[shot] = Shot.Miss;

            foreach(HunterBoard b in targetBoards)
            {
                b.ShotMiss(shot);  // Update the potential map.
            }
        }
        public void GameWon()
        {
            Trace.WriteLine  ("I won the game!");
        }
        public void GameLost()
        {
            Trace.WriteLine  ("I lost the game!");
        }
        public void MatchOver()
        {
            Trace.WriteLine("This match is over.");
        }

#endregion 

        public ShotBoard theShotBoard
        {
            get { return shotBoard; }
        }
        public Size theBoardSize
        {
            get { return size; }
        }

        private Random rand = new Random();
        private Version ver = new Version(6, 3); // USS Missouri is BB-63, hence version 6.3
        private String name = "USS Missouri (abelenky@alum.mit.edu)";
        private Size size;
        private List<HunterBoard> targetBoards;
        private ShotBoard shotBoard;
        private Stack<Attack> attackVector;
    }

    // An Attack is the data on the ship we are currently working on sinking.
    // It consists of a set of points, horizontal and vertical, from a central point.
    // And can be extended in any direction.
    public class Attack
    {
        public Attack(USSMissouri root, Point p)
        {
            Player = root;
            hit = p;
            horzExtent = new Extent(p.X, p.X);
            vertExtent = new Extent(p.Y, p.Y);
        }

        public Extent HorizontalExtent
        {
            get { return horzExtent; }
        }
        public Extent VerticalExtent
        {
            get { return vertExtent; }
        }
        public Point  FirstHit
        {
            get { return hit; }
        }

        public void AddHit(Point p)
        {
            if (hit.X == p.X) // New hit in the vertical direction
            {
                vertExtent.Min = Math.Min(vertExtent.Min, p.Y);
                vertExtent.Max = Math.Max(vertExtent.Max, p.Y);
            }
            else if (hit.Y == p.Y)
            {
                horzExtent.Min = Math.Min(horzExtent.Min, p.X);
                horzExtent.Max = Math.Max(horzExtent.Max, p.X);
            }
        }
        public Point[] GetNextTargets() 
        {
            List<Point> bors = new List<Point>();

            Point p;

            p = new Point(hit.X, vertExtent.Min-1);
            while (p.Y >= 0 && Player.theShotBoard[p] == Shot.Hit)
            {
                if (Player.theShotBoard[p] == Shot.Miss)
                {
                    break; // Don't add p to the List 'bors.  
                }
                --p.Y;
            }
            if (p.Y >= 0 && Player.theShotBoard[p] == Shot.None) // Add next-target only if there is no shot here yet.
            {
                bors.Add(p);
            }

            //-------------------

            p = new Point(hit.X, vertExtent.Max+1);
            while (p.Y < Player.theBoardSize.Height && Player.theShotBoard[p] == Shot.Hit)
            {
                if (Player.theShotBoard[p] == Shot.Miss)
                {
                    break; // Don't add p to the List 'bors.  
                }
                ++p.Y;
            }
            if (p.Y < Player.theBoardSize.Height && Player.theShotBoard[p] == Shot.None)
            {
                bors.Add(p);
            }

            //-------------------

            p = new Point(horzExtent.Min-1, hit.Y);
            while (p.X >= 0 && Player.theShotBoard[p] == Shot.Hit)
            {
                if (Player.theShotBoard[p] == Shot.Miss)
                {
                    break; // Don't add p to the List 'bors.  
                }
                --p.X;
            }
            if (p.X >= 0 && Player.theShotBoard[p] == Shot.None)
            {
                bors.Add(p);
            }

            //-------------------

            p = new Point(horzExtent.Max+1, hit.Y);
            while (p.X < Player.theBoardSize.Width && Player.theShotBoard[p] == Shot.Hit)
            {
                if (Player.theShotBoard[p] == Shot.Miss)
                {
                    break; // Don't add p to the List 'bors.  
                }
                ++p.X;
            }
            if (p.X < Player.theBoardSize.Width && Player.theShotBoard[p] == Shot.None)
            {
                bors.Add(p);
            }

            return bors.ToArray();
        }

        private Point hit; 
        private Extent horzExtent;
        private Extent vertExtent;
        private USSMissouri Player;
    }

    public struct Extent
    {
        public Extent(Int32 min, Int32 max)
        {
            Min = min;
            Max = max;
        }
        public Int32 Min;
        public Int32 Max;
    }

    public class Board  // The potential-Board, which measures the full potential of each square.
    {
        // A Board is the status of many things.
        public Board(Size boardsize)
        {
            size = boardsize;
            grid = new int[size.Width , size.Height];
            Array.Clear(grid,0,size.Width*size.Height);
        }

        public int this[int c,int r]
        {
            get { return grid[c,r];  }
            set { grid[c,r] = value; }
        }
        public int this[Point p]
        {
            get { return grid[p.X, p.Y];  }
            set { grid[p.X, p.Y] = value; }
        }

        public Point GetWeightedRandom(double r)
        {
            Int32 sum = 0;
            foreach(Int32 i in grid)
            {
                sum += i;
            }

            Int32 index = (Int32)(r*sum);

            Int32 x=0, y=0;
            for(y=0; y<size.Height; ++y)
            {
                for(x=0; x<size.Width; ++x)
                {
                    if (grid[x,y] == 0) continue; // Skip any zero-cells
                    index -= grid[x,y];
                    if (index < 0) break;
                }
                if (index < 0) break;
            }

            if (x == 10 || y == 10)
                throw new Exception("WTF");

            return new Point(x,y);
        }

        public Point GetBestShot()
        {
            int max=grid[0,0];
            for(int y=0; y<size.Height; ++y)
            {
                for (int x=0; x<size.Width; ++x)
                {
                    max = (grid[x,y] > max)? grid[x,y] : max;
                }
            }

            for(int y=0; y<size.Height; ++y)
            {
                for (int x=0; x<size.Width; ++x)
                {
                    if (grid[x,y] == max)
                    {
                        return new Point(x,y);
                    }
                }
            }
            return new Point(0,0);
        }

        public bool IsZero()
        {
            foreach(Int32 p in grid)
            {
                if (p > 0)
                {
                    return false;
                }
            }
            return true;
        }

        public override String ToString()
        {
            String output = "";
            String horzDiv = "   +----+----+----+----+----+----+----+----+----+----+\n";
            String disp;
            int x,y;

            output += "      A    B    C    D    E    F    G    H    I    J    \n" + horzDiv;

            for(y=0; y<size.Height; ++y)
            {
                output += String.Format("{0} ", y+1).PadLeft(3);
                for(x=0; x<size.Width; ++x)
                {
                    switch(grid[x,y])
                    {
                        case (int)Shot.None:       disp = "";  break;
                        case (int)Shot.Hit:        disp = "#"; break;
                        case (int)Shot.Miss:       disp = "."; break;
                        case (int)Shot.Unresolved: disp = "?"; break;
                        default:                   disp = "!"; break;
                    }

                    output += String.Format("| {0} ", disp.PadLeft(2));
                }
                output += "|\n" + horzDiv;
            }

            return output;
        }

        protected Int32[,] grid;
        protected Size     size;
    }

    public class HunterBoard
    {
        public HunterBoard(USSMissouri root, Size boardsize, Ship target)
        {
            size = boardsize;
            grid = new int[size.Width , size.Height];
            Array.Clear(grid,0,size.Width*size.Height);

            Player = root;
            Target = target;
            Initialize();
        }

        public void Initialize()
        {
            int x, y, i;

            for(y=0; y<size.Height; ++y)
            {
                for(x=0; x<size.Width - Target.Length+1; ++x)
                {
                    for(i=0; i<Target.Length; ++i)
                    {
                        grid[x+i,y]++;
                    }
                }
            }

            for(y=0; y<size.Height-Target.Length+1; ++y)
            {
                for(x=0; x<size.Width; ++x)
                {
                    for(i=0; i<Target.Length; ++i)
                    {
                        grid[x,y+i]++;
                    }
                }
            }
        }

        public int this[int c,int r]
        {
            get { return grid[c,r];  }
            set { grid[c,r] = value; }
        }
        public int this[Point p]
        {
            get { return grid[p.X, p.Y];  }
            set { grid[p.X, p.Y] = value; }
        }

        public void ShotMiss(Point p)
        {
            int x,y;
            int min, max;

            min = Math.Max(p.X-Target.Length+1, 0);
            max = Math.Min(p.X, size.Width-Target.Length);
            for(x=min; x<=max; ++x)
            {
                DecrementRow(p.Y, x, x+Target.Length-1);
            }

            min = Math.Max(p.Y-Target.Length+1, 0);
            max = Math.Min(p.Y, size.Height-Target.Length);
            for(y=min; y<=max; ++y)
            {
                DecrementColumn(p.X, y, y+Target.Length-1);
            } 

            grid[p.X, p.Y] = 0;
        }

        public void ShotHit(Point p)
        {
        }

        public override String ToString()
        {
            String output = String.Format("Target size is {0}\n", Target.Length);
            String horzDiv = "   +----+----+----+----+----+----+----+----+----+----+\n";
            int x,y;

            output += "      A    B    C    D    E    F    G    H    I    J    \n" + horzDiv;
            for(y=0; y<size.Height; ++y)
            {
                output += String.Format("{0} ", y+1).PadLeft(3);
                for(x=0; x<size.Width; ++x)
                {
                    output += String.Format("| {0} ", grid[x,y].ToString().PadLeft(2));
                }
                output += "|\n" + horzDiv;
            }
            return output;
        }

        // If we shoot at point P, how does that affect the potential of the board?
        public Int32 GetWeightAt(Point p)
        {
            int x,y;
            int potential = 0;
            int min, max;

            min = Math.Max(p.X-Target.Length+1, 0);
            max = Math.Min(p.X, size.Width-Target.Length);
            for(x=min; x<=max; ++x)
            {
                if (Player.theShotBoard.isMissInRow(p.Y, x, x+Target.Length-1) == false)
                {
                    ++potential;
                }
            }

            min = Math.Max(p.Y-Target.Length+1, 0);
            max = Math.Min(p.Y, size.Height-Target.Length);
            for(y=min; y<=max; ++y)
            {
                if (Player.theShotBoard.isMissInColumn(p.X, y, y+Target.Length-1) == false)
                {
                    ++potential;
                }
            } 

            return potential;
        }

        public void DecrementRow(int row, int rangeA, int rangeB)
        {
            int x;
            for(x=rangeA; x<=rangeB; ++x)
            {
                grid[x,row] = (grid[x,row]==0)? 0 : grid[x,row]-1;
            }
        }
        public void DecrementColumn(int col, int rangeA, int rangeB)
        {
            int y;
            for(y=rangeA; y<=rangeB; ++y)
            {
                grid[col,y] = (grid[col,y]==0)? 0 : grid[col,y]-1;
            }
        }

        private Ship Target = null;
        private USSMissouri Player;
        private Int32[,] grid;
        private Size     size;
    }

    public enum Shot
    {
        None = 0,
        Hit = 1,
        Miss = 2,
        Unresolved = 3
    };

    public class ShotBoard
    {
        public ShotBoard(Size boardsize)
        {
            size = boardsize;
            grid = new Shot[size.Width , size.Height];

            for(int y=0; y<size.Height; ++y)
            {
                for(int x=0; x<size.Width; ++x)
                {
                    grid[x,y] = Shot.None;
                }
            }
        }

        public Shot this[int c,int r]
        {
            get { return grid[c,r];  }
            set { grid[c,r] = value; }
        }
        public Shot this[Point p]
        {
            get { return grid[p.X, p.Y];  }
            set { grid[p.X, p.Y] = value; }
        }

        public override String ToString()
        {
            String output = "";
            String horzDiv = "   +----+----+----+----+----+----+----+----+----+----+\n";
            String disp;
            int x,y;

            output += "      A    B    C    D    E    F    G    H    I    J    \n" + horzDiv;

            for(y=0; y<size.Height; ++y)
            {
                output += String.Format("{0} ", y+1).PadLeft(3);
                for(x=0; x<size.Width; ++x)
                {
                    switch(grid[x,y])
                    {
                        case Shot.None:       disp = "";  break;
                        case Shot.Hit:        disp = "#"; break;
                        case Shot.Miss:       disp = "."; break;
                        case Shot.Unresolved: disp = "?"; break;
                        default:              disp = "!"; break;
                    }

                    output += String.Format("| {0} ", disp.PadLeft(2));
                }
                output += "|\n" + horzDiv;
            }
            return output;
        }

        // Functions to find shots on the board, at a specific point, or in a row or column, within a range
        public bool ShotAt(Point p)
        {
            return !(this[p]==Shot.None);
        }
        public bool isMissInColumn(int col, int rangeA, int rangeB)
        {
            for(int y=rangeA; y<=rangeB; ++y)
            {
                if (grid[col,y] == Shot.Miss)
                {
                    return true;
                }
            }
            return false;
        }
        public bool isMissInRow(int row, int rangeA, int rangeB)
        {
            for(int x=rangeA; x<=rangeB; ++x)
            {
                if (grid[x,row] == Shot.Miss)
                {
                    return true;
                }
            }
            return false;
        }
        protected Shot[,] grid;
        protected Size     size;
    }
}

A teraz, kiedy przesłałem swoje zgłoszenie, niektóre przybliżone statystyki: vs. BP7 wygrywa 44%. / vs. Dreadnought wygrywa 20%. / vs. Farnsworth 42% wygrywa. To był fajny projekt.
abelenky,

2

To nie jest minimax. Czy po umieszczeniu statków każdy gracz nie może grać sam, co spowodowało, że zajęło mu wiele tur, aby zatopić każdy statek przeciwnika? Ten, który zajął mniej tur, wygrywa.

Nie sądzę, że istnieją dobre ogólne strategie poza zatapianiem uderzonych statków i próbą zminimalizowania liczby strzałów, aby pokryć pozostałe możliwe miejsca, w których mogłyby się ukryć statki.

Oczywiście mogą istnieć kontr-strategie dla wszystkiego, co nie jest losowe. Ale nie sądzę, że istnieją strategie, które są dobre przeciwko wszystkim możliwym graczom.


1
Potencjalnie mogliby grać na własną rękę. Nie tak to będzie działać. Świetny pomysł. W tych zawodach chcę, aby możliwe było statystycznie unikanie strzałów przeciwnika.
John Gietzen

2
Widzę. Korzystając z danych z poprzednich gier przeciwko temu samemu przeciwnikowi można się do niego dostosować?
ziggystar

2

Myślę, że największym problemem związanym z układanką jest to, że składa się ona zasadniczo z dwóch ruchów. Jeden ruch polega na umieszczeniu twoich statków, a drugi na znalezieniu wrogich statków (bez względu na to, że druga część może być podzielona na segmenty, prócz wybicia zegara losowym czynnikiem, to po prostu „uruchom algorytm”). Nie ma mechanizmu określania, a następnie przeciwstawiania się strategii wroga, co sprawia, że ​​podobne konkursy oparte na kolejnych rundach „papierowych nożyczek” są dość interesujące.

Myślę też, że byłoby fajniej, gdybyś podał grę jako protokół sieciowy, a następnie zapewnił ramy do implementacji tego protokołu w języku C #, zamiast dyktować, że wszystkie rozwiązania powinny być w języku C #, ale to tylko moja opinia.

EDYCJA: Cofam swój punkt początkowy, ponieważ nie przeczytałem wystarczająco dokładnie zasad konkursu.


Nie wszystkie rozwiązania muszą być w języku C #. Mogę skompilować i połączyć osobny zestaw. Ponadto powinieneś być w stanie statystycznie przeciwstawić się swojemu przeciwnikowi.
John Gietzen

JOT#? może? Lol, jk. Mam do tego strukturę TCP, ale ten turniej musi przebiegać bardzo szybko.
John Gietzen

Dlaczego miałbyś zakładać, że komunikacja TCP między dwoma procesami na tym samym komputerze nie byłaby niesamowicie szybka?
Jherico

@Jherico: Gdybym korzystał z protokołu TCP, izolowałbym silniki na ich własnych komputerach, aby mogły korzystać z dowolnych zasobów procesora.
John Gietzen

Mimo to dwie maszyny na tym samym komputerze mogą z łatwością ukończyć grę w
niecałą

2

Zawsze lubiłem zaczynać w środku i ruszać spiralnie od tego jednego punktu, pozostawiając nie więcej niż 1 pustą przestrzeń między innymi punktami, aby uwzględnić ten cholerny okręt podwodny ... odległość między strzałami była zależna od tego, które statki zostały zatopione. jeśli statek B był ostatni, strzały musiały pozostawić tylko 4 pola pomiędzy nimi, aby zminimalizować straty


1
Więc ... Muszę tylko trzymać się z dala od środka? :)
darron

14
Musisz także trzymać się z dala od krawędzi, ponieważ trafienie na krawędź zawiera więcej informacji dla przeciwnika niż trafienie bez krawędzi. Powinieneś więc umieścić wszystkie swoje statki w regionie, który nie jest środkowy i pozbawiony krawędzi. Chyba że tego właśnie oczekują .
Jherico

1
Jeśli zaczniesz od pozostawienia 3 lub 4 pól, możesz mieć szczęście i tak trafić na okręt podwodny. Jeśli nie, wróć i spróbuj wypełnić luki. Więcej na: somethinkodd.com/oddthinking/2009/10/29/battleship-strategy
Dziwne

18
Statek z dwoma otworami nie jest cholernym okrętem podwodnym , to cholerna łódź PT . Sub ma trzy otwory. :)
kruk

2

Podobny konkurs prowadził dr James Heather z The University of Surrey w imieniu British Computer Society.

Ograniczono zasoby - mianowicie maksymalny czas procesora na turę, między ruchami nie można było zapisać żadnego stanu, narzucono maksymalny rozmiar sterty. Aby ograniczyć czas, AI może przesłać ruch w dowolnym punkcie przedziału czasowego i zostanie poproszony o ruch po zakończeniu tury.

Bardzo interesujące - zobacz więcej na: http://www.bcsstudentcontest.com/

Może dać ci więcej pomysłów.


2

W tej chwili rozwiązanie otwiera się i działa bez modyfikacji w monodevelop w Ubuntu 9.10 Linux


1

Napisałeś:

  • Wszystko, co zostanie uznane za niezgodne z duchem konkurencji, będzie podstawą do dyskwalifikacji.
  • Ingerowanie w przeciwnika jest sprzeczne z duchem konkurencji.

proszę zdefiniować „wbrew duchowi konkurencji” i „ingerowanie w przeciwnika”?

Również - dla uproszczenia, polecam:

  • nie zezwalaj w ogóle na używanie procesora podczas gniazda procesora przeciwnika.
  • nie zezwalaj na równoległość wątków i zamiast tego daj więcej CPU sekund w jednym wątku. Uprości to programowanie AI i nie zaszkodzi nikomu, kto jest związany z procesorem / pamięcią.

PS - czai się tutaj pytanie post-docs CS: czy ta gra nie jest do rozwiązania (tj. Czy istnieje jedna, najlepsza strategia?). tak, rozmiar planszy i liczba kroków sprawiają, że minimax i in. są obowiązkowe, ale wciąż muszę się zastanawiać ... daleko mu do skomplikowanej gry w szachy.


Miałem na myśli refleksję, kiedy powiedziałem „przeszkadzać”. Nie chcę, aby konkurenci wygrali, ponieważ zmiażdżyli kolejny silnik na śmierć.
John Gietzen

8
Sugeruję, że szpiegostwo jest ważną częścią współczesnej wojny, więc zastanawianie się nad znalezieniem celów byłoby idealne - w końcu była to jedna z metod stosowanych podczas drugiej wojny światowej ...
Rowland Shaw,

Mam platformę do izolowania silników na różnych komputerach, komunikowania się przez TCP / IP, co czyni odbicie bezwartościowym. Jednak ze względu na moją szacunkową liczbę zgłoszeń spowodowałoby to, że konkurs trwałby zbyt długo.
John Gietzen

6
Nie wiedziałem wtedy, że mieli Reflection!
Markus Nigbur

1

Przewiduję, że wygra osoba, której uda się odtworzyć losowy materiał siewny i wzorzec przeciwnika.

Nie jestem jednak pewien, jak prawdopodobne jest to.


Przeciwnicy mają opcję użycia CSPRNG.
John Gietzen

Dobra uwaga, chociaż przyznaję, że inżynieria odwrotna takiego materiału siewnego jest poza moim zakresem wiedzy. Wydaje mi się, że najbardziej wrażliwym aspektem byłby algorytm wyboru wzoru ognia, ale nawet wtedy niekoniecznie skorzystałbyś na zerwaniu go, ponieważ nie ma możliwości, abyś mógł poruszyć swoje statki po rozpoczęciu gry.
Triston Attridge

Kiedy ubiegałem się o staż naukowy, pisaliśmy programy pancerników i rywalizowaliśmy. Ustawiając losowe ziarno, właśnie wygrałem X)
P Shved

1
Przy założeniu dość prostego algorytmu rozmieszczania statków, wyobrażam sobie, że po otrzymaniu kilku trafień na różnych statkach można zacząć od zapętlenia większości tur przez wszystkie możliwe losowe nasiona (prawdopodobnie zaczynając gdzieś w pobliżu bieżącego czasu i poruszając się do przodu / o jeden krok wstecz) i sprawdzenie, które generują rozmieszczenie statków zgodne z zaobserwowanymi działaniami.
Domenic

1

Prawdopodobnie byłoby również możliwe uruchomienie ich serii z różnymi wariantami gry.

Dodanie rzeczy takich jak samolot 3D lub możliwość przemieszczania jednego statku zamiast strzelania na turę prawdopodobnie zmieniłyby nieco grę.


2
Istnieje odmiana „salwy”. Gdzie możesz oddać tyle strzałów na turę, ile pozostało statków.
John Gietzen

Ciekawa odmiana również. Wydaje mi się, że pamiętam także grę komputerową z samolotem. Losowo strzelał w miejsca na planszy przeciwnika.
Glenn,

inna odmiana: bądź wielkością planszy + liczby statków.
Russau

1

Całkowity czas jednej sekundy zależy od maszyny. Raz druga wartość operacji procesora na moim komputerze będzie inna niż na maszynie turniejowej. Jeśli zoptymalizuję algorytm „Okręt bojowy”, aby wykorzystać jak najwięcej czasu procesora w ciągu 1 sekundy, to będzie on działał na potencjalnie wolniejszej maszynie turniejowej, zawsze będzie przegrywać.

Nie jestem pewien, jak obejść to ograniczenie ram, ale należy się tym zająć.

...

Jednym z pomysłów jest zrobienie tego, co zostało zrobione w tym konkursie http://www.bcsstudentcontest.com /

I mają maksymalny czas na turę, a nie maksymalny całkowity czas gry. W ten sposób mogłem ograniczyć algorytmy do dopasowania w znanym czasie tury. Gra może trwać od 50 do 600+ tur, jeśli mój algorytm zarządza całkowitym czasem gry, może nie dać wystarczająco dużo czasu na wykonanie swojej najlepszej pracy lub może dać zbyt dużo czasu i stracić. Bardzo trudno jest zarządzać całkowitym czasem gry w algorytmie pancernika.

Sugerowałbym zmianę zasad, aby ograniczyć czas tury, a nie całkowity czas gry.

Edytować

Jeśli napisałem algorytm, który wylicza wszystkie możliwe strzały, a następnie szereguje je, to robi zdjęcie o najwyższym rankingu. Wygenerowanie wszystkich możliwych ujęć zajęłoby zbyt wiele czasu, więc pozwoliłem algorytmowi działać przez pewien czas, a następnie go zatrzymać.

Gdyby istniał limit turowy, mógłbym pozwolić algorytmowi działać przez 0,9 sekundy i zwrócić strzał o najwyższej randze, i być dobrze z limitem czasu tury.

Jeśli ograniczę się do całkowitego czasu gry wynoszącego jedną sekundę, trudno będzie określić, jak długo algorytm powinien działać w każdej turze. Będę chciał maksymalnie wydłużyć czas pracy procesora. Jeśli gra trwała 500 rund, mógłbym ograniczyć każdą turę do 0,002 sekundy, ale jeśli gra trwała 100 rund, mógłbym dać każdej rundzie 0,01 sekundy czasu procesora.

Byłoby niepraktyczne, gdyby algorytm używał częściowo wyczerpującego przeszukiwania przestrzeni strzału, aby znaleźć najlepszy strzał z obecnym ograniczeniem.

Całkowity 1-sekundowy czas gry ogranicza rodzaj algorytmów, które można skutecznie wykorzystać do konkurowania w grze.


Będzie to działało na czterordzeniowym procesorze Intel Q9550SX, 8 GB pamięci RAM, na maszynie Vista 64. Czy 1 sekunda będzie czynnikiem ograniczającym?
John Gietzen

Myślę, że powinieneś uczynić swój pancernik AI wielowątkowym, aby obliczyć maksymalną liczbę strzałów w tym przedziale czasu.
Jeff Atwood

sztuczka polega na tym, jak ograniczyć przedział czasu skrętu. Jeśli ograniczę go do 0,00005 sekundy, mogę bezpiecznie przekroczyć limit czasu, ale znacznie ograniczam przestrzeń wyszukiwania. Jeśli zwiększę limit czasu obrotu, przestrzeń wyszukiwania zostanie zwiększona, ale ryzykuję, że skończy się czas.
TonyAbell,

@TonyAbell: Jeśli ważne jest posiadanie limitu czasu na turę, dlaczego nie zacząć od wartości początkowej, a następnie dostosowywać ją z rundy na rundę? Po około połowie rund najprawdopodobniej znajdziesz optymalną długość tury dla przeciwnika.
Kyokley,

Powinieneś śledzić pozostały czas i ograniczyć go do 1/2 pozostałego dostępnego czasu.
John Gietzen,

1

Pracuję tutaj, nie wprowadzając rzeczywistego kodu - ale zaryzykuję kilka ogólnych spostrzeżeń:

  • Ponieważ wszystkie statki mają co najmniej 2 komórki, możesz użyć optymalizacji, którą widziałem podczas implementacji gry w Space Quest V - która strzela tylko do alternatywnych komórek w kształcie rombu, gdy „szuka” celu. To eliminuje połowę kwadratów, jednocześnie gwarantując, że w końcu znajdziesz wszystkie statki.
  • Losowy wzorzec strzelania podczas szukania celów przyniesie statystycznie najlepsze wyniki w wielu grach.

1

! [Gęstość prawdopodobieństwa] [1] wprowadź jej opis obrazu

! [wprowadź opis zdjęcia tutaj] [2]

Eksperymentowałem z porównaniem wyników strzelania randon z głupim polowaniem / celem i wreszcie wyszukanym wyszukiwaniem.

Wydaje się, że najlepszym rozwiązaniem jest utworzenie funkcji gęstości prawdopodobieństwa dla prawdopodobieństwa wykorzystania każdego kwadratu przez pozostałe statki i wycelowanie w kwadrat o najwyższej wartości.

Możesz zobaczyć moje wyniki tutaj, wprowadź opis linku tutaj


Czy mógłbyś naprawić swoją odpowiedź, a zwłaszcza swoje zdjęcia i link?
Bart

-2

„Pancernik” to tak zwany klasyczny problem informatyki NP.

http://en.wikipedia.org/wiki/List_of_NP-complete_problems

(szukaj pancernika - jest tam, pod grami i łamigłówkami)


4
Który jest łamigłówką pancernika ( en.wikipedia.org/wiki/Battleship_(puzzle) ), a nie Battleship the game ( en.wikipedia.org/wiki/Battleship_(game) ).
Jason Berkan

Tak, jak stwierdził Jason, jest to zupełnie inne zwierzę.
John Gietzen

3
Hehehe Kolejne zadanie, które dostaję do pracy, powiem, że jest kompletne, a potem zjem długi lunch. :-)
Bork Blatt
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.