Quelle est la meilleure IA de cuirassé?


315

Bataille navale!

En 2003 (quand j'avais 17 ans), j'ai participé à un concours de codage Battleship AI . Même si j'ai perdu ce tournoi, je me suis beaucoup amusé et j'en ai beaucoup appris.

Maintenant, je voudrais ressusciter cette compétition, à la recherche du meilleur cuirassé AI.

Voici le framework, désormais hébergé sur Bitbucket .

Le gagnant recevra une réputation de +450! Le concours se tiendra à partir du 17 novembre 2009 . Aucune inscription ou modification postérieure à zéro heure le 17 ne sera acceptée. (Heure normale du Centre) Soumettez vos entrées plus tôt afin de ne manquer aucune opportunité!

Pour garder cet OBJECTIF , veuillez suivre l'esprit de la compétition.

Règles du jeu:

  1. Le jeu se joue sur une grille 10x10.
  2. Chaque concurrent placera chacun de 5 navires (de longueurs 2, 3, 3, 4, 5) sur sa grille.
  3. Aucun navire ne peut se chevaucher, mais ils peuvent être adjacents.
  4. Les concurrents tirent ensuite à tour de rôle sur leur adversaire.
    • Une variante du jeu permet de tirer plusieurs coups par volée, un pour chaque navire survivant.
  5. L'adversaire informera le compétiteur si le tir coule, frappe ou manque.
  6. Le jeu se termine lorsque tous les navires d'un joueur sont coulés.

Règlement du concours:

  1. L'esprit de la compétition est de trouver le meilleur algorithme de cuirassé.
  2. Tout ce qui est jugé contraire à l'esprit de la compétition sera un motif de disqualification.
  3. Interférer avec un adversaire est contraire à l'esprit de la compétition.
  4. Le multithreading peut être utilisé sous les restrictions suivantes:
    • Pas plus d'un thread peut être en cours d'exécution alors que ce n'est pas votre tour. (Cependant, un certain nombre de threads peuvent être dans un état "Suspendu").
    • Aucun thread ne peut s'exécuter avec une priorité autre que "Normal".
    • Compte tenu des deux restrictions ci-dessus, vous serez garanti au moins 3 cœurs de processeur dédiés pendant votre tour.
  5. Une limite de 1 seconde de temps CPU par partie est allouée à chaque concurrent sur le thread principal.
  6. Le manque de temps entraîne la perte de la partie en cours.
  7. Toute exception non gérée entraînera la perte de la partie en cours.
  8. L'accès au réseau et l'accès au disque sont autorisés, mais vous pouvez trouver les restrictions de temps assez prohibitives. Cependant, quelques méthodes de montage et de démontage ont été ajoutées pour alléger la contrainte de temps.
  9. Le code doit être publié sur le débordement de pile comme réponse ou, s'il est trop volumineux, lié.
  10. La taille totale maximale (non compressée) d'une entrée est de 1 Mo.
  11. Officiellement, .Net 2.0 / 3.5 est la seule exigence du cadre.
  12. Votre entrée doit implémenter l'interface IBattleshipOpponent.

Notation:

  1. Les 51 meilleurs jeux sur 101 sont le vainqueur d'un match.
  2. Tous les concurrents joueront les uns contre les autres, à la manière d'un tournoi à la ronde.
  3. La meilleure moitié des concurrents jouera alors un tournoi à double élimination pour déterminer le vainqueur. (La plus petite puissance de deux supérieure ou égale à la moitié, en fait.)
  4. J'utiliserai le TournamentApi framework pour le tournoi.
  5. Les résultats seront affichés ici.
  6. Si vous soumettez plusieurs candidatures, seule votre candidature ayant obtenu le meilleur score est éligible au double élim.

Bonne chance! S'amuser!


EDIT 1:
Merci à Freed , qui a trouvé une erreur dans la Ship.IsValidfonction. Il a été corrigé. Veuillez télécharger la version mise à jour du framework.

EDIT 2:
Étant donné qu'il y a eu un intérêt significatif pour la persistance des statistiques sur le disque et autres, j'ai ajouté quelques événements de configuration et de suppression non chronométrés qui devraient fournir les fonctionnalités requises. Il s'agit d'un changement semi-révolutionnaire . C'est-à-dire: l'interface a été modifiée pour ajouter des fonctions, mais aucun corps n'est requis pour elles. Veuillez télécharger la version mise à jour du framework.

EDIT 3:
Bug Fix 1: GameWonet GameLostne recevaient un appel qu'en cas de temps mort.
Correction de bug 2: Si un moteur expirait à chaque partie, la compétition ne se terminerait jamais.
Veuillez télécharger la version mise à jour du framework.

EDIT 4:
Résultats du tournoi:


Si l'entrée nécessite une grande base de données, peut-elle s'y connecter via Internet? C'est à dire. l'entrée peut-elle effectuer des appels de service Web?
Remus Rusanu

y a-t-il une limite de taille sur les entrées?
Jherico

8
@Steven: De plus, j'ai consulté Jeff Atwood juste pour voir si c'était approprié. Voici sa réponse: twitter.com/codinghorror/status/5203185621
John Gietzen

1
J'ajouterais également du taht, étant donné que la composante aléatoire inévitable de ces 50 jeux ne sera pas suffisante pour distinguer avec précision entre de très bonnes implémentations. Je pense que 501 ou plus peut être nécessaire pour une vue raisonnable sur ce qui est mieux.
ShuggyCoUk

1
Un adversaire "pacifique" qui refuse de placer des navires fait suspendre la compétition. Je ne sais pas à quel point vous vous souciez des gens qui font des choses stupides comme ça. :)
Joe

Réponses:


56

J'appuie la motion pour faire beaucoup plus de matchs par match. Faire 50 jeux, c'est simplement lancer une pièce. J'ai dû faire 1000 jeux pour obtenir une distinction raisonnable entre les algorithmes de test.

Téléchargez Dreadnought 1.2 .

Stratégies:

  • garder une trace de toutes les positions possibles pour les navires qui ont> 0 coups sûrs. La liste ne dépasse jamais ~ 30K, elle peut donc être conservée exactement, contrairement à la liste de toutes les positions possibles pour tous les navires (qui est très grande).

  • L'algorithme GetShot comporte deux parties, l'une qui génère des tirs aléatoires et l'autre qui tente de terminer le naufrage d'un navire déjà touché. Nous effectuons des tirs aléatoires s'il existe une position possible (dans la liste ci-dessus) dans laquelle tous les navires touchés sont coulés. Sinon, nous essayons de terminer le naufrage d'un navire en choisissant un emplacement de tir qui élimine le plus de positions possibles (pondérées).

  • Pour les tirs aléatoires, calculez le meilleur emplacement pour tirer en fonction de la probabilité qu'un des navires non coulés chevauche l'emplacement.

  • algorithme adaptatif qui place les navires dans des endroits où l'adversaire est statistiquement moins susceptible de tirer.

  • algorithme adaptatif qui préfère tirer sur des endroits où l'adversaire est statistiquement plus susceptible de placer ses navires.

  • placez les navires la plupart du temps sans se toucher.


sur ma machine de test (un netbook ULV Celeron), ce code perd systématiquement par timeout. Quand je le laisse prendre tout le temps qu'il veut, il fouette Simple (taux de réussite d'environ 90%). Si vous comptez beaucoup sur les spécifications de la machine sur laquelle vous allez courir pour vous atteindre des délais, vous voudrez peut-être vous donner une marge de manœuvre ...
ShuggyCoUk

Intéressant ... Il fonctionne très bien sur la machine du tournoi. Cependant, un moteur "parfait" s'adapterait au temps qu'il avait déjà passé.
John Gietzen

35

Voici mon entrée! (La solution la plus naïve possible)

"Random 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 fait, cette réponse est agréable car elle montre sous une forme très concise les API que vous devez implémenter pour concurrencer ... :)
dicroce

1
À l'époque où j'ai construit un projet similaire dans ma classe d'algorithmes, j'ai utilisé une logique aléatoire entrelacée avec une prise de décision. C'était bon parfois!
Nathan Taylor

2
Cela pourrait tenter de placer des navires qui se chevauchent, n'est-ce pas?

6
Oui, mais le moteur l'interdira. Il dira alors à l'IA de les replacer, mais cette fois, d'une voix plus sévère. (Vu par pop ax \ cmp ax, 1 \ je stern)
John Gietzen

5
Remarque importante pour tous ceux qui, comme moi, ont pensé qu'ils pourraient facilement battre cela en se souvenant des coups précédemment placés et en ne les répétant pas. Le cadre ignorera les répétitions et vous donnera une autre chance tant que votre temps total est inférieur à la limite. C'est pauvre à mon avis, si quelqu'un gâche son algo, il devrait être pénalisé ...
ShuggyCoUk

22

Voici un adversaire contre lequel les gens peuvent jouer:

Au lieu d'utiliser une stratégie fixe inspirée de la géométrie, j'ai pensé qu'il serait intéressant d'essayer d' estimer les probabilités sous-jacentes qu'un espace inexploré particulier contienne un navire.

Pour ce faire, vous devez explorer toutes les configurations possibles de navires qui correspondent à votre vision actuelle du monde, puis calculer les probabilités en fonction de ces configurations. Vous pourriez penser à cela comme explorer un arbre:

une expansion des états possibles du cuirassé http://natekohl.net/media/battleship-tree.png

Après avoir considéré toutes les feuilles de cet arbre qui concordent avec ce que vous savez sur le monde (par exemple, les navires ne peuvent pas se chevaucher, tous les carrés touchés doivent être des navires, etc.), vous pouvez compter la fréquence à laquelle les navires se produisent à chaque position inexplorée pour estimer la probabilité que un navire est assis là.

Cela peut être visualisé comme une carte thermique, où les points chauds sont plus susceptibles de contenir des navires:

une carte thermique des probabilités pour chaque position inexplorée http://natekohl.net/media/battleship-probs.png

Une chose que j'aime dans cette compétition Battleship, c'est que l'arbre ci-dessus est presque assez petit pour forcer ce type d'algorithme. S'il y a ~ 150 positions possibles pour chacun des 5 navires, c'est 150 5 = 75 milliards de possibilités. Et ce nombre ne fait que diminuer, surtout si vous pouvez éliminer des navires entiers.

L'adversaire que j'ai lié ci-dessus n'explore pas l'arbre entier; 75 milliards sont encore trop gros pour entrer en moins d'une seconde. Il tente cependant d'estimer ces probabilités à l'aide de quelques heuristiques.


Jusqu'à présent, vous battez notre seule autre solution complète d'environ 67,7% à 32,3% :)
John Gietzen

2
Je suis vraiment curieux de voir comment une "approche probabiliste" se compare à une "approche géométrique". J'ai remarqué que cette probabilité que l'adversaire fasse des mouvements qui suivent les motifs géométriques discutés dans d'autres réponses. Il se pourrait que l'utilisation de la géométrie soit tout aussi bonne et beaucoup plus rapide. :)
Nate Kohl

12

Pas une réponse à part entière, mais il semble inutile d'encombrer les vraies réponses avec du code commun. Je présente donc quelques extensions / classes générales dans l'esprit open source. Si vous les utilisez, veuillez changer l'espace de noms ou essayer de tout compiler en une seule DLL ne fonctionnera pas.

BoardView vous permet de travailler facilement avec une carte annotée.

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

Certaines extensions, certaines de ces fonctionnalités dupliquées dans le cadre principal, mais devraient vraiment être faites par vous.

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

Quelque chose que j'utilise beaucoup.

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

Randomisation. Sécurisé mais testable, utile pour les tests.

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

Je n'ai pas le temps pour le moment d'écrire un algorithme à part entière, mais voici une pensée: si votre adversaire a placé des navires au hasard, les probabilités de placement ne seraient-elles pas une simple distribution centrée sur (5.5,5.5)? Par exemple, les possibilités de placement pour le cuirassé (5 unités de long) dans la dimension x sont ici:

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

Les mêmes calculs seraient valables pour y. Les autres navires n'auraient pas une distribution aussi abrupte, mais votre meilleure estimation est toujours le centre. Après cela, l'approche mathématique serait de rayonner lentement des diagonales (peut-être avec la longueur du navire moyen, 17/5) hors du centre. Ex:

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

Évidemment, il faudrait ajouter un peu de hasard à l'idée, mais je pense que purement mathématiquement c'est la voie à suivre.


Oui, en effet. Mon ancien moteur a compensé cela.
John Gietzen

1
D'où je viens, les diagonales rayonnant lentement hors du centre sont considérées comme de la triche .
bzlm

Si cela est considéré comme de la triche, il existe une contre-mesure assez simple. Évitez (x, y) où x = y. :)
ine

5
Je pense qu'il faisait allusion au comptage des cartes? Ce qui, à mon avis, ne triche pas.
John Gietzen

10

Rien de si sophistiqué, mais voici ce que j'ai trouvé. Il bat l'adversaire aléatoire 99,9% du temps. Serait intéressé si quelqu'un avait d'autres petits défis comme celui-ci, c'était très amusant.

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

Légèrement condensé pour occuper un minimum d'espace ici et être toujours lisible.


6

Quelques commentaires sur le moteur de compétition:

Paramètres NewGame:

Si IBattleshipOpponent :: NewGame est destiné à la configuration d'avant-jeu et prend une taille de planches, il devrait également prendre une liste des navires et leurs tailles respectives. Cela n'a aucun sens de permettre une taille de carte variable sans autoriser des configurations de navire variables.

Les navires sont scellés:

Je ne vois aucune raison pour laquelle la classe Ship est scellée. Entre autres choses de base, je voudrais que les navires aient un nom, afin que je puisse sortir des messages comme ("Vous avez coulé mon {0}", ship.Name); . J'ai d'autres extensions en tête, je pense donc que Ship devrait être héritable.

Délais:

Bien que la limite de temps de 1 seconde soit logique pour une règle de tournoi, elle gâche totalement le débogage. BattleshipCompetition devrait avoir un réglage facile pour ignorer les violations de temps afin d'aider au développement / débogage. Je suggérerais également d'enquêter sur System.Diagnostics.Process :: UserProcessorTime / Privileged ProcessorTime / TotalProcessorTime pour une vue plus précise de la quantité de temps utilisée.

Navires coulés:

L'API actuelle vous informe lorsque vous avez coulé le navire d'un oppenent:

ShotHit(Point shot, bool sunk);

mais pas quel navire vous avez coulé! Je considère que cela fait partie des règles humaines du cuirassé que vous devez déclarer "Vous avez coulé mon cuirassé!" (ou destructeur, ou sous-marin, etc.).

Cela est particulièrement critique lorsqu'une IA essaie de débusquer les navires qui se butent les uns contre les autres. Je voudrais demander un changement d'API pour:

ShotHit(Point shot, Ship ship);

Si le navire n'est pas nul, cela implique que le coup était un coup de naufrage, et vous savez quel navire vous avez coulé, et combien de temps il a été. Si le coup était un coup non coulant, le navire est nul et vous n'avez aucune autre information.


Veuillez poster des exemples de code si vous pensez que le timing peut être fait avec plus de précision. Je ne veux pas trop changer les règles en ce moment.
John Gietzen

De plus, les tailles de vaisseau sont transmises pendant le PlaceShips () qui est exécuté exactement une fois par partie et peut également être utilisé comme phase de configuration. N'hésitez pas à desceller le vaisseau pour vos propres tests, mais j'ai l'intention d'utiliser celui scellé pour le tournoi.
John Gietzen

BOGUE: @John Gietzen: J'ai déterminé que PlaceShips n'est PAS exécuté exactement une fois par partie (comme vous l'avez indiqué). Si un joueur place ses vaisseaux de manière incorrecte (comme le fait souvent le RandomOpponent), alors PlaceShips est appelé à plusieurs reprises, sans appel de NewGame.
abelenky

5
J'ai toujours considéré que c'était une stratégie de placer deux vaisseaux dans une configuration L pour faire croire à mon adversaire qu'il avait coulé un cuirassé alors qu'en fait ce n'était pas le cas. Je n'ai jamais eu l'impression que vous aviez à déclarer quel bateau avait coulé.
Josh Smeaton

3
@DJ: Je respecte les règles originales du stylo et du papier. N'oubliez pas que Hasbro est une société de jouets et que ce jeu est antérieur à Hasbro.
John Gietzen

5

CrossFire mis à jour. Je sais qu'il ne peut pas rivaliser avec Farnsworth ou Dreadnought mais il est beaucoup plus rapide que ce dernier et simple à jouer au cas où quelqu'un voudrait essayer. Cela dépend de l'état actuel de mes bibliothèques, incluses ici pour le rendre facile à utiliser.

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

C'est à peu près le meilleur que j'ai pu rassembler pendant mon temps libre, qui est à peu près inexistant. Il y a des statistiques de décompte des matchs et des matchs en cours, alors que je configure la fonction principale pour boucler et exécuter en continu le BattleshipCompetition jusqu'à ce que j'appuie sur une touche.

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

Cette logique est la plus proche que j'ai eue de battre Dreadnought, remportant environ 41% des matchs individuels. (Il a en fait remporté un match par un nombre de 52 à 49.) Curieusement, cette classe ne fait pas aussi bien contre FarnsworthOpponent qu'une version antérieure qui était beaucoup moins avancée.


5

Mon ordinateur est en cours de réparation par Dell en ce moment, mais c'est là que j'étais la semaine dernière:

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
Félicitations pour l'argent. Pourriez-vous décrire votre algorithme avec des mots? Ce serait intéressant à savoir.
Thomas Ahle

4

Si vous forcez brutalement votre analyse, vous pouvez trouver la mécanique du RandomOpponent fourni très inefficace. Il se permet de resélectionner des emplacements déjà ciblés et permet au framework de le forcer à répéter jusqu'à ce qu'il touche celui qu'il n'a pas encore touché ou que la limite de temps par mouvement expire.

Cet adversaire a un comportement similaire (la distribution de placement effective est la même), il vérifie lui-même et ne consomme qu'une génération de nombres aléatoires par appel (amorti)).

Cela utilise les classes dans ma réponse extensions / bibliothèque et je ne fournis que les méthodes / états clés.

Shuffle est retiré de la réponse de Jon Skeet ici

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

Je ne vais pas pouvoir participer, mais voici l'algorithme que j'implémenterais si j'avais le temps:

Tout d'abord, lorsque je détecte un coup, je ne poursuis pas le reste du navire immédiatement - je construis un tableau des emplacements des navires et je détermine si j'ai touché les cinq au moins une fois avant de commencer à les couler complètement. (Notez que c'est une mauvaise politique pour la variante à plusieurs coups - voir les commentaires)

  1. Frappez le centre (voir la note finale ci-dessous - «centre» est juste une commodité pour la description)
  2. Frappez le point 4 à droite du centre
  3. Frappez l'endroit 1 vers le bas et un à droite du centre
  4. Frappez le point quatre à droite du coup précédent
  5. Continuez dans ce schéma (devrait se terminer par des lignes diagonales séparées par 3 espaces remplissant le tableau) Cela devrait toucher tous les bateaux de 4 et 5 longueurs, et un nombre statistiquement élevé de 3 et 2 bateaux.

  6. Commencez à frapper au hasard des taches entre les diagonales, cela attrapera les bateaux de 2 et 3 longueurs qui n'ont pas déjà été remarqués.

Une fois que j'ai détecté 5 coups sûrs, je déterminerais si les 5 coups sûrs sont sur des bateaux séparés. Ceci est relativement facile en réalisant quelques tirs supplémentaires à proximité d'emplacements où deux coups sûrs sont sur la même ligne horizontale ou verticale et à moins de 5 emplacements l'un de l'autre (peut-être deux coups sûrs sur le même bateau). Si ce sont des bateaux séparés, continuez à couler tous les navires. S'ils s'avèrent être le même bateau, continuez les schémas de remplissage ci-dessus jusqu'à ce que les 5 bateaux soient localisés.

Cet algorithme est un algorithme de remplissage simple. Les principales caractéristiques sont qu'il ne perd pas de temps à couler les navires qu'il connaît lorsqu'il y a encore des navires dont il n'est pas au courant, et qu'il n'utilise pas un modèle de remplissage inefficace (c'est-à-dire qu'un modèle entièrement aléatoire serait du gaspillage).

Notes finales:

A) "Centre" est un point de départ aléatoire sur le plateau. Cela élimine la principale faiblesse de cet algorithme. B) Alors que la description indique de dessiner des diagonales immédiatement depuis le début, l'idéal est que l'algorithme ne tire qu'à des endroits «aléatoires» le long de ces diagonales. Cela aide à empêcher le concurrent de chronométrer combien de temps jusqu'à ce que leurs navires soient touchés par des schémas prévisibles.

Cela décrit un algorithme «parfait» en ce sens qu'il obtiendra tous les navires sous (9x9) / 2 + 10 coups.

Cependant, il peut être considérablement amélioré:

Une fois qu'un navire est touché, identifiez sa taille avant de faire les lignes diagonales «internes». Vous avez peut-être trouvé les 2 navires, auquel cas les diagonales internes peuvent être simplifiées pour trouver plus rapidement les 3 navires de taille.

Identifiez les étapes du jeu et agissez en conséquence. Cet algorithme peut être bon jusqu'à un certain point du jeu, mais d'autres algorithmes peuvent donner de meilleurs avantages dans le cadre de la phase finale. De plus, si l'autre joueur est très proche de vous vaincre, un autre algorithme pourrait mieux fonctionner - par exemple, un algorithme à haut risque pourrait échouer plus souvent, mais quand il fonctionne, il fonctionne rapidement et vous pouvez battre votre adversaire qui est plus proche de gagner que vous. .

Identifiez le style de jeu du concurrent - cela peut vous donner des indices sur la façon dont ils planifient le placement des navires (c.-à-d., Il est probable que leur propre algorithme identifie le plus rapidement comment ils placent leurs propres navires - si le seul outil dont vous disposez est un marteau, tout ressemble à un clou)

-Adam


La stratégie consistant à attendre de couler les navires jusqu'à ce que tous soient trouvés dépend fortement de la variation d'un coup par tour. Dans la version (nombre de vaisseaux survivants) - coups par tour, il est avantageux de couler les vaisseaux le plus rapidement possible afin de ralentir votre adversaire.
Jason Owen

4

Mon entrée.

Rien de très spécial et je n'ai pas eu le temps d'ajouter toutes les bonnes idées que j'avais.

Mais cela semble assez bien jouer. Nous verrons comment cela se passe en compétition:

(mettez-le dans un fichier Missouri.cset ajouté au projet.)

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

Et maintenant que j'ai soumis mon entrée, quelques statistiques approximatives: contre BP7 44% de victoires. / vs Dreadnought 20% gagne. / vs Farnsworth 42% gagne. C'était un projet amusant.
abelenky

2

Ce n'est pas minimax. En fait, après avoir placé les navires, chaque joueur ne peut-il pas jouer seul, ce qui lui a valu un certain nombre de tours pour couler tous les navires adverses? Celui qui a pris le moins de tours gagne.

Je ne pense pas qu'il existe de bonnes stratégies générales au-delà du naufrage des navires touchés et de la tentative de minimiser le nombre de tirs pour couvrir les autres endroits possibles où les navires pourraient se cacher.

Bien sûr, il pourrait y avoir des contre-stratégies pour tout ce qui n'est pas aléatoire. Mais je ne pense pas qu'il existe de bonnes stratégies contre tous les joueurs possibles.


1
Potentiellement, oui, ils pourraient jouer seuls. Ce n'est pas ainsi que cela se déroulera. Excellente idée, cependant. Dans cette compétition, je veux qu'il soit possible d'éviter statistiquement les tirs de votre adversaire.
John Gietzen

2
Je vois. En utilisant des données de jeux antérieurs contre le même adversaire, on pourrait peut-être s'adapter à lui?
ziggystar

2

En fait, je pense que le plus gros problème du puzzle est qu'il s'agit essentiellement de deux mouvements. Un mouvement consiste à placer vos navires, l'autre à trouver les navires ennemis (quelle que soit la segmentation de cette deuxième partie, en plus d'essayer de battre une horloge avec un facteur aléatoire, il suffit de «lancer votre algorithme»). Il n'y a aucun mécanisme pour essayer de déterminer puis contrer une stratégie ennemie, ce qui rend les compétitions similaires basées sur des séries successives de "ciseaux à papier de roche" assez intéressantes.

De plus, je pense que ce serait plus cool si vous spécifiez le jeu en tant que protocole réseau et fournissez ensuite le cadre pour implémenter ce protocole en C #, plutôt que de dicter que toutes les solutions doivent être C #, mais c'est juste mon avis.

EDIT: J'annule mon point de départ, car je n'ai pas lu les règles du concours suffisamment attentivement.


Toutes les solutions ne doivent pas être en C #. Je peux compiler et relier un assemblage séparé. De plus, vous devriez pouvoir contrer statistiquement votre adversaire.
John Gietzen

J #? peut être? Lol, jk. J'ai un framework TCP pour cela, mais ce tournoi doit se dérouler très rapidement.
John Gietzen

Pourquoi supposeriez-vous que la communication TCP entre deux processus sur la même machine ne serait pas incroyablement rapide?
Jherico

@Jherico: Si j'utilisais TCP, j'isolerais les moteurs sur leurs propres PC afin qu'ils puissent utiliser toutes les ressources CPU qu'ils voulaient.
John Gietzen,

Même ainsi, deux machines sur le même lan pourraient facilement terminer un jeu en moins d'une seconde, les frais généraux du réseau étant minimes
Jherico

2

J'ai toujours aimé commencer au milieu et m'éloigner de ce point en ne laissant pas plus d'un espace vide entre les autres points pour expliquer ce putain de sous-marin ... l'espace entre les tirs dépendait des navires coulés. si le B-ship était le dernier, les tirs devaient seulement laisser 4 espaces entre eux pour minimiser les tirs perdus


1
Alors ... j'ai juste besoin de rester loin du milieu? :)
darron

14
Vous devez également rester à l'écart des bords, car un coup de bord contient plus d'informations pour votre adversaire qu'un coup non-bord. Vous devez donc placer tous vos navires dans une région non intermédiaire et non périphérique. À moins que ce soit ce qu'ils attendent de vous.
Jherico

1
Si vous commencez par laisser 3 ou 4 espaces, vous aurez peut-être la chance de toucher le sub quand même. Sinon, revenez en arrière et essayez de combler les lacunes. Plus sur: somethinkodd.com/oddthinking/2009/10/29/battleship-strategy
Oddthinking

18
Le navire à deux trous n'est pas un putain de sous - marin , c'est un putain de bateau PT . Le sous-marin a trois trous. :)
corbeau

2

Un concours similaire a été organisé par le Dr James Heather de l'Université de Surrey au nom de la British Computer Society.

Des limitations ont été placées sur les ressources - à savoir le temps processeur maximum par tour, aucun état ne pouvait être stocké entre les mouvements, la taille maximale du tas imposée. Pour limiter le temps, l'IA pourrait soumettre un mouvement à tout moment dans le créneau horaire et il lui serait demandé de le faire à la fin du tour.

Très intéressant - voir plus sur: http://www.bcsstudentcontest.com/

Pourrait vous donner plus d'idées.


2

En l'état, la solution s'ouvre et s'exécute sans modification dans monodevelop sous Ubuntu 9.10 Linux


1

Tu as écrit:

  • Tout ce qui est jugé contraire à l'esprit de la compétition sera un motif de disqualification.
  • Interférer avec un adversaire est contraire à l'esprit de la compétition.

veuillez définir "contre l'esprit de la compétition" et "interférer avec un adversaire"?

Aussi - pour simplifier, je vous recommande:

  • interdire l'utilisation du CPU pendant le slot CPU de l'adversaire.
  • interdire le parallélisme des threads et donner à la place plus de secondes CPU sur un seul thread. Cela simplifiera la programmation de l'IA et ne blessera personne de toute façon qui est lié au processeur / à la mémoire.

PS - une question pour les post-docs CS qui se cachent ici: ce jeu n'est-il pas résoluble (c'est-à-dire qu'il existe une seule et meilleure stratégie?). oui, la taille de la planche et le nombre de pas rendent minimax et al obligatoires, mais je dois quand même me demander ... c'est loin de Go et d'échecs en complexité.


J'avais une réflexion en tête quand j'ai dit "Interférer". Je ne veux pas que les concurrents gagnent parce qu'ils ont fait tourner un autre moteur à mort.
John Gietzen

8
Je dirais que l'espionnage est une partie importante de la guerre moderne, donc réfléchir pour trouver les cibles serait idéal - après tout, c'était l'une des méthodes utilisées pendant la seconde guerre mondiale ...
Rowland Shaw

J'ai un cadre pour isoler les moteurs sur différents PC, communiquer sur TCP / IP, rendant la réflexion sans valeur. Cependant, en raison de mon nombre estimé d'entrées, cela rendrait la compétition trop longue.
John Gietzen

6
Je ne savais pas qu'ils avaient Reflection à l'époque!
Markus Nigbur

1

Je prédis que la personne qui réussit à inverser l'ingénierie aléatoire de son adversaire et de son schéma d'appel gagnera.

Je ne sais pas dans quelle mesure c'est probable.


Les adversaires ont la possibilité d'utiliser un CSPRNG.
John Gietzen

Bon point, même si j'admets que l'ingénierie inverse d'une telle graine dépasse de toute façon mon expertise. Je suppose que l'aspect le plus vulnérable serait l'algorithme de sélection du modèle de tir, mais même alors, vous ne gagneriez pas nécessairement beaucoup à le casser, car il n'y a aucun moyen de déplacer vos navires une fois le jeu commencé.
Triston Attridge

Lorsque je postulais pour un stage de recherche, nous avons écrit des programmes de cuirassés et avons concouru. En fixant des graines au hasard, c'est exactement comme ça que j'ai gagné X)
P Shved

1
En supposant un algorithme de placement de navire raisonnablement simple, j'imagine que l'on pourrait, après avoir reçu quelques coups sur différents navires, commencer à utiliser la plupart de son tour en boucle à travers toutes les graines aléatoires possibles (commençant probablement quelque part près de l'heure actuelle et se déplaçant vers l'avant / en arrière d'une étape ou deux) et voir lesquels génèrent des emplacements de navire compatibles avec les résultats observés.
Domenic

1

Il serait également vraisemblablement possible d'exécuter une série de ceux-ci avec des variations sur le jeu.

L'ajout de choses comme un avion 3D ou la possibilité de déplacer un seul navire au lieu de tirer pour un tour changerait probablement le jeu un peu.


2
Il y a la variante "salve". Où vous pouvez tirer autant de coups par tour qu'il vous reste de vaisseaux.
John Gietzen

Une variation intéressante aussi. Il me semble que je me souviens d'avoir joué à une version informatique qui avait également un avion. Il tirerait au hasard sur des emplacements du plateau adverse.
Glenn

autre variante: la taille du plateau + le nombre de navires.
russau

1

Le temps de jeu total d' une seconde est spécifique à la machine. Une fois que la seconde opération du CPU sera différente sur ma machine par rapport à la machine de tournoi. Si j'optimise l'algorithme de Battle Ship pour utiliser le plus de temps CPU en 1 seconde, alors il s'exécute sur une machine de tournoi potentiellement plus lente, il perdra toujours.

Je ne sais pas comment contourner cette limitation du cadre, mais il faudrait y remédier.

...

Une idée est de faire ce qui a été fait dans ce concours http://www.bcsstudentcontest.com /

Et ayez un temps maximum par tour par opposition au temps de jeu total maximum. De cette façon, je pouvais limiter les algorithmes pour s'adapter à un temps de tour connu. Un jeu peut durer de 50 à 600+ tours, si mon algorithme gère son temps de jeu total, il peut ne pas donner suffisamment de temps pour faire son meilleur travail ou il peut donner trop de temps et perdre. Il est très difficile de gérer le temps de jeu total dans l'algorithme Battleship.

Je suggérerais de changer les règles pour limiter le temps de tour et non le temps de jeu total.

Éditer

Si j'ai écrit un algorithme qui énumère tous les plans possibles, puis les classe, puis prend le plan le plus élevé. Il faudrait trop de temps pour générer toutes les prises de vue possibles, donc je laisserais l'algorithme fonctionner pendant un certain temps, puis je l'arrêterais.

S'il y avait une limite basée sur le tour, je pourrais laisser l'algorithme fonctionner pendant 0,9 seconde et retourner le tir le plus haut, et être bien avec la limite de temps de tour.

Si je suis limité au temps de jeu total d'une seconde, il sera difficile de déterminer la durée de fonctionnement de l'algorithme pour chaque tour. Je veux maximiser mon temps CPU. Si un jeu durait 500 tours, je pouvais limiter chaque tour à 0,002 seconde, mais si un jeu durait 100 tours, je pouvais donner à chaque tour 0,01 seconde de temps CPU.

Il ne serait pas pratique pour un algorithme d'utiliser une recherche semi-exhaustive de l'espace de prise de vue pour trouver la meilleure prise de vue avec la limitation actuelle.

Le temps total de jeu d'une seconde limite le type d'algorithmes qui peuvent être efficacement utilisés pour participer au jeu.


Il sera exécuté sur un processeur quadricœur Intel Q9550SX, 8 Go de RAM, une machine Vista 64. Une seconde va-t-elle être un facteur limitant?
John Gietzen

Je suppose que vous devriez faire de votre cuirassé AI multithread, alors, pour calculer le nombre maximal de tirs par cet intervalle de temps.
Jeff Atwood

l'astuce est de savoir comment limiter l'intervalle de temps de virage. Si je la limite à 0,00005 seconde, je ne risque pas de dépasser le délai imparti, mais je limite considérablement l'espace de recherche. Si j'augmente la limite de temps de rotation, l'espace de recherche est augmenté mais je risque de manquer de temps.
TonyAbell

@TonyAbell: S'il est important d'avoir une limite de temps au tour, pourquoi ne pas commencer par une valeur initiale et l'ajuster ensuite de tour en tour? Après environ la moitié des tours, vous aurez probablement trouvé la longueur de tour optimale pour l'adversaire auquel vous faites face.
kyokley

Vous devez garder une trace de votre temps restant et le limiter à la moitié du temps restant.
John Gietzen

1

Je m'arrête ici en ne mettant pas de code réel - mais je vais risquer quelques observations générales:

  • Étant donné que tous les navires ont au moins 2 cellules, vous pouvez utiliser une optimisation que j'ai vue sur une implémentation du jeu dans Space Quest V - qui ne tire que sur des cellules alternées en losange pendant qu'il "cherche" une cible. Cela élimine la moitié des carrés, tout en garantissant que vous trouverez éventuellement tous les navires.
  • Un modèle de tir aléatoire lors de la recherche de cibles donnera statistiquement les meilleurs résultats sur de nombreux jeux.

1

! [Densité de probabilité] [1] entrez la description de l'image

! [entrez la description de l'image ici] [2]

J'ai expérimenté en comparant les résultats du tir Randon vs une chasse / cible stupide et enfin une recherche sophistiquée.

La meilleure solution semble être de créer une fonction de densité de probabilité pour la probabilité d'utilisation d'un carré individuel par les navires restants, et de viser le carré avec la valeur la plus élevée.

Vous pouvez voir mes résultats ici entrez la description du lien ici


Pourriez-vous peut-être corriger votre réponse et surtout vos images et votre lien?
Bart

-2

"Battleship" est ce qui est connu comme un problème NP-complet informatique classique.

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

(recherchez Battleship - il est là, sous les jeux et les puzzles)


4
Ce qui est un puzzle de cuirassé ( en.wikipedia.org/wiki/Battleship_(puzzle) ), pas Battleship le jeu ( en.wikipedia.org/wiki/Battleship_(game) ).
Jason Berkan

Oui, comme Jason l'a déclaré, c'est un animal entièrement différent.
John Gietzen

3
Hehehe. Prochaine mission Je me mets au travail, je vais dire que c'est NP-complet, puis je prend un long déjeuner :-)
Bork Blatt
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.