¿Cuál es la mejor IA de acorazado?


315

¡Acorazado!

En 2003 (cuando tenía 17 años), compití en una competencia de codificación de IA de Battleship . Aunque perdí ese torneo, me divertí mucho y aprendí mucho de él.

Ahora, me gustaría resucitar esta competencia, en busca de la mejor IA de acorazado.

Aquí está el marco, ahora alojado en Bitbucket .

¡El ganador recibirá una reputación de +450! La competencia se llevará a cabo a partir del 17 de noviembre de 2009 . No se aceptarán entradas o ediciones posteriores a la hora cero del día 17. (Hora estándar central) ¡Envíe sus entradas temprano, para que no pierda su oportunidad!

Para mantener este OBJETIVO , siga el espíritu de la competencia.

Reglas del juego:

  1. El juego se juega en una cuadrícula de 10x10.
  2. Cada competidor colocará cada uno de los 5 barcos (de longitud 2, 3, 3, 4, 5) en su cuadrícula.
  3. Ningún barco puede superponerse, pero puede ser adyacente.
  4. Los competidores luego se turnan para disparar tiros individuales a su oponente.
    • Una variación en el juego permite disparar múltiples disparos por volea, uno para cada barco superviviente.
  5. El oponente notificará al competidor si el tiro se hunde, golpea o falla.
  6. El juego termina cuando todas las naves de cualquier jugador están hundidas.

Reglas de la competencia:

  1. El espíritu de la competencia es encontrar el mejor algoritmo de Battleship.
  2. Cualquier cosa que se considere contra el espíritu de la competencia será motivo de descalificación.
  3. Interferir con un oponente va en contra del espíritu de la competencia.
  4. El subprocesamiento múltiple se puede usar bajo las siguientes restricciones:
    • No se puede ejecutar más de un hilo mientras no sea tu turno. (Sin embargo, cualquier cantidad de subprocesos puede estar en estado "Suspendido").
    • Ningún subproceso puede ejecutarse con una prioridad que no sea "Normal".
    • Dadas las dos restricciones anteriores, se le garantizará al menos 3 núcleos de CPU dedicados durante su turno.
  5. Se asigna un límite de 1 segundo de tiempo de CPU por juego a cada competidor en el hilo primario.
  6. Quedarse sin tiempo resulta en perder el juego actual.
  7. Cualquier excepción no controlada resultará en la pérdida del juego actual.
  8. Se permite el acceso a la red y al disco, pero puede encontrar que las restricciones de tiempo son bastante prohibitivas. Sin embargo, se han agregado algunos métodos de configuración y desmontaje para aliviar la tensión del tiempo.
  9. El código debe publicarse en el desbordamiento de la pila como respuesta o, si es demasiado grande, vinculado.
  10. El tamaño total máximo (sin comprimir) de una entrada es de 1 MB.
  11. Oficialmente, .Net 2.0 / 3.5 es el único requisito de marco.
  12. Su entrada debe implementar la interfaz IBattleshipOpponent.

Puntuación:

  1. Los mejores 51 juegos de 101 juegos son los ganadores de un partido.
  2. Todos los competidores jugarán enfrentados entre sí, al estilo round-robin.
  3. La mejor mitad de los competidores jugará un torneo de doble eliminación para determinar el ganador. (El poder más pequeño de dos que es mayor o igual a la mitad, en realidad).
  4. Usaré el marco TournamentApi para el torneo.
  5. Los resultados serán publicados aquí.
  6. Si envía más de una entrada, solo su entrada con la mejor puntuación es elegible para la doble eliminación.

¡Buena suerte! ¡Que te diviertas!


EDITAR 1:
Gracias a Freed , que ha encontrado un error en la Ship.IsValidfunción. Ha sido arreglado. Descargue la versión actualizada del marco.

EDIT 2:
dado que ha habido un interés significativo en las estadísticas persistentes en el disco y tal, he agregado algunos eventos de configuración y desmontaje no programados que deberían proporcionar la funcionalidad requerida. Este es un cambio semi-rompedor . Es decir: la interfaz se ha modificado para agregar funciones, pero no se requiere ningún cuerpo para ellas. Descargue la versión actualizada del marco.

EDITAR 3:
Solución de error 1: GameWony GameLostsolo se llamaba en caso de un tiempo de espera.
Corrección de errores 2: si un motor estuviera agotando cada juego, la competencia nunca terminaría.
Descargue la versión actualizada del marco.

EDITAR 4:
Resultados del torneo:


Si la entrada requiere una base de datos grande, ¿puede conectarse a ella a través de la red? Es decir. ¿Puede la entrada hacer llamadas al servicio web?
Remus Rusanu

¿Hay un límite de tamaño en las entradas?
Jherico

8
@ Steven: Además, consulté a Jeff Atwood solo para ver si era apropiado. Aquí está su respuesta: twitter.com/codinghorror/status/5203185621
John Gietzen

1
También agregaría que, dado el inevitable componente aleatorio de estos 50 juegos, no será suficiente para distinguir con precisión entre muy buenas implementaciones. Creo que 501 o más pueden ser necesarios para una visión razonable de cuál es mejor.
ShuggyCoUk

1
Un oponente "pacífico" que se niega a colocar barcos hace que la competencia se cuelgue. No estoy seguro de cuánto te importa que la gente haga cosas tontas como esa. :)
Joe

Respuestas:


56

Secundo la moción para hacer muchos más juegos por partido. Hacer 50 juegos es solo lanzar una moneda. Necesitaba hacer 1000 juegos para obtener una distinción razonable entre los algoritmos de prueba.

Descargar Dreadnought 1.2 .

Estrategias:

  • realiza un seguimiento de todas las posiciones posibles para barcos que tienen> 0 golpes. La lista nunca se hace más grande que ~ 30K, por lo que puede mantenerse exactamente, a diferencia de la lista de todas las posiciones posibles para todos los barcos (que es muy grande).

  • El algoritmo GetShot tiene dos partes, una que genera disparos aleatorios y la otra que intenta terminar de hundir un barco que ya ha sido alcanzado. Hacemos disparos aleatorios si hay una posible posición (de la lista anterior) en la que todos los barcos afectados están hundidos. De lo contrario, intentamos terminar de hundir un barco eligiendo un lugar para disparar que elimine la mayor cantidad de posiciones posibles (ponderadas).

  • Para disparos aleatorios, calcule la mejor ubicación para disparar en función de la probabilidad de que uno de los barcos no hundidos se superponga a la ubicación.

  • algoritmo adaptativo que coloca las naves en lugares donde el oponente es estadísticamente menos propenso a disparar.

  • algoritmo adaptativo que prefiere disparar en lugares donde el oponente es estadísticamente más probable que coloque sus naves.

  • colocar naves en su mayoría sin tocarse entre sí.


en mi máquina de prueba (un netbook ULV Celeron) este código pierde constantemente por tiempo de espera. Cuando dejo que tome todo el tiempo que quiera, azota Simple (aproximadamente 90% de tasa de éxito). Si se basa en gran medida de las especificaciones de la máquina que va a estar en ejecución en plazos a pegar es posible que desee dar un poco de margen de maniobra ...
ShuggyCoUk

Interesante ... Funciona bien en la máquina del torneo. Sin embargo, un motor "perfecto" se adaptaría a la cantidad de tiempo que ya había pasado.
John Gietzen el

35

Aquí está mi entrada! (La solución más ingenua posible)

"Aleatorio 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
En realidad, esta respuesta es buena porque muestra de forma muy concisa las API que necesitarías implementar para competir ... :)
dicroce

1
Cuando construí un proyecto similar en la clase de Algoritmos de mi universidad, utilicé lógica aleatoria entrelazada con cierta toma de decisiones. ¡Fue bueno a veces!
Nathan Taylor

2
Esto podría intentar colocar naves superpuestas, ¿no?

66
Sí, pero el motor no permitirá esto. Luego le dirá a la IA que los coloque nuevamente, pero esta vez, con una voz más severa. (Visto por pop ax \ cmp ax, 1 \ je stern)
John Gietzen

55
Nota importante para cualquiera que, como yo, supusiera que podría superar esto fácilmente recordando los disparos colocados previamente y no repitiendo. El marco ignorará las repeticiones y le dará otra oportunidad siempre que su tiempo total sea inferior al límite. Esta es pobre en mi opinión, si alguien se mete hasta su algo que debe ser penalizado ...
ShuggyCoUk

22

Aquí hay un oponente para que la gente juegue contra:

En lugar de utilizar una estrategia fija inspirada en la geometría, pensé que sería interesante intentar estimar las probabilidades subyacentes de que un espacio inexplorado en particular contenga una nave.

Para hacer esto bien, exploraría todas las configuraciones posibles de naves que se ajustan a su visión actual del mundo, y luego calcularía las probabilidades basadas en esas configuraciones. Podrías pensar en ello como explorar un árbol:

una expansión de posibles estados de acorazados http://natekohl.net/media/battleship-tree.png

Después de considerar todas las hojas de ese árbol que coinciden con lo que sabes sobre el mundo (por ejemplo, los barcos no pueden superponerse, todos los cuadrados de impacto deben ser barcos, etc.) puedes contar con qué frecuencia ocurren los barcos en cada posición inexplorada para estimar la probabilidad de que Un barco está sentado allí.

Esto se puede visualizar como un mapa de calor, donde los puntos calientes tienen más probabilidades de contener barcos:

un mapa de probabilidades de cada posición inexplorada http://natekohl.net/media/battleship-probs.png

Una cosa que me gusta de esta competencia de acorazado es que el árbol de arriba es casi lo suficientemente pequeño como para utilizar este tipo de algoritmo con fuerza bruta. Si hay ~ 150 posiciones posibles para cada una de las 5 naves, eso es 150 5 = 75 mil millones de posibilidades. Y ese número solo se reduce, especialmente si puedes eliminar barcos enteros.

El oponente al que me vinculé anteriormente no explora todo el árbol; 75 mil millones todavía son demasiado grandes para entrar en menos de un segundo. Sin embargo, intenta estimar estas probabilidades con la ayuda de algunas heurísticas.


Hasta ahora, está superando a nuestra única otra solución completa en aproximadamente 67.7% a 32.3% :)
John Gietzen

2
Definitivamente tengo curiosidad por ver cómo un "enfoque de probabilidad" se compara con un "enfoque geométrico". Me di cuenta de que este oponente de probabilidad realmente realiza movimientos que siguen los patrones geométricos discutidos en otras respuestas. Podría ser que usar geometría sea igual de bueno y mucho más rápido. :)
Nate Kohl el

12

No es una respuesta completa, pero parece poco útil saturar las respuestas reales con un código común. Por lo tanto, presento algunas extensiones / clases generales en el espíritu del código abierto. Si los usa, cambie el espacio de nombres o intentar compilar todo en un dll no funcionará.

BoardView le permite trabajar fácilmente con un tablero anotado.

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

Algunas extensiones, algunas de ellas duplican la funcionalidad en el marco principal, pero realmente debería hacerlo usted.

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

Algo que termino usando mucho.

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

Aleatorización Seguro pero comprobable, útil para pruebas.

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

No tengo tiempo en este momento para escribir un algoritmo completo, pero aquí hay un pensamiento: si su oponente coloca naves al azar, ¿no serían las probabilidades de colocación una distribución simple centrada en (5.5,5.5)? Por ejemplo, las posibilidades de colocación para el acorazado (5 unidades de largo) en la dimensión x están aquí:

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

Los mismos cálculos serían válidos para y. Las otras naves no tendrían una distribución tan pronunciada, pero su mejor suposición sigue siendo el centro. Después de eso, el enfoque matemático estaría irradiando lentamente diagonales (quizás con la longitud de la nave promedio, 17/5) fuera del centro. Ex:

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

Obviamente, habría que agregar algo de aleatoriedad a la idea, pero creo que, matemáticamente, ese es el camino a seguir.


Sí, de hecho lo harían. Mi viejo motor compensó eso.
John Gietzen

1
De donde vengo, irradiar lentamente diagonales fuera del centro se considera trampa .
bzlm

Si se considera hacer trampa, hay una contramedida bastante fácil. Evite (x, y) donde x = y. :)
ine

55
¿Creo que aludía al conteo de cartas? Lo cual, en mi opinión, no es trampa.
John Gietzen

10

Nada tan sofisticado pero aquí está lo que se me ocurrió. Le gana al oponente aleatorio el 99.9% del tiempo. Estaría interesado si alguien tiene otros pequeños desafíos como este, fue muy divertido.

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

Ligeramente condensado para ocupar un espacio mínimo aquí y aún ser legible.


6

Algunos comentarios sobre el motor de competencia:

Parámetros de NewGame:

Si IBattleshipOpponent :: NewGame está destinado a la configuración previa al juego y toma un tamaño de tabla, también debe tomar una lista de barcos y sus respectivos tamaños. No tiene sentido permitir un tamaño de tabla variable sin permitir configuraciones de nave variables.

Las naves están selladas:

No veo ninguna razón por la cual la nave de clase esté sellada. Entre otras cosas básicas, me gustaría que Ships tenga un Nombre, para poder enviar mensajes como ("Has hundido mi {0}", ship.Name); . También tengo otras extensiones en mente, así que creo que Ship debería ser heredable.

Límites de tiempo:

Si bien el límite de tiempo de 1 segundo tiene sentido para una regla de torneo, se mete totalmente con la depuración. BattleshipCompetition debería tener una configuración fácil para ignorar las violaciones de tiempo para ayudar con el desarrollo / depuración. También sugeriría investigar System.Diagnostics.Process :: UserProcessorTime / Privileged ProcessorTime / TotalProcessorTime para obtener una vista más precisa de cuánto tiempo se está utilizando.

Barcos hundidos:

La API actual te informa cuando has hundido el barco de un oponente:

ShotHit(Point shot, bool sunk);

¡Pero no qué barco hundiste! Considero que es parte de las reglas del acorazado humano que debes declarar "¡Hundiste mi acorazado!" (o destructor, o sub, etc.).

Esto es especialmente crítico cuando una IA está tratando de eliminar barcos que se unen entre sí. Me gustaría solicitar un cambio de API para:

ShotHit(Point shot, Ship ship);

Si el barco no es nulo, implica que el disparo fue un hundimiento, y usted sabe qué barco hundió y cuánto tiempo fue. Si el disparo no se hundió, el barco es nulo y no tiene más información.


Publique muestras de código si cree que el tiempo se puede hacer con mayor precisión. No quiero cambiar demasiado las reglas en este momento.
John Gietzen

Además, los tamaños de los barcos se pasan durante el PlaceShips (), que se ejecuta exactamente una vez por juego y también se puede utilizar como una fase de configuración. Por favor, siéntase libre de abrir el barco para su propia prueba, pero planeo usar el sellado para el torneo.
John Gietzen

ERROR: @John Gietzen: he determinado que PlaceShips NO se ejecuta exactamente una vez por juego (como dijiste). Si un jugador coloca sus naves incorrectamente (como suele hacer el RandomOpponent), entonces se llama a PlaceShips repetidamente, sin una llamada interpuesta de NewGame.
abelenky

55
Siempre consideré una estrategia colocar dos naves en una configuración L para hacer que mi oponente pensara que habían hundido un barco de guerra cuando en realidad no lo habían hecho. Nunca tuve la impresión de que tuvieras que declarar qué barco se hundió.
Josh Smeaton el

3
@DJ: Estoy siguiendo las reglas originales de lápiz y papel. Recuerda que Hasbro es una empresa de juguetes y que este juego es anterior a Hasbro.
John Gietzen el

5

CrossFire actualizado. Sé que no puede competir con Farnsworth o Dreadnought, pero es mucho más rápido que este último y fácil de jugar en caso de que alguien quiera probar. Esto se basa en el estado actual de mis bibliotecas, incluidas aquí para facilitar su uso.

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

Esto es lo mejor que pude reunir en mi tiempo libre, que es casi inexistente. Hay algunas estadísticas de recuento de juegos y partidos en curso, ya que configuré la función principal para hacer un bucle y ejecutar continuamente BattleshipCompetition hasta que presioné una tecla.

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

Esta lógica es lo más cerca que tuve de vencer a Dreadnought, ganando aproximadamente el 41% de los juegos individuales. (En realidad ganó un partido por un conteo de 52 a 49). Curiosamente, esta clase no funciona tan bien contra FarnsworthOpponent como una versión anterior que era mucho menos avanzada.


5

Dell está reparando mi computadora en este momento, pero aquí es donde estaba la semana pasada:

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
Felicidades por la plata. ¿Te importaría describir tu algoritmo en palabras? Sería interesante saberlo.
Thomas Ahle

4

Si es un bruto que fuerza su análisis, entonces puede encontrar que la mecánica del RandomOpponent suministrado es altamente ineficiente. Se permite volver a seleccionar ubicaciones ya seleccionadas y permite que el marco lo obligue a repetir hasta que llegue a una que aún no ha tocado o hasta que caduque el límite de tiempo por movimiento.

Este oponente tiene un comportamiento similar (la distribución de colocación efectiva es la misma), solo realiza la comprobación de la cordura y solo consume una generación de números aleatorios por llamada (amortizada).

Esto usa las clases en mis extensiones / respuesta de biblioteca y solo proporciono los métodos / estado clave.

Shuffle se levanta de la respuesta de Jon Skeet aquí

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

No voy a poder participar, pero aquí está el algoritmo que implementaría si tuviera tiempo:

Primero, cuando detecto un golpe, no persigo al resto del barco de inmediato: construyo una tabla de ubicaciones de barcos y descubro si he golpeado los cinco al menos una vez antes de comenzar a hundirlos por completo. (Tenga en cuenta que esta es una mala política para la variante de disparos múltiples - ver comentarios)

  1. Golpee el centro (vea la nota final a continuación: 'centro' es solo una conveniencia para la descripción)
  2. Golpea el punto 4 a la derecha del centro
  3. Golpea el punto 1 hacia abajo y uno a la derecha del centro
  4. Golpea el punto cuatro a la derecha del golpe anterior
  5. Continúe en ese patrón (debe terminar con líneas diagonales separadas por 3 espacios que llenan el tablero) Esto debería golpear a todos los botes de 4 y 5 esloras, y un número estadísticamente grande de 3 y 2 botes.

  6. Comience a golpear al azar puntos entre las diagonales, esto capturará los barcos de 2 y 3 esloras que aún no se han notado.

Una vez que haya detectado 5 golpes, determinaría si los 5 golpes están en botes separados. Esto es relativamente fácil haciendo algunos disparos más cerca de ubicaciones donde dos golpes están en la misma línea horizontal o vertical y están dentro de 5 ubicaciones entre sí (podrían ser dos golpes en el mismo barco). Si son botes separados, continúe hundiendo todos los barcos. Si se encuentra que son el mismo bote, continúe con los patrones de llenado anteriores hasta que se encuentren los 5 botes.

Este algoritmo es un algoritmo de llenado simple. Las características clave son que no pierde el tiempo hundiendo barcos que conoce cuando todavía hay barcos que desconoce, y no utiliza un patrón de llenado ineficiente (es decir, un patrón completamente aleatorio sería un desperdicio).

Notas finales

A) "Centro" es un punto de partida aleatorio en el tablero. Esto elimina la debilidad primaria de este algoritmo. B) Si bien la descripción indica el dibujo de diagonales inmediatamente desde el comienzo, idealmente el algoritmo simplemente dispara en ubicaciones 'aleatorias' que se encuentran a lo largo de esas diagonales. Esto ayuda a evitar que el competidor cronometre el tiempo hasta que sus barcos sean golpeados por patrones predecibles.

Esto describe un algoritmo 'perfecto' en el que obtendrá todos los barcos en menos de (9x9) / 2 + 10 disparos.

Sin embargo, se puede mejorar significativamente:

Una vez que se golpea un barco, identifica su tamaño antes de hacer las líneas diagonales 'internas'. Es posible que haya encontrado la nave 2, en cuyo caso las diagonales internas se pueden simplificar para encontrar las naves de 3 tamaños más rápidamente.

Identificar etapas en el juego y actuar en consecuencia. Este algoritmo puede ser bueno hasta cierto punto en el juego, pero otros algoritmos pueden producir mejores beneficios como parte del juego final. Además, si el otro jugador está muy cerca de derrotarlo, otro algoritmo podría funcionar mejor; por ejemplo, un algoritmo de alto riesgo podría fallar con más frecuencia, pero cuando funciona funciona rápidamente y puede vencer a su oponente que está más cerca de ganar que usted .

Identifique el estilo de juego del competidor: puede darle pistas sobre cómo planean la colocación del barco (es decir, es muy probable que su propio algoritmo identifique más rápidamente cómo colocan sus propios barcos), si la única herramienta que tiene es un martillo, todo parece un clavo)

-Adán


La estrategia de esperar para hundir barcos hasta que se encuentren todos depende en gran medida de la variación de un disparo por turno. En la versión (número de naves sobrevivientes) de disparos por turno, es ventajoso hundir naves lo más rápido posible para ralentizar a tu oponente.
Jason Owen el

4

Mi entrada

Nada terriblemente especial, y no tuve tiempo de agregar todas las buenas ideas que tenía.

Pero parece jugar bastante bien. Veremos cómo le va en competencia:

(poner esto en un archivo Missouri.csy agregarlo al proyecto).

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

Y ahora que he enviado mi entrada, algunas estadísticas aproximadas: vs. BP7 44% gana. / vs. Dreadnought gana un 20%. / vs. Farnsworth 42% de victorias. Fue un proyecto divertido.
abelenky

2

Esto no es minimax. En realidad, después de colocar las naves, ¿no puede cada jugador jugar solo, lo que resulta en una serie de turnos que le llevó a hundir cada nave oponente? El que tomó menos turnos gana.

No creo que haya buenas estrategias generales más allá de hundir barcos de impacto y tratar de minimizar el número de disparos para cubrir los posibles lugares restantes donde los barcos podrían esconderse.

Por supuesto, puede haber contra-estrategias para cualquier cosa que no sea aleatoria. Pero no creo que haya estrategias que sean buenas contra todos los jugadores posibles.


1
Potencialmente, sí, podrían jugar solos. Así no se ejecutará esto. Gran idea, sin embargo. En esta competencia, quiero que sea posible evitar estadísticamente los disparos de tu oponente.
John Gietzen

2
Veo. ¿Usando datos de juegos anteriores contra el mismo oponente, uno podría adaptarse a él?
ziggystar

2

En realidad, creo que el mayor problema con el rompecabezas es que son esencialmente dos movimientos. Un movimiento es colocar tus naves, el otro es encontrar las naves enemigas (por más segmentada que esté la segunda parte, aparte de tratar de vencer a un reloj con un factor aleatorio, es solo "ejecutar tu algoritmo"). No hay ningún mecanismo para tratar de determinar y luego contrarrestar una estrategia enemiga, que es lo que hace que competiciones similares basadas en rondas sucesivas de "piedra, papel o tijera" sean bastante interesantes.

Además, creo que sería mejor si especificaras el juego como un protocolo de red y luego proporcionaras el marco para implementar ese protocolo en C #, en lugar de dictar que todas las soluciones deberían ser C #, pero esa es solo mi opinión.

EDITAR: rescindir mi punto inicial, ya que no leí las reglas de la competencia con suficiente cuidado.


No todas las soluciones deben estar en C #. Puedo compilar y vincular en un ensamblaje separado. Además, deberías poder contrarrestar estadísticamente a tu oponente.
John Gietzen

J #? ¿tal vez? LOL JK. Tengo un marco TCP para esto, pero este torneo debe ejecutarse muy rápidamente.
John Gietzen

¿Por qué asumirías que la comunicación TCP entre dos procesos en la misma máquina no sería extremadamente rápida?
Jherico

@Jherico: Si estuviera usando TCP, estaría aislando los motores en sus propias PC para que pudieran usar los recursos de CPU que quisieran.
John Gietzen

Aun así, dos máquinas en el mismo lan podrían completar fácilmente un juego en menos de un segundo con una sobrecarga de red mínima
Jherico

2

Siempre me gustó comenzar en el medio y alejarse en espiral de ese punto, dejando no más de 1 espacio en blanco entre cualquier otro punto para dar cuenta de ese maldito submarino ... el espacio entre disparos dependía de qué barcos se hundieran. si el barco B fue el último, los disparos solo tuvieron que dejar 4 espacios en el medio para minimizar los disparos desperdiciados


1
Entonces ... ¿solo necesito alejarme del medio? :)
darron

14
También debes mantenerte alejado de los bordes, porque un golpe de borde contiene más información para tu oponente que un golpe sin borde. Por lo tanto, debe colocar todas sus naves en una región no intermedia ni borde. A menos que eso sea lo que esperan que hagas.
Jherico

1
Si comienzas dejando 3 o 4 espacios, podrías tener la suerte de golpear el submarino de todos modos. De lo contrario, regrese e intente llenar los espacios en blanco. Más en: somethinkodd.com/oddthinking/2009/10/29/battleship-strategy
Oddthinking

18
El barco con dos agujeros no es un maldito submarino , es un maldito barco PT . El submarino tiene tres agujeros. :)
cuervo

2

Hubo una competencia similar organizada por el Dr. James Heather de la Universidad de Surrey en nombre de la British Computer Society.

Se establecieron limitaciones en los recursos, es decir, el tiempo máximo de procesador por turno, no se pudo almacenar ningún estado entre movimientos, se impuso el tamaño máximo de almacenamiento dinámico. Para limitar el tiempo, la IA podría enviar un movimiento en cualquier punto dentro del intervalo de tiempo y se le solicitaría un movimiento al finalizar el turno.

Muy interesante - ver más en: http://www.bcsstudentcontest.com/

Podría darte algunas ideas más.


2

Tal como está, la solución se abre y se ejecuta sin modificación en monodevelop en ubuntu 9.10 linux


1

Tu escribiste:

  • Cualquier cosa que se considere contra el espíritu de la competencia será motivo de descalificación.
  • Interferir con un oponente va en contra del espíritu de la competencia.

por favor defina "contra el espíritu de la competencia" e "interferir con un oponente"?

Además, para simplificar, le recomiendo que:

  • no permitir el uso de CPU durante la ranura de CPU del oponente.
  • no permita el paralelismo de subprocesos y, en su lugar, dé más segundos de CPU en un solo subproceso. Esto simplificará la programación de la IA y de todos modos no dañará a nadie que esté vinculado a la CPU / memoria.

PD: una pregunta para los post-docs de CS que acechan aquí: ¿no se puede resolver este juego (es decir, hay una única y mejor estrategia?). sí, el tamaño del tablero y la cantidad de pasos hacen que minimax et al sean obligatorios, pero aún así me pregunto ... está lejos de ser Go y ajedrez en complejidad.


Tenía en mente la reflexión cuando dije "Interferir". No quiero que ganen los competidores porque mataron a otro motor.
John Gietzen

8
Yo sugeriría que el espionaje es una parte importante de la guerra moderna, por lo que refleja encontrar los objetivos sería ideal - después de todo, era uno de los métodos utilizados durante la segunda guerra mundial ...
Rowland Shaw

Tengo un marco para aislar los motores en diferentes PC, comunicarme a través de TCP / IP, haciendo que Reflection no tenga valor. Sin embargo, debido a mi número estimado de participaciones, esto haría que la competencia tomara un tiempo prohibitivamente largo.
John Gietzen

66
¡No sabía que tenían Reflection en aquel entonces!
Markus Nigbur

1

Mi predicción es que la persona que logre aplicar ingeniería inversa al patrón inicial de semillas y llamadas de sus oponentes ganará.

Sin embargo, no estoy seguro de qué tan probable es eso.


Los oponentes tienen la opción de usar un CSPRNG.
John Gietzen

Buen punto, aunque admito que la ingeniería inversa de esa semilla está más allá de mi experiencia de todos modos. Supongo que el aspecto más vulnerable sería el algoritmo de selección de patrones de fuego, pero incluso así no necesariamente ganarías mucho al romperlo, ya que no hay forma de que puedas mover tus naves una vez que el juego ha comenzado.
Triston Attridge

Cuando solicité una pasantía de investigación, escribimos programas de acorazados y competimos. Al establecer semilla aleatoria fue exactamente cómo gané X)
P Shved

1
Suponiendo un algoritmo de colocación de barcos razonablemente simple, me imagino que uno podría, después de recibir un par de golpes en diferentes barcos, comenzar a usar la mayor parte de su turno recorriendo todas las semillas aleatorias posibles (probablemente comenzando en algún lugar cerca de la hora actual y avanzando / retroceder un paso más o menos) y ver cuáles generan ubicaciones de barcos compatibles con los golpes observados.
Domenic

1

También, presumiblemente, sería posible ejecutar una serie de estos con variaciones en el juego.

Agregar cosas como un avión en 3D o poder mover una sola nave en lugar de disparar por un turno probablemente cambiaría el juego un poco.


2
Existe la variación "salvo". Donde puedes disparar tantos disparos por turno como te quedan barcos.
John Gietzen

Una variación interesante también. Me parece recordar haber jugado una versión de computadora que también tenía un avión. Dispararía al azar en ubicaciones en el tablero contrario.
Glenn

Otra variación: ser el tamaño del tablero + número de barcos.
russau el

1

El total de un segundo tiempo juego de es específico de la máquina. Una vez que el segundo valor de las operaciones de la CPU sea diferente en mi máquina en comparación con la máquina del torneo. Si optimizo el algoritmo Battle Ship para utilizar la mayor cantidad de tiempo de CPU en 1 segundo, entonces se ejecuta en una posible máquina de torneo más lenta, siempre perderá.

No estoy seguro de cómo sortear esta limitación del marco, pero debería abordarse.

...

Una idea es hacer lo que se hizo en esta competencia http://www.bcsstudentcontest.com /

Y tenga un tiempo máximo por turno en lugar del tiempo total máximo del juego. De esta manera, podría limitar los algoritmos para que se ajusten dentro de un tiempo de giro conocido. Un juego puede durar más de 50 a 600 turnos, si el algoritmo my maneja su tiempo total de juego, podría no dar el tiempo suficiente para hacer su mejor trabajo o podría dar demasiado tiempo y perder. Es muy difícil administrar el tiempo total del juego dentro del algoritmo de Battleship.

Sugeriría cambiar las reglas para limitar el tiempo de turno, no el tiempo total del juego.

Editar

Si escribí un algoritmo que enumera todos los disparos posibles y luego los clasifica, toma el tiro de mayor rango. Llevaría demasiado tiempo generar todas las tomas posibles, por lo que dejaría que el algoritmo se ejecute durante un cierto tiempo y luego lo detenga.

Si hubiera un límite basado en turnos, podría dejar que el algoritmo se ejecute durante 0.9 segundos y devolver el tiro de mayor rango, y estar bien dentro del límite de tiempo de giro.

Si estoy limitado al tiempo total de juego de un segundo, será difícil determinar cuánto tiempo debe ejecutarse el algoritmo para cada turno. Querré maximizar el tiempo de mi CPU. Si un juego duró 500 rondas, podría limitar cada turno a 0.002 segundos, pero si un juego duró 100 rondas, podría dar a cada turno 0.01 segundos de tiempo de CPU.

No sería práctico para un algoritmo utilizar una búsqueda semi exhaustiva del espacio de disparo para encontrar el mejor disparo con la limitación actual.

El tiempo total de juego de 1 segundo está limitando el tipo de algoritmos que se pueden usar efectivamente para competir en el juego.


Esto se ejecutará en una máquina Intel Q9550SX de cuatro núcleos, 8 GB de RAM y Vista 64. ¿1 segundo va a ser un factor limitante?
John Gietzen el

Supongo que deberías hacer que tu acorazado AI sea multiproceso, entonces, para calcular el número máximo de disparos por ese intervalo de tiempo.
Jeff Atwood el

El truco es cómo limitar el intervalo de tiempo de giro. Si lo limito 0.00005 segundos, estoy a salvo de correr más allá del límite de tiempo, pero estoy limitando significativamente el espacio de búsqueda. Si aumento el límite de tiempo de giro, el espacio de búsqueda aumenta, pero corro el riesgo de quedarme sin tiempo.
TonyAbell 03 de

@TonyAbell: Si es importante tener un límite de tiempo por turnos, ¿por qué no comenzar con un valor inicial y luego ajustarlo de ronda en ronda? Después de aproximadamente la mitad de las rondas, lo más probable es que hayas encontrado la longitud de giro óptima para el oponente que estás enfrentando.
kyokley el

Debe realizar un seguimiento de su tiempo restante y limitarlo a la mitad del tiempo disponible restante.
John Gietzen el

1

Estoy haciendo frente aquí al no poner el código real, pero me arriesgaré a algunas observaciones generales:

  • Como todas las naves tienen un tamaño de al menos 2 celdas, puedes usar una optimización que vi en una implementación del juego en Space Quest V, que solo dispara a celdas alternativas en un patrón de diamante mientras está "buscando" un objetivo. Esto elimina la mitad de los cuadrados, al tiempo que garantiza que finalmente encontrarás todas las naves.
  • Un patrón de disparo aleatorio cuando se buscan objetivos estadísticamente producirá los mejores resultados en muchos juegos.

1

[Densidad de probabilidad] [1] ingrese la descripción de la imagen

! [ingrese la descripción de la imagen aquí] [2]

Experimenté comparando los resultados de los disparos de randon con una cacería / objetivo tonto y finalmente una búsqueda sofisticada.

La mejor solución parece ser crear una función de densidad de probabilidad para la probabilidad de que las naves restantes usen un cuadrado individual y apuntar con el cuadrado con el valor más alto.

Puedes ver mis resultados aquí ingresa la descripción del enlace aquí


¿Podría quizás arreglar su respuesta y especialmente sus imágenes y enlaces?
Bart

-2

"Battleship" es lo que se conoce como un clásico problema de NP completo de ciencias de la computación.

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

(busca Battleship - está ahí, debajo de juegos y rompecabezas)


44
Que es un rompecabezas de acorazado ( en.wikipedia.org/wiki/Battleship_(puzzle) ), no Battleship the game ( en.wikipedia.org/wiki/Battleship_(game) ).
Jason Berkan

Sí, como Jason declaró, este es un animal completamente diferente.
John Gietzen

3
Jejeje. La próxima tarea que tenga en el trabajo voy a decir que está completa NP, luego tomaré un largo almuerzo. :-)
Bork Blatt el
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.