Qual è la migliore AI da battaglia?


315

Corazzata!

Nel 2003 (quando avevo 17 anni), ho partecipato a una competizione di codifica AI di Battleship . Anche se ho perso quel torneo, mi sono divertito molto e ho imparato molto da esso.

Ora, vorrei resuscitare questa competizione, alla ricerca della migliore corazzata AI.

Ecco il framework, ora ospitato su Bitbucket .

Al vincitore verrà assegnata +450 reputazione! Il concorso si svolgerà a partire dal 17 novembre 2009 . Non saranno accettate iscrizioni o modifiche successive all'ora zero il 17. (Ora solare centrale) Invia le tue iscrizioni in anticipo, in modo da non perdere l'occasione!

Per mantenere questo OBIETTIVO , si prega di seguire lo spirito della competizione.

Le regole del gioco:

  1. Il gioco si gioca su una griglia 10x10.
  2. Ogni concorrente posizionerà ciascuna delle 5 navi (delle lunghezze 2, 3, 3, 4, 5) sulla propria griglia.
  3. Nessuna nave può sovrapporsi, ma può essere adiacente.
  4. I concorrenti quindi a turno sparano singoli colpi al loro avversario.
    • Una variante del gioco consente di sparare più colpi per scarica, uno per ogni nave sopravvissuta.
  5. L'avversario avviserà il concorrente se il tiro affonda, colpisce o manca.
  6. Il gioco termina quando tutte le navi di un singolo giocatore vengono affondate.

Regole del concorso:

  1. Lo spirito della competizione è trovare il miglior algoritmo di Battleship.
  2. Tutto ciò che viene ritenuto contrario allo spirito della competizione sarà motivo di squalifica.
  3. Interferire con un avversario è contro lo spirito della competizione.
  4. Il multithreading può essere utilizzato in base alle seguenti restrizioni:
    • Non può essere in esecuzione più di un thread mentre non è il tuo turno. (Tuttavia, qualsiasi numero di thread può essere in uno stato "sospeso").
    • Nessun thread può essere eseguito con una priorità diversa da "Normale".
    • Date le due restrizioni di cui sopra, durante il tuo turno ti verranno garantiti almeno 3 core CPU dedicati.
  5. Un limite di 1 secondo di tempo CPU per partita è assegnato a ciascun concorrente sul thread principale.
  6. Se si esaurisce il tempo, si perde la partita in corso.
  7. Qualsiasi eccezione non gestita comporterà la perdita del gioco corrente.
  8. L'accesso alla rete e l'accesso al disco sono consentiti, ma è possibile che le restrizioni temporali siano piuttosto proibitive. Tuttavia, sono stati aggiunti alcuni metodi di impostazione e demolizione per alleviare la fatica del tempo.
  9. Il codice deve essere pubblicato sullo stack di overflow come risposta o, se troppo grande, collegato.
  10. La dimensione totale massima (non compressa) di una voce è 1 MB.
  11. Ufficialmente, .Net 2.0 / 3.5 è l'unico requisito quadro.
  12. La tua voce deve implementare l'interfaccia IBattleshipOpponent.

punteggio:

  1. I migliori 51 giochi su 101 giochi sono i vincitori di una partita.
  2. Tutti i concorrenti giocheranno abbinati l'uno contro l'altro, in stile round-robin.
  3. La metà migliore dei concorrenti giocherà quindi un torneo a doppia eliminazione per determinare il vincitore. (Il più piccolo potere di due che è in realtà maggiore o uguale alla metà).
  4. Userò il framework TournamentApi per il torneo.
  5. I risultati saranno pubblicati qui.
  6. Se invii più di una voce, solo la voce con il punteggio migliore è idonea per il doppio elim.

In bocca al lupo! Divertiti!


EDIT 1:
Grazie a Freed , che ha riscontrato un errore nella Ship.IsValidfunzione. È stato corretto. Si prega di scaricare la versione aggiornata del framework.

EDIT 2:
Dal momento che c'è stato un interesse significativo nel persistere delle statistiche su disco e simili, ho aggiunto alcuni eventi di installazione e smontaggio non temporizzati che dovrebbero fornire la funzionalità richiesta. Questo è un cambiamento semi-rottura . Vale a dire: l'interfaccia è stata modificata per aggiungere funzioni, ma per loro non è richiesto alcun corpo. Si prega di scaricare la versione aggiornata del framework.

EDIT 3:
Bug Fix 1: GameWone GameLostvenivano chiamati solo in caso di timeout.
Bug Fix 2: se un motore stava scadendo ogni partita, la competizione non sarebbe mai finita.
Si prega di scaricare la versione aggiornata del framework.

EDIT 4:
Risultati del torneo:


Se la voce richiede un database di grandi dimensioni, può connettersi ad esso su rete? Vale a dire. la voce può effettuare chiamate al servizio web?
Remus Rusanu,

c'è un limite di dimensione per le voci?
Jherico,

8
@Steven: Inoltre, ho consultato Jeff Atwood solo per vedere se era appropriato. Ecco la sua risposta: twitter.com/codinghorror/status/5203185621
John Gietzen,

1
Inoltre aggiungerei questo, dato che l'inevitabile componente casuale di questi 50 giochi non sarà sufficiente per distinguere accuratamente tra ottime implementazioni. Penso che 501 o più potrebbero essere necessari per una visione ragionevole su quale sia meglio.
ShuggyCoUk,

1
Un avversario "pacifico" che rifiuta di piazzare navi fa impiccare la competizione. Non sono sicuro di quanto ti interessi delle persone che fanno cose stupide come quelle. :)
Joe

Risposte:


56

Secondo la mozione di fare molti più giochi per partita. Fare 50 partite significa semplicemente lanciare una moneta. Avevo bisogno di fare 1000 giochi per ottenere una ragionevole distinzione tra algoritmi di test.

Scarica Dreadnought 1.2 .

strategie:

  • tenere traccia di tutte le posizioni possibili per le navi che hanno> 0 colpi. L'elenco non diventa mai più grande di ~ 30K, quindi può essere mantenuto esattamente, a differenza dell'elenco di tutte le posizioni possibili per tutte le navi (che è molto grande).

  • L'algoritmo GetShot ha due parti, una che genera colpi casuali e l'altra che tenta di finire di affondare una nave già colpita. Effettuiamo colpi casuali se esiste una posizione possibile (dall'elenco sopra) in cui tutte le navi colpite vengono affondate. Altrimenti, proviamo a finire di affondare una nave selezionando una posizione per sparare in cui elimina le posizioni più possibili (ponderate).

  • Per scatti casuali, calcola la posizione migliore per sparare in base alla probabilità che una delle navi non occupate si sovrapponga alla posizione.

  • algoritmo adattivo che posiziona le navi in ​​luoghi in cui l'avversario ha statisticamente meno probabilità di tirare.

  • algoritmo adattivo che preferisce sparare in punti in cui l'avversario è statisticamente più propenso a posizionare le sue navi.

  • posizionare le navi per lo più senza toccarsi.


sulla mia macchina di prova (un netbook Celeron ULV) questo codice perde costantemente per timeout. Quando lascio che impieghi tutto il tempo che vuole, frusta Simple (circa il 90% di percentuale di successo). Se fai molto affidamento sulle specifiche della macchina su cui corri per colpire i tuoi timelimits potresti darti un po 'di spazio di
manovra

Interessante ... Funziona bene sulla macchina del torneo. Tuttavia, un motore "perfetto" si adatterà al tempo che ha già trascorso.
John Gietzen,

35

Ecco la mia voce! (La soluzione più ingenua possibile)

"Casuale 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
In realtà, questa risposta è buona perché mostra in una forma molto concisa le API che dovresti implementare per competere ... :)
dicroce,

1
Quando ho costruito un progetto simile nella mia classe Algorithms del college, ho usato la logica casuale intrecciata con alcune decisioni. A volte andava bene!
Nathan Taylor,

2
Questo potrebbe tentare di posizionare navi sovrapposte, vero?

6
Sì, ma il motore non lo consentirà. Quindi dirà all'IA di posizionarli di nuovo, ma questa volta, con una voce più severa. (Visto da pop ax \ cmp ax, 1 \ je stern)
John Gietzen,

5
Nota importante per tutti coloro che, come me, hanno pensato di poter facilmente battere questo ricordando i colpi precedentemente posizionati e non ripetendoli. Il framework ignorerà le ripetizioni e ti darà un'altra possibilità a condizione che il tempo totale sia inferiore al limite. Questo è scarso secondo me, se qualcuno incasina il loro algo dovrebbero essere penalizzati ...
ShuggyCoUk

22

Ecco un avversario per cui le persone devono giocare contro:

Invece di usare una strategia ispirata alla geometria fissa, ho pensato che sarebbe stato interessante tentare di stimare le probabilità sottostanti che un determinato spazio inesplorato detenga una nave.

Per fare ciò, esploreresti tutte le possibili configurazioni di navi che si adattano alla tua attuale visione del mondo, e quindi calcolerai le probabilità sulla base di tali configurazioni. Potresti pensarlo come esplorare un albero:

un'espansione di possibili stati di corazzata http://natekohl.net/media/battleship-tree.png

Dopo aver considerato tutte le foglie di quell'albero che coincidono con ciò che sai del mondo (ad esempio le navi non possono sovrapporsi, tutti i quadrati colpiti devono essere navi, ecc.) Puoi contare la frequenza con cui si verificano le navi in ​​ogni posizione inesplorata per stimare la probabilità che una nave è seduta lì.

Questo può essere visualizzato come una mappa di calore, dove è più probabile che i punti caldi contengano navi:

una mappa di calore delle probabilità per ogni posizione inesplorata http://natekohl.net/media/battleship-probs.png

Una cosa che mi piace di questa competizione di Battleship è che l'albero sopra è quasi abbastanza piccolo da forzare questo tipo di algoritmo. Se ci sono ~ 150 posizioni possibili per ciascuna delle 5 navi, sono 150 5 = 75 miliardi di possibilità. E quel numero si riduce solo, specialmente se puoi eliminare intere navi.

L'avversario a cui ho collegato sopra non esplora l'intero albero; 75 miliardi sono ancora troppo grandi per entrare in meno di un secondo. Tenta di stimare queste probabilità, tuttavia, con l'aiuto di alcune euristiche.


Finora, stai battendo la nostra unica altra soluzione completa da circa il 67,7% al 32,3% :)
John Gietzen

2
Sono decisamente curioso di vedere come un "approccio probabilistico" sia paragonabile a un "approccio geometrico". Ho notato che questo avversario di probabilità in realtà fa mosse che seguono gli schemi geometrici discussi in altre risposte. Potrebbe essere che usare la geometria sia altrettanto buono e molto più veloce. :)
Nate Kohl,

12

Non è una risposta a tutti gli effetti, ma sembra poco utile ingombrare le risposte reali con il codice che è comune. Vi presento quindi alcune estensioni / classi generali nello spirito dell'open source. Se li usi, ti preghiamo di cambiare lo spazio dei nomi o provare a compilare tutto in una dll non funzionerà.

BoardView ti consente di lavorare facilmente con una lavagna annotata.

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

Alcune estensioni, alcune di queste duplicano la funzionalità nel framework principale, ma dovrebbero davvero essere eseguite da te.

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

Qualcosa che finisco per usare molto.

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

Randomizzazione. Sicuro ma testabile, utile per i test.

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

In questo momento non ho il tempo di scrivere un vero algoritmo, ma ecco un pensiero: se il tuo avversario posizionasse le navi in ​​modo casuale, le probabilità di posizionamento non sarebbero una semplice distribuzione centrata su (5.5.5.5)? Ad esempio, le possibilità di posizionamento per la corazzata (5 unità lunghe) nella dimensione x sono qui:

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

Gli stessi calcoli sarebbero validi per y. Le altre navi non sarebbero così ripide di distribuzioni, ma la tua ipotesi migliore è ancora il centro. Successivamente, l'approccio matematico si irradierebbe lentamente dal centro diagonali (forse con la lunghezza della nave media, 17/5). Ex:

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

Ovviamente un po 'di casualità dovrebbe essere aggiunta all'idea, ma penso che puramente matematicamente sia la strada da percorrere.


Sì, davvero lo farebbero. Il mio vecchio motore ha compensato quello.
John Gietzen,

1
Da dove vengo, irradiare lentamente le diagonali dal centro è considerato un imbroglio .
bzlm,

Se è considerato un imbroglio, c'è una contromisura piuttosto semplice. Evita (x, y) dove x = y. :)
ine

5
Penso che alludesse al conteggio delle carte? Il che, secondo me, non è barare.
John Gietzen,

10

Niente di così sofisticato, ma ecco cosa mi è venuto in mente. Batte l'avversario casuale il 99,9% delle volte. Sarebbe interessato se qualcuno avesse altre piccole sfide come questa, è stato molto divertente.

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

Leggermente condensato per occupare uno spazio minimo qui ed essere ancora leggibile.


6

Alcuni commenti sul motore della competizione:

Parametri NewGame:

Se IBattleshipOpponent :: NewGame è progettato per la configurazione pre-partita e prende una dimensione board, dovrebbe anche prendere un elenco di navi e le loro rispettive dimensioni. Non ha senso consentire dimensioni della scheda variabili senza consentire configurazioni variabili della nave.

Le navi sono sigillate:

Non vedo alcun motivo per cui la classe Ship sia sigillata. Tra le altre cose di base, vorrei che le navi avessero un nome, in modo da poter emettere messaggi come ("Hai affondato il mio {0}", ship.Name);. Ho in mente anche altre estensioni, quindi penso che Ship dovrebbe essere ereditabile.

Limiti di tempo:

Mentre il limite di tempo di 1 secondo ha senso per una regola del torneo, fa totalmente casino con il debugging. BattleshipCompetition dovrebbe avere un'impostazione semplice per ignorare le violazioni del tempo per facilitare lo sviluppo / il debug. Vorrei anche suggerire di studiare System.Diagnostics.Process :: UserProcessorTime / Privileged ProcessorTime / TotalProcessorTime per una visione più accurata di quanto tempo viene utilizzato.

Navi affondate:

L'API corrente ti informa quando hai affondato la nave di un avversario:

ShotHit(Point shot, bool sunk);

ma non quale nave hai affondato! Lo considero parte delle regole della nave da guerra umana che devi dichiarare "Hai affondato la mia nave da guerra!" (o distruttore, o sub, ecc.).

Ciò è particolarmente critico quando un'intelligenza artificiale sta cercando di stanare le navi che si scontrano l'una contro l'altra. Vorrei richiedere una modifica dell'API per:

ShotHit(Point shot, Ship ship);

Se la nave non è nulla, implica che il colpo era un colpo che affondava e tu sai quale nave hai affondato e quanto tempo è stato. Se il colpo era un colpo non affondante, allora la nave è nulla e non hai ulteriori informazioni.


Si prega di inviare esempi di codice se si ritiene che i tempi possano essere eseguiti in modo più accurato. Non voglio cambiare troppo le regole adesso.
John Gietzen,

Inoltre, le dimensioni della nave vengono trasmesse durante PlaceShips () che viene eseguito esattamente una volta per partita e può essere utilizzato anche come fase di configurazione. Non esitare a disfare la nave per i tuoi test, ma ho intenzione di utilizzare quella sigillata per il torneo.
John Gietzen,

ERRORE: @John Gietzen: ho stabilito che PlaceShips NON viene eseguito esattamente una volta per partita (come hai affermato). Se un giocatore posiziona le proprie navi in ​​modo errato (come spesso accade con RandomOpponent), allora PlaceShips viene chiamato ripetutamente, senza intervenire su NewGame.
abelenky,

5
Ho sempre considerato una strategia quella di posizionare due navi in ​​una configurazione a L per far credere al mio avversario di aver affondato una nave da guerra, mentre in realtà non lo aveva fatto. Non ho mai avuto l'impressione che tu debba dichiarare quale barca è stata affondata.
Josh Smeaton,

3
@DJ: sto seguendo le regole originali di carta e penna. Ricorda che Hasbro è una società di giocattoli e che questo gioco precede Hasbro.
John Gietzen,

5

CrossFire aggiornato. So che non può competere con Farnsworth o Dreadnought, ma è molto più veloce di quest'ultimo e semplice da giocare nel caso in cui qualcuno voglia provare. Questo si basa sullo stato attuale delle mie librerie, incluso qui per facilitarne l'utilizzo.

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

Questo è il meglio che potrei mettere insieme nel mio tempo libero, che è inesistente. Ci sono alcune statistiche di gioco e match in corso, mentre imposto la funzione principale per eseguire il loop ed eseguire continuamente la BattleshipCompetition fino a quando non premo un tasto.

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

Questa logica è la più vicina che ho dovuto sconfiggere Dreadnought, vincendo circa il 41% dei singoli giochi. (In realtà ha vinto una partita con un numero da 52 a 49.) Stranamente, questa classe non fa altrettanto contro FarnsworthOpponent come una versione precedente che era molto meno avanzata.


5

Il mio computer è stato riparato da Dell in questo momento, ma questo è dove ero la scorsa settimana:

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
Complimenti per l'argento. Ti dispiace descrivere il tuo algoritmo a parole? Sarebbe interessante saperlo.
Thomas Ahle,

4

Se stai forzando brutalmente la tua analisi, potresti trovare altamente inefficiente la meccanica del RandomOpponent in dotazione. Si consente di selezionare nuovamente posizioni già targetizzate e consente al framework di costringerlo a ripetere fino a quando non colpisce uno che non ha ancora toccato o il tempo limite per mossa scade.

Questo avversario ha un comportamento simile (la distribuzione effettiva del posizionamento è la stessa) fa solo il controllo della sanità mentale e consuma solo una generazione di numeri casuali per chiamata (ammortizzata)).

Questo utilizza le classi nella risposta delle mie estensioni / librerie e fornisco solo i metodi / lo stato chiave.

Shuffle viene sollevato dalla risposta di Jon Skeet qui

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

Non potrò partecipare, ma ecco l'algoritmo che implementerei se avessi tempo:

In primo luogo, quando rilevo un colpo non inseguo immediatamente il resto della nave - costruisco un tavolo di posizioni delle navi e capisco se ho colpito tutti e cinque almeno una volta prima di iniziare ad affondarli completamente. (Si noti che questa è una cattiva politica per la variante a colpo multiplo - vedere i commenti)

  1. Colpisci il centro (vedi la nota finale di seguito - "center" è solo una comodità per la descrizione)
  2. Colpisci il punto 4 a destra del centro
  3. Colpisci il punto 1 in basso e uno a destra del centro
  4. Colpisci il punto quattro a destra del colpo precedente
  5. Continua in quel modello (dovrebbe finire con linee diagonali separate da 3 spazi che riempiono il tabellone). Questo dovrebbe colpire tutte le barche di lunghezza 4 e 5 e un numero statisticamente elevato di 3 e 2 barche.

  6. Inizia a colpire casualmente i punti tra le diagonali, questo prenderà le barche di lunghezza 2 e 3 che non sono già state notate.

Una volta rilevati 5 colpi, determinerei se i 5 colpi sono su barche separate. Ciò è relativamente semplice effettuando alcuni scatti in più vicino a posizioni in cui due colpi si trovano sulla stessa linea orizzontale o verticale e si trovano entro 5 posizioni l'uno dall'altro (potrebbero essere due colpi sulla stessa barca). Se sono barche separate, continua ad affondare tutte le navi. Se si scopre che sono la stessa barca, continua i modelli di riempimento sopra fino a quando tutte e 5 le barche non si trovano.

Questo algoritmo è un semplice algoritmo di riempimento. Le caratteristiche principali sono che non perde tempo a sprofondare navi di cui è a conoscenza quando ci sono ancora navi di cui non è a conoscenza e che non utilizza uno schema di riempimento inefficiente (ovvero, uno schema completamente casuale sarebbe dispendioso).

Note finali:

A) "Centro" è un punto di partenza casuale sulla scacchiera. Questo elimina il principale punto debole di questo algoritmo. B) Mentre la descrizione indica il disegno di diagonali immediatamente dall'inizio, idealmente l'algoritmo spara semplicemente in posizioni 'casuali' che si trovano lungo quelle diagonali. Questo aiuta a evitare che il concorrente cronometri per quanto tempo prima che le sue navi vengano colpite da schemi prevedibili.

Questo descrive un algoritmo 'perfetto' in quanto metterà tutte le navi sotto (9x9) / 2 + 10 colpi.

Tuttavia, può essere migliorato in modo significativo:

Una volta che una nave viene colpita, identificane le dimensioni prima di eseguire le linee diagonali "interne". Potresti aver trovato la nave 2, nel qual caso le diagonali interne possono essere semplificate per trovare più rapidamente le navi di 3 dimensioni.

Identifica le fasi del gioco e agisci di conseguenza. Questo algoritmo può andare bene fino a un certo punto nel gioco, ma altri algoritmi possono produrre migliori benefici come parte del gioco finale. Inoltre, se l'altro giocatore è molto vicino a sconfiggerti, un altro algoritmo potrebbe funzionare meglio, ad esempio un algoritmo ad alto rischio potrebbe fallire più spesso, ma quando funziona funziona rapidamente e potresti battere il tuo avversario che è più vicino alla vittoria di te .

Identifica lo stile di gioco del concorrente - potrebbe darti indizi su come pianificano il posizionamento della nave (vale a dire, è probabile che il loro algoritmo identifichi più rapidamente il modo in cui posizionano le loro navi - se l'unico strumento che hai è un martello, tutto sembra un chiodo)

-Adamo


La strategia di attesa per affondare le navi fino a quando non si trovano tutti dipende fortemente dalla variazione di un colpo per turno. Sotto la versione (numero di navi sopravvissute) di colpi per turno, è vantaggioso affondare le navi il più rapidamente possibile in modo da rallentare l'avversario.
Jason Owen,

4

La mia voce

Niente di terribilmente speciale, e non ho avuto il tempo di aggiungere tutte le buone idee che avevo.

Ma sembra giocare abbastanza bene. Vedremo come va in competizione:

(metti questo nel file Missouri.cse aggiunto al progetto.)

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

E ora che ho presentato la mia iscrizione, alcune statistiche approssimative: contro BP7 il 44% vince. / vs. Dreadnought vince il 20%. / vs. Farnsworth ha vinto il 42%. È stato un progetto divertente.
abelenky,

2

Questo non è minimax. In realtà dopo aver piazzato le navi, ogni giocatore non può giocare da solo, risultando in un certo numero di turni che ha impiegato per affondare ogni nave avversaria? Quello che ha preso meno turni vince.

Non penso che ci siano buone strategie generali oltre all'affondamento delle navi colpite e al tentativo di minimizzare il numero di colpi per coprire i possibili posti rimanenti dove le navi potrebbero nascondersi.

Ovviamente potrebbero esserci contro-strategie per tutto ciò che non è casuale. Ma non penso che ci siano strategie valide contro tutti i possibili giocatori.


1
Potenzialmente, sì, potevano giocare da soli. Non è così che verrà eseguito. Ottima idea, comunque. In questa competizione, voglio che sia possibile evitare statisticamente i colpi del tuo avversario.
John Gietzen,

2
Vedo. Utilizzando i dati delle partite precedenti contro lo stesso avversario si potrebbe essere in grado di adattarsi a lui?
ziggystar,

2

In realtà, penso che il problema più grande con il puzzle sia che si tratta essenzialmente di due mosse. Una mossa sta posizionando le tue navi, l'altra è trovare le navi nemiche (per quanto segmentata quella seconda parte potrebbe essere, a parte il tentativo di battere un orologio con un fattore casuale, è semplicemente "esegui il tuo algoritmo"). Non esiste alcun meccanismo per tentare di determinare e quindi contrastare una strategia nemica, che è ciò che rende piuttosto interessanti competizioni simili basate su cicli successivi di "forbici da roccia".

Inoltre, penso che sarebbe più bello se tu specificassi il gioco come protocollo di rete e poi fornissi il framework per implementare quel protocollo in C #, piuttosto che dettare che tutte le soluzioni dovrebbero essere C #, ma questa è solo la mia opinione.

EDIT: annullo il mio punto iniziale, dal momento che non ho letto abbastanza attentamente le regole della competizione.


Non tutte le soluzioni devono essere in C #. Posso compilare e collegare in un assembly separato. Inoltre, dovresti essere in grado di contrastare statisticamente il tuo avversario.
John Gietzen,

J #? può essere? Lol, jk. Ho un framework TCP per questo, ma questo torneo deve svolgersi molto rapidamente.
John Gietzen,

Perché dovresti presumere che la comunicazione TCP tra due processi sulla stessa macchina non sarebbe incredibilmente veloce?
Jherico,

@Jherico: Se stessi usando TCP, isolerei i motori sui loro PC in modo da poter utilizzare tutte le risorse della CPU che volevano.
John Gietzen,

Anche così, due macchine sulla stessa lan potrebbero facilmente completare un gioco in meno di un secondo con il sovraccarico della rete minimo
Jherico,

2

Mi è sempre piaciuto iniziare nel mezzo e allontanarmi a spirale da quel punto lasciando non più di 1 spazio vuoto tra gli altri punti per tenere conto di quel dannato sottomarino ... lo spazio tra i colpi dipendeva da quali navi erano affondate. se la nave B era l'ultima, i colpi dovevano lasciare solo 4 spazi in mezzo per minimizzare i colpi sprecati


1
Quindi ... ho solo bisogno di stare lontano dal centro? :)
darron,

14
Devi anche stare lontano dai bordi, perché un colpo al limite contiene più informazioni per il tuo avversario rispetto a un colpo al limite. Quindi dovresti posizionare tutte le tue navi in ​​una regione non intermedia, non di frontiera. A meno che non sia quello che si aspettano che tu faccia.
Jherico,

1
Se inizi lasciando 3 o 4 spazi, potresti essere abbastanza fortunato da colpire comunque il sottomarino. In caso contrario, torna indietro e prova a riempire gli spazi vuoti. Altro su: somethinkodd.com/oddthinking/2009/10/29/battleship-strategy
Oddthinking

18
La nave con due fori non è un maledetto sottomarino , è una maledetta barca PT . Il sottomarino ha tre fori. :)
corvo,

2

Un concorso simile è stato organizzato dal dott. James Heather dell'Università del Surrey per conto della British Computer Society.

Sono state poste delle limitazioni alle risorse - ovvero il tempo massimo del processore per turno, nessuno stato poteva essere memorizzato tra le mosse, la dimensione massima dell'heap imposta. Per limitare il tempo l'IA potrebbe presentare una mossa in qualsiasi momento all'interno della fascia oraria e verrebbe richiesta una mossa al termine del turno.

Molto interessante - vedi di più su: http://www.bcsstudentcontest.com/

Potrebbe darti qualche idea in più.


2

Così com'è, la soluzione si apre e funziona senza modifiche in monodevelop in Ubuntu 9.10 Linux


1

Hai scritto:

  • Tutto ciò che viene ritenuto contrario allo spirito della competizione sarà motivo di squalifica.
  • Interferire con un avversario è contro lo spirito della competizione.

definire "contro lo spirito della competizione" e "interferire con un avversario"?

Inoltre, per semplificare, ti consiglio di:

  • non consentire l'utilizzo della CPU durante lo slot CPU dell'avversario.
  • non consente il parallelismo dei thread e offre invece più secondi CPU su un singolo thread. Ciò semplificherà la programmazione dell'intelligenza artificiale e non danneggerà comunque chiunque sia associato a CPU / memoria.

PS: una domanda per i post-doc di CS in agguato qui: questo gioco non è risolvibile (ovvero esiste un'unica strategia migliore?). sì, le dimensioni del tabellone e il numero di passaggi rendono minimax et al obbligatoria, ma devo ancora chiedermi ... è tutt'altro che Go e gli scacchi nella complessità.


Avevo in mente la riflessione quando ho detto "Interferire". Non voglio che i concorrenti vincano perché hanno messo a segno un altro motore a morte.
John Gietzen,

8
Suggerirei che lo spionaggio sia una parte importante della guerra moderna, quindi riflettere per trovare gli obiettivi sarebbe l'ideale - dopo tutto, era uno dei metodi utilizzati durante la seconda guerra mondiale ...
Rowland Shaw,

Ho un framework per isolare i motori su diversi PC, comunicare su TCP / IP, rendendo inutile Reflection. Tuttavia, a causa del mio numero stimato di iscrizioni, ciò renderebbe la competizione proibizionalmente lunga.
John Gietzen,

6
Allora non sapevo che avevano Reflection!
Markus Nigbur,

1

Prevedo che vincerà la persona che riesce a decodificare i propri avversari in modo casuale seed e call pattern.

Non sono sicuro di quanto sia probabile.


Gli avversari hanno la possibilità di usare un CSPRNG.
John Gietzen,

Un buon punto, anche se ammetto che il reverse engineering di un tale seme è comunque al di là delle mie competenze. Immagino che l'aspetto più vulnerabile sarebbe l'algoritmo di selezione dei modelli di fuoco, ma anche in questo caso non trarrai necessariamente molto vantaggio dalla sua rottura, poiché non è possibile spostare le tue navi una volta iniziato il gioco.
Triston Attridge,

Quando stavo facendo domanda per uno stage di ricerca, abbiamo scritto programmi di battaglia e gareggiato. Impostando seed casuali è stato esattamente come ho vinto X)
P Shved il

1
Supponendo un algoritmo di posizionamento della nave ragionevolmente semplice, immagino che, dopo aver ottenuto un paio di colpi su navi diverse, si potrebbe iniziare a usare la maggior parte del proprio turno in sequenza attraverso tutti i possibili semi casuali (probabilmente iniziando da qualche parte vicino all'ora corrente e andando avanti / all'indietro di un passo o giù di lì) e vedere quali generano posizionamenti della nave compatibili con i colpi osservati.
Domenic,

1

Presumibilmente, sarebbe anche possibile eseguirne una serie con variazioni sul gioco.

Aggiungendo cose come un aereo 3d o essere in grado di muovere una singola nave invece di sparare per un turno, probabilmente cambierebbe un po 'il gioco.


2
C'è la variazione "salvo". Dove puoi sparare tanti colpi per turno quante restano le navi.
John Gietzen,

Una variazione interessante pure. Mi sembra di ricordare di aver giocato una versione per computer che aveva anche un aereo. Sparerebbe casualmente in punti sul tabellone avversario.
Glenn,

un'altra variante: essere la dimensione della scheda + numero di navi.
russau,

1

Il secondo in totale tempo di gioco è specifico per la macchina. Una volta che il secondo valore delle operazioni della CPU sarà diverso sulla mia macchina rispetto alla macchina del torneo. Se ottimizzo l'algoritmo della nave da battaglia per utilizzare la maggior parte del tempo di CPU entro 1 secondo, allora viene eseguito su una possibile macchina da torneo più lenta, perderà sempre.

Non sono sicuro di come aggirare questa limitazione del framework, ma dovrebbe essere affrontato.

...

Un'idea è di fare ciò che è stato fatto in questo concorso http://www.bcsstudentcontest.com /

E ha un tempo massimo per turno rispetto al tempo totale di gioco massimo. In questo modo potrei limitare gli algoritmi per adattarli entro un tempo di svolta noto. Un gioco potrebbe durare da 50 a 600+ turni, se il mio algoritmo gestisce il suo tempo di gioco totale, potrebbe non dare il tempo sufficiente per fare il suo lavoro migliore o potrebbe perdere troppo tempo e perdere. È molto difficile gestire il tempo di gioco totale all'interno dell'algoritmo Battleship.

Suggerirei di modificare le regole per limitare il tempo di turno e non il tempo di gioco totale.

modificare

Se ho scritto un algoritmo che elenca tutti i colpi possibili e li classifica, quindi prende il punteggio più alto. Ci vorrebbe troppo tempo per generare tutti i possibili scatti, quindi lascerei correre l'algoritmo per un certo periodo di tempo e poi fermarlo.

Se ci fosse un limite basato su turni, potrei lasciare correre l'algoritmo per 0,9 secondi e restituire il tiro più alto in classifica, e stare bene con il limite di tempo di virata.

Se sono limitato al tempo di gioco totale di un secondo, sarà difficile determinare per quanto tempo deve essere eseguito l'algoritmo per ogni turno. Voglio massimizzare il tempo della mia CPU. Se un gioco è durato 500 round, potrei limitare ogni turno a 0,002 secondi, ma se un gioco è durato 100 round, potrei dare ad ogni turno 0,01 secondi di tempo di CPU.

Sarebbe poco pratico per un algoritmo utilizzare una ricerca semi-esaustiva dello spazio di tiro per trovare il tiro migliore con la limitazione attuale.

Il tempo di gioco totale di 1 secondo limita il tipo di algoritmi che possono essere effettivamente utilizzati per competere nel gioco.


Questo verrà eseguito su un quad core Intel Q9550SX, 8 GB di ram, macchina Vista 64. 1 secondo sarà un fattore limitante?
John Gietzen,

Immagino che dovresti rendere la tua corazzata AI multithread, quindi, per calcolare il numero massimo di colpi per quell'intervallo di tempo.
Jeff Atwood,

il trucco è come limitare l'intervallo di tempo di svolta. Se lo limito a 0,00005 secondi, non posso correre oltre il limite di tempo, ma sto limitando significativamente lo spazio di ricerca. Se aumento il limite di tempo di virata, lo spazio di ricerca aumenta ma corro il rischio di rimanere senza tempo.
TonyAbell,

@TonyAbell: se è importante avere un limite di tempo basato sui turni, perché non iniziare con un valore iniziale e poi regolarlo da un round all'altro? Dopo circa la metà dei round, molto probabilmente avrai trovato la lunghezza del turno ottimale per l'avversario che stai affrontando.
Kyokley,

Dovresti tenere traccia del tempo rimasto e limitarlo a 1/2 del tempo disponibile rimanente.
John Gietzen,

1

Sto uscendo qui non inserendo il codice reale, ma rischierò alcune osservazioni generali:

  • Dal momento che tutte le navi hanno una dimensione di almeno 2 celle, puoi usare un'ottimizzazione che ho visto in un'implementazione del gioco in Space Quest V - che spara solo a celle alternate in uno schema a diamante mentre "cerca" un bersaglio. Questo elimina la metà dei quadrati, garantendo comunque che alla fine troverai tutte le navi.
  • Un modello di tiro casuale quando si cercano bersagli produrrà statisticamente i migliori risultati in molti giochi.

1

! [Probabilità densità] [1] inserisci la descrizione dell'immagine lei

! [inserisci qui la descrizione dell'immagine] [2]

Ho sperimentato il confronto tra i risultati delle riprese di abbandono con una caccia / bersaglio stupido e infine una ricerca sofisticata.

La soluzione migliore sembra essere quella di creare una funzione di densità di probabilità per la probabilità con cui ogni singolo quadrato viene utilizzato dalle navi rimanenti e puntare con il quadrato con il valore più alto.

Puoi vedere i miei risultati qui inserisci la descrizione del link qui


Potresti forse risolvere la tua risposta e soprattutto le tue immagini e il tuo link?
Bart, il

-2

"Battleship" è ciò che è noto come un classico problema NP completo di informatica.

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

(cerca Battleship - è lì, sotto giochi e puzzle)


4
Che è un puzzle da battaglia ( en.wikipedia.org/wiki/Battleship_(puzzle) ), non Battleship the game ( en.wikipedia.org/wiki/Battleship_(game) ).
Jason Berkan,

Sì, come ha affermato Jason, questo è un animale completamente diverso.
John Gietzen,

3
Hehehe. Il prossimo incarico che arrivo al lavoro dirò che è NP-completo, quindi pranzerò a lungo. :-)
Bork Blatt,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.