Який найкращий ШІ?


315

Бортовий корабель!

Ще в 2003 році (коли мені було 17 років) я змагався у ШІ змаганні з кодування . Незважаючи на те, що я програв цей турнір, я дуже розважався і багато чому навчився.

Тепер я хотів би воскресити цей конкурс у пошуках кращого ШІ.

Ось основи, тепер розміщені на Bitbucket .

Переможець отримає +450 репутації! Змагання відбудуться 17 листопада 2009 року . Записи чи правки пізніше нульової години 17-го не приймаються. (Центральний стандартний час) Надішліть свої записи рано, щоб не пропустити свою можливість!

Щоб зберегти цю ЦЕЛЮ , будь ласка, дотримуйтесь духу змагань.

Правила гри:

  1. Гра проводиться по сітці 10х10.
  2. Кожен учасник розмістить кожен із 5 кораблів (довжиною 2, 3, 3, 4, 5) на своїй сітці.
  3. Жодне судно не може перекриватися, але вони можуть бути суміжними.
  4. Потім конкуренти по черзі вистрілюють поодинокі постріли в опонента.
    • Варіант гри дозволяє вистрілювати кілька пострілів за залп, по одному на кожен вижили корабель.
  5. Суперник сповістить учасника, якщо постріл затопить, б'є або промахнеться.
  6. Гра закінчується, коли всі кораблі будь-якого гравця потоплені.

Правила конкурсу:

  1. Дух змагань полягає у тому, щоб знайти найкращий алгоритм броненосця.
  2. Все, що буде визнано проти духу змагань, буде підставою для дискваліфікації.
  3. Втручання суперника проти духу змагань.
  4. Багатопотокове читання може використовуватися при таких обмеженнях:
    • Не більше однієї нитки може працювати, поки це не ваша черга. (Хоча будь-яка кількість потоків може перебувати у стані "Призупинено").
    • Жоден потік не може працювати з пріоритетом, окрім "Звичайний".
    • З огляду на два вищезгадані обмеження, вам будуть гарантовані щонайменше 3 виділені ядра CPU під час вашої черги.
  5. Кожному учаснику в основній темі надається обмеження в 1 секунді часу процесора на гру.
  6. Вичерпання часу призводить до втрати поточної гри.
  7. Будь-яке незроблене виняток призведе до втрати поточної гри.
  8. Доступ до мережі та доступ до дисків дозволено, але ви можете вважати обмеження часу досить забороненим. Однак для зменшення часового напруження було додано декілька методів налаштування та усунення.
  9. Код слід розміщувати на переповнюванні стека як відповідь або, якщо він занадто великий, пов'язаний.
  10. Максимальний загальний розмір (нестиснений) запису - 1 Мб.
  11. Офіційно .Net 2.0 / 3.5 - єдина рамкова вимога.
  12. У вашій записи має бути реалізований інтерфейс IBattleshipOpponent.

Оцінка:

  1. Найкраща 51 гра із 101 гри - переможець матчу.
  2. Усі учасники будуть грати зіставленими один проти одного, в стилі круглолиття.
  3. Найкраща половина учасників змагань відіграє турнір з подвійною елімінацією для визначення переможця. (Найменша потужність двох, яка фактично більша або дорівнює половині.)
  4. Я буду використовувати TournamentApi рамку для турніру.
  5. Результати будуть розміщені тут.
  6. Якщо ви подаєте більше одного запису, лише дві кращі результати мають право на подвійне виключення.

Удачі! Весело!


EDIT 1:
Дякую Фріду , який виявив помилку у Ship.IsValidфункції. Це було виправлено. Завантажте оновлену версію фреймворку.

EDIT 2:
Оскільки існує значна зацікавленість у збереженні статистики на диску та подібних даних, я додав кілька невчасних налаштувань та пошкоджень, які мають забезпечити необхідну функціональність. Це напіврозривна зміна . Тобто: інтерфейс змінено для додавання функцій, але тіло для них не потрібно. Завантажте оновлену версію фреймворку.

EDIT 3:
Виправлення помилок 1: GameWonі GameLostдзвонили лише у випадку тайм-ауту.
Виправлення помилок 2: Якби двигун призупиняв кожну гру, змагання ніколи не закінчиться.
Завантажте оновлену версію фреймворку.

EDIT 4:
Результати турніру:


Якщо для запису потрібна велика база даних, чи може вона підключитися до неї через мережу? Тобто чи може вхід робити телефонні дзвінки?
Рем Русану

чи є обмеження розміру в записах?
Jherico

8
@Steven: Крім того, я порадився з Джеффом Етвудом, щоб побачити, чи підходить це. Ось його відповідь: twitter.com/codinghorror/status/5203185621
Джон Гітцен

1
Також я додам, що, враховуючи неминучу випадкову складову цих 50 ігор, буде недостатньо, щоб точно розмежувати дуже хороші реалізації. Я думаю, що для розумного погляду на те, що краще, може знадобитися 501 або більше.
ShuggyCoUk

1
"Мирний" противник, який відмовляється розміщувати кораблі, спричиняє змагання. Не впевнений, наскільки ти дбаєш про те, щоб люди робили такі дурні речі. :)
Джо

Відповіді:


56

Я другий рух робити набагато більше ігор за матч. Робити 50 ігор - це просто гортати монету. Мені потрібно було зробити 1000 ігор, щоб отримати будь-яку розумну відмінність між тестовими алгоритмами.

Завантажте Dreadnought 1.2 .

Стратегії:

  • слідкуйте за всіма можливими позиціями для суден, які мають> 0 хітів. Список ніколи не перевищує ~ 30 К, тому його можна точно зберігати, на відміну від переліку всіх можливих позицій для всіх суден (що дуже велике).

  • Алгоритм GetShot складається з двох частин: одна, яка генерує випадкові постріли, а друга, яка намагається закінчити потоплення вже потрапляючого корабля. Ми робимо випадкові постріли, якщо є можлива позиція (зі списку вище), в якій затонули всі вражені кораблі. В іншому випадку ми намагаємося закінчити потоплення судна, вибравши місце для стрільби, в якому виключаються найбільш можливі позиції (зважені).

  • Для випадкових знімків обчисліть найкраще місце для зйомки, виходячи з ймовірності того, що один із непронурених суден перекриває місцеположення.

  • адаптивний алгоритм, який розміщує кораблі в місцях, де опонент статистично рідше стріляє.

  • адаптивний алгоритм, який вважає за краще стріляти в місцях, де опонент статистично більше шансів розмістити свої кораблі.

  • розміщуйте кораблі, переважно не торкаючись один одного.


на моїй тестовій машині (нетбук ULV Celeron) цей код послідовно втрачає час. Коли я дозволю йому займати весь час, він хоче, щоб він батоги простий (приблизно 90% успіху). Якщо ви сильно покладаєтесь на технічні характеристики машини, над якою ви будете працювати, щоб вдарити вас вчасно, ви, можливо, захочете віддати собі кімнату
хитатися

Цікаво ... Це добре працює на турнірній машині. Однак "ідеальний" двигун адаптувався б до того, скільки часу він вже витратив.
Джон Гітцен

35

Ось мій запис! (Найнаївніше можливе рішення)

"Випадковий 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
Насправді ця відповідь є приємною, оскільки вона в дуже стислій формі показує API, який вам потрібно буде впровадити, щоб конкурувати ... :)
dicroce

1
Ще коли я будував подібний проект у своєму класі коледжу з алгоритмів, я використовував випадкову логіку, переплетену з деяким прийняттям рішень. Іноді було добре!
Натан Тейлор

2
Це могло б спробувати розмістити судна, що перекриваються, чи не так?

6
Так, але двигун це заборонить. Тоді він скаже ШІ, щоб розмістити їх знову, але цього разу суворішим голосом. ( pop ax \ cmp ax, 1 \ je stern
Побачено

5
Важлива примітка foranyone, який, як і я, зрозумів, що вони можуть легко перемогти це, запам'ятавши раніше зроблені кадри і не повторюючись. Рамка буде ігнорувати повтори і дасть вам ще один шанс, якщо ваш загальний час буде меншим за ліміт. На мою думку, це погано, якщо хтось зіпсує своє альго, його слід покарати ...
ShuggyCoUk

22

Ось противник, проти якого люди можуть грати:

Замість того, щоб використовувати стратегію натхненної геометрії, я подумав, що було б цікаво спробувати оцінити основні ймовірності того, що будь-який конкретний невивчений простір містить корабель.

Для цього слід вивчити всі можливі конфігурації кораблів, які відповідають вашому поточному погляду на світ, а потім обчислити ймовірності на основі цих конфігурацій. Ви можете подумати про це, як вивчити дерево:

розширення можливих штатів лінкора http://natekohl.net/media/battleship-tree.png

Розглянувши всі листя того дерева, які стикаються з тим, що ви знаєте про світ (наприклад, кораблі не можуть перекриватися, всі пошкоджені квадрати повинні бути кораблями тощо), ви можете порахувати, наскільки часто кораблі трапляються на кожній незвіданій позиції, щоб оцінити ймовірність того, що там сидить корабель.

Це можна візуалізувати як теплову карту, де більші шанси на те, що містять кораблі:

теплова карта ймовірностей для кожної незвіданої позиції http://natekohl.net/media/battleship-probs.png

Одне, що мені подобається в цьому змаганні Battleship - це те, що дерево вгорі майже мало, щоб жорстоко застосовувати подібний алгоритм. Якщо для кожного з 5 кораблів є ~ 150 можливих позицій, це 150 5 = 75 мільярдів можливостей. І ця кількість стає лише меншою, особливо якщо ви можете усунути цілі кораблі.

Супротивник, якого я зв'язав вище, не досліджує все дерево; 75 мільярдів ще великі, щоб потрапити за секунду. Однак вони намагаються оцінити ці ймовірності за допомогою кількох евристик.


Поки ви
переможете

2
Мені напевно цікаво побачити, як "імовірнісний підхід" порівнюється з "геометричним підходом". Я помітив, що цей імовірний противник насправді робить рухи, які слідують геометричним зразкам, обговореним в інших відповідях. Можливо, використання геометрії так само добре, і набагато швидше. :)
Нейт Кол

12

Не повноцінна відповідь, але, здається, мало сенсу захаращувати реальні відповіді загальним кодом. Таким чином, я представляю деякі розширення / загальні класи в дусі відкритого коду. Якщо ви користуєтесь ними, то, будь ласка, змініть простір імен або намагаєтесь зібрати все в один dll не вийде.

BoardView дозволяє вам легко працювати з дошкою з примітками.

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

namespace Battleship.ShuggyCoUk
{
    public enum Compass
    {
        North,East,South,West
    }

    class Cell<T>
    {
        private readonly BoardView<T> view;
        public readonly int X;
        public readonly int Y;
        public T Data;
        public double Bias { get; set; }

        public Cell(BoardView<T> view, int x, int y) 
        { 
            this.view = view; this.X = x; this.Y = y; this.Bias = 1.0;  
        }

        public Point Location
        {
            get { return new Point(X, Y); }
        }

        public IEnumerable<U> FoldAll<U>(U acc, Func<Cell<T>, U, U> trip)
        {
            return new[] { Compass.North, Compass.East, Compass.South, Compass.West }
                .Select(x => FoldLine(x, acc, trip));
        }

        public U FoldLine<U>(Compass direction, U acc, Func<Cell<T>, U, U> trip)
        {
            var cell = this;
            while (true)
            {
                switch (direction)
                {
                    case Compass.North:
                        cell = cell.North; break;
                    case Compass.East:
                        cell = cell.East; break;
                    case Compass.South:
                        cell = cell.South; break;
                    case Compass.West:
                        cell = cell.West; break;
                }
                if (cell == null)
                    return acc;
                acc = trip(cell, acc);
            }
        }

        public Cell<T> North
        {
            get { return view.SafeLookup(X, Y - 1); }
        }

        public Cell<T> South
        {
            get { return view.SafeLookup(X, Y + 1); }
        }

        public Cell<T> East
        {
            get { return view.SafeLookup(X+1, Y); }
        }

        public Cell<T> West
        {
            get { return view.SafeLookup(X-1, Y); }
        }

        public IEnumerable<Cell<T>> Neighbours()
        {
            if (North != null)
                yield return North;
            if (South != null)
                yield return South;
            if (East != null)
                yield return East;
            if (West != null)
                yield return West;
        }
    }

    class BoardView<T>  : IEnumerable<Cell<T>>
    {
        public readonly Size Size;
        private readonly int Columns;
        private readonly int Rows;

        private Cell<T>[] history;

        public BoardView(Size size)
        {
            this.Size = size;
            Columns = size.Width;
            Rows = size.Height;
            this.history = new Cell<T>[Columns * Rows];
            for (int y = 0; y < Rows; y++)
            {
                for (int x = 0; x < Rows; x++)
                    history[x + y * Columns] = new Cell<T>(this, x, y);
            }
        }

        public T this[int x, int y]
        {
            get { return history[x + y * Columns].Data; }
            set { history[x + y * Columns].Data = value; }
        }

        public T this[Point p]
        {
            get { return history[SafeCalc(p.X, p.Y, true)].Data; }
            set { this.history[SafeCalc(p.X, p.Y, true)].Data = value; }
        }

        private int SafeCalc(int x, int y, bool throwIfIllegal)
        {
            if (x < 0 || y < 0 || x >= Columns || y >= Rows)
            {    if (throwIfIllegal)
                    throw new ArgumentOutOfRangeException("["+x+","+y+"]");
                 else
                    return -1;
            }
            return x + y * Columns;
        }

        public void Set(T data)
        {
            foreach (var cell in this.history)
                cell.Data = data;
        }

        public Cell<T> SafeLookup(int x, int y)
        {
            int index = SafeCalc(x, y, false);
            if (index < 0)
                return null;
            return history[index];
        }

        #region IEnumerable<Cell<T>> Members

        public IEnumerator<Cell<T>> GetEnumerator()
        {
            foreach (var cell in this.history)
                yield return cell;
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return this.GetEnumerator();
        }

        public BoardView<U> Transform<U>(Func<T, U> transform)
        {
            var result = new BoardView<U>(new Size(Columns, Rows));
            for (int y = 0; y < Rows; y++)
            {
                for (int x = 0; x < Columns; x++)
                {
                    result[x,y] = transform(this[x, y]);
                }
            }
            return result;
        }

        public void WriteAsGrid(TextWriter w)
        {
            WriteAsGrid(w, "{0}");
        }

        public void WriteAsGrid(TextWriter w, string format)
        {
            WriteAsGrid(w, x => string.Format(format, x.Data));
        }

        public void WriteAsGrid(TextWriter w, Func<Cell<T>,string> perCell)
        {
            for (int y = 0; y < Rows; y++)
            {
                for (int x = 0; x < Columns; x++)
                {
                    if (x != 0)
                        w.Write(",");
                    w.Write(perCell(this.SafeLookup(x, y)));
                }
                w.WriteLine();
            }
        }

        #endregion
    }
}

Деякі розширення, частина цього дублює функціональність в основній базі, але насправді слід виконувати ви.

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

namespace Battleship.ShuggyCoUk
{
    public static class Extensions
    {        
        public static bool IsIn(this Point p, Size size)
        {
            return p.X >= 0 && p.Y >= 0 && p.X < size.Width && p.Y < size.Height;
        }

        public static bool IsLegal(this Ship ship,
            IEnumerable<Ship> ships, 
            Size board,
            Point location, 
            ShipOrientation direction)
        {
            var temp = new Ship(ship.Length);
            temp.Place(location, direction);
            if (!temp.GetAllLocations().All(p => p.IsIn(board)))
                return false;
            return ships.Where(s => s.IsPlaced).All(s => !s.ConflictsWith(temp));
        }

        public static bool IsTouching(this Point a, Point b)
        {
            return (a.X == b.X - 1 || a.X == b.X + 1) &&
                (a.Y == b.Y - 1 || a.Y == b.Y + 1);
        }

        public static bool IsTouching(this Ship ship,
            IEnumerable<Ship> ships,
            Point location,
            ShipOrientation direction)
        {
            var temp = new Ship(ship.Length);
            temp.Place(location, direction);
            var occupied = new HashSet<Point>(ships
                .Where(s => s.IsPlaced)
                .SelectMany(s => s.GetAllLocations()));
            if (temp.GetAllLocations().Any(p => occupied.Any(b => b.IsTouching(p))))
                return true;
            return false;
        }

        public static ReadOnlyCollection<Ship> MakeShips(params int[] lengths)
        {
            return new System.Collections.ObjectModel.ReadOnlyCollection<Ship>(
                lengths.Select(l => new Ship(l)).ToList());       
        }

        public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> source, Rand rand)
        {
            T[] elements = source.ToArray();
            // Note i > 0 to avoid final pointless iteration
            for (int i = elements.Length - 1; i > 0; i--)
            {
                // Swap element "i" with a random earlier element it (or itself)
                int swapIndex = rand.Next(i + 1);
                T tmp = elements[i];
                elements[i] = elements[swapIndex];
                elements[swapIndex] = tmp;
            }
            // Lazily yield (avoiding aliasing issues etc)
            foreach (T element in elements)
            {
                yield return element;
            }
        }

        public static T RandomOrDefault<T>(this IEnumerable<T> things, Rand rand)
        {
            int count = things.Count();
            if (count == 0)
                return default(T);
            return things.ElementAt(rand.Next(count));
        }
    }
}

Щось я закінчую багато.

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

Рандомізація. Безпечний, але перевірений, корисний для тестування.

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

namespace Battleship.ShuggyCoUk
{
    public class Rand
    {
        Random r;

        public Rand()
        {
            var rand = System.Security.Cryptography.RandomNumberGenerator.Create();
            byte[] b = new byte[4];
            rand.GetBytes(b);
            r = new Random(BitConverter.ToInt32(b, 0));
        }

        public int Next(int maxValue)
        {
            return r.Next(maxValue);
        }

        public double NextDouble(double maxValue)
        {
            return r.NextDouble() * maxValue;
        }

        public T Pick<T>(IEnumerable<T> things)
        {
            return things.ElementAt(Next(things.Count()));
        }

        public T PickBias<T>(Func<T, double> bias, IEnumerable<T> things)
        {
            double d = NextDouble(things.Sum(x => bias(x)));
            foreach (var x in things)
            {
                if (d < bias(x))
                    return x;
                d -= bias(x);                
            }
            throw new InvalidOperationException("fell off the end!");
        }
    }
}

10

Зараз у мене немає часу написати повноцінний алгоритм, але ось думка: якби ваш опонент розмістив кораблі випадковим чином, чи не були б ймовірності розміщення простою дистрибуцією в центрі (5.5,5.5)? Наприклад, можливості розміщення лінкора (довжиною 5 одиниць) у розмірі x є тут:

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

Такі ж розрахунки були б справедливими і для y. Інші кораблі не мали б такого крутого розповсюдження, але найкраща здогадка все-таки є центром. Після цього математичний підхід повільно випромінює діагоналі (можливо, з довжиною середнього корабля, 17/5) поза центром. Наприклад:

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

Очевидно, до ідеї потрібно було б додати деяку випадковість, але я вважаю, що саме математично це шлях.


Так, справді вони б. Мій старий двигун це компенсував.
Джон Гітцен

1
Звідки я родом, повільно випромінювання діагоналей із центру вважається обманом .
bzlm

Якщо це вважається обманом, є досить легкий контрзахід. Уникайте (x, y), де x = y. :)
ine

5
Я думаю, що він натякав на підрахунок карт? Що, на мою думку, не обман.
Джон Гітцен

10

Нічого такого витонченого, але ось те, що я придумав. Він перемагає випадкового опонента 99,9% часу. Було б цікаво, якщо у когось є якісь інші невеликі виклики, як це, це було весело.

namespace Battleship
{
    using System;
    using System.Collections.ObjectModel;
    using System.Drawing;
    using System.Collections.Generic;
    using System.Linq;
    public class AgentSmith : IBattleshipOpponent
    {        
        public string Name { get { return "Agent Smith"; } }
        public Version Version { get { return this.version; } }
        private Random rand = new Random();
        private Version version = new Version(2, 1);
        private Size gameSize;
        private enum Direction { Up, Down, Left, Right }
        private int MissCount;
        private Point?[] EndPoints = new Point?[2];
        private LinkedList<Point> HitShots = new LinkedList<Point>();
        private LinkedList<Point> Shots = new LinkedList<Point>();
        private List<Point> PatternShots = new List<Point>();
        private Direction ShotDirection = Direction.Up;
        private void NullOutTarget()
        {
            EndPoints = new Point?[2];
            MissCount = 0;
        }
        private void SetupPattern()
        {
            for (int y = 0; y < gameSize.Height; y++)
                for (int x = 0; x < gameSize.Width; x++)
                    if ((x + y) % 2 == 0) PatternShots.Add(new Point(x, y));
        }
        private bool InvalidShot(Point p)
        {
            bool InvalidShot = (Shots.Where(s => s.X == p.X && s.Y == p.Y).Any());
            if (p.X < 0 | p.Y<0) InvalidShot = true;
            if (p.X >= gameSize.Width | p.Y >= gameSize.Height) InvalidShot = true;
            return InvalidShot;
        }
        private Point FireDirectedShot(Direction? direction, Point p)
        {
            ShotDirection = (Direction)direction;
            switch (ShotDirection)
            {
                case Direction.Up: p.Y--; break;
                case Direction.Down: p.Y++; break;
                case Direction.Left: p.X--; break;
                case Direction.Right: p.X++; break;
            }
            return p;
        }
        private Point FireAroundPoint(Point p)
        {
            if (!InvalidShot(FireDirectedShot(ShotDirection,p)))
                return FireDirectedShot(ShotDirection, p);
            Point testShot = FireDirectedShot(Direction.Left, p);
            if (InvalidShot(testShot)) { testShot = FireDirectedShot(Direction.Right, p); }
            if (InvalidShot(testShot)) { testShot = FireDirectedShot(Direction.Up, p); }
            if (InvalidShot(testShot)) { testShot = FireDirectedShot(Direction.Down, p); }
            return testShot;
        }
        private Point FireRandomShot()
        {
            Point p;
            do
            {
                if (PatternShots.Count > 0)
                    PatternShots.Remove(p = PatternShots[rand.Next(PatternShots.Count)]);
                else do
                    {
                        p = FireAroundPoint(HitShots.First());
                        if (InvalidShot(p)) HitShots.RemoveFirst();
                    } while (InvalidShot(p) & HitShots.Count > 0);
            }
            while (InvalidShot(p));
            return p;
        }
        private Point FireTargettedShot()
        {
            Point p;
            do
            {
                p = FireAroundPoint(new Point(EndPoints[1].Value.X, EndPoints[1].Value.Y));
                if (InvalidShot(p) & EndPoints[1] != EndPoints[0])
                    EndPoints[1] = EndPoints[0];
                else if (InvalidShot(p)) NullOutTarget();
            } while (InvalidShot(p) & EndPoints[1] != null);
            if (InvalidShot(p)) p = FireRandomShot();
            return p;
        }
        private void ResetVars()
        {
            Shots.Clear();
            HitShots.Clear();
            PatternShots.Clear();
            MissCount = 0;
        }
        public void NewGame(Size size, TimeSpan timeSpan)
        {
            gameSize = size;
            ResetVars();
            SetupPattern();
        }
        public void PlaceShips(ReadOnlyCollection<Ship> ships)
        {
            foreach (Ship s in ships)
                s.Place(new Point(rand.Next(this.gameSize.Width), rand.Next(this.gameSize.Height)), (ShipOrientation)rand.Next(2));
        }
        public Point GetShot()
        {
            if (EndPoints[1] != null) Shots.AddLast(FireTargettedShot());
            else Shots.AddLast(FireRandomShot());
            return Shots.Last();
        }
        public void ShotHit(Point shot, bool sunk)
        {            
            HitShots.AddLast(shot);
            MissCount = 0;
            EndPoints[1] = shot;
            if (EndPoints[0] == null) EndPoints[0] = shot;
            if (sunk) NullOutTarget();
        }
        public void ShotMiss(Point shot)
        {
            if (++MissCount == 6) NullOutTarget();
        }
        public void GameWon() { }
        public void GameLost() { }
        public void NewMatch(string opponent) { }
        public void OpponentShot(Point shot) { }
        public void MatchOver() { }
    }
}

Трохи ущільнений, щоб зайняти тут мінімальний простір, і все ще читати.


6

Деякі зауваження щодо конкурента:

Параметри NewGame:

Якщо IBattleshipOpponent :: NewGame призначений для налаштування перед грою та має розмір дошки, він також повинен приймати список суден та їх відповідні розміри. Немає сенсу допускати змінний розмір плати, не допускаючи змінних конфігурацій корабля.

Кораблі запечатані:

Я не бачу жодної причини, чому класовий корабель запечатаний. Серед інших основних речей, я хотів би, щоб кораблі мали ім'я, щоб я могла виводити повідомлення, як ("Ви потопили мій {0}", ship.Name); . Я маю на увазі й інші розширення, тому вважаю, що Корабель має бути спадковим.

Ліміти часу:

У той час як обмеження часу в 1 секунду має сенс для правила турніру, воно повністю переплутається з налагодженням. BattleshipCom конкуренція повинна мати просте налаштування, щоб ігнорувати порушення часу, щоб сприяти розробці / налагодженню. Я б також запропонував дослідити System.Diagnostics.Process :: UserProcessorTime / Privileged ProcessorTime / TotalProcessorTime для більш точного уявлення про те, скільки часу використовується.

Затонулі кораблі:

Поточний API повідомляє вас, коли ви потопили корабель операнта:

ShotHit(Point shot, bool sunk);

але ні корабель, який ти потопив! Я вважаю це частиною правил "Бойового корабля", які ви зобов'язані оголосити "Ви потопили мій Лінкор!" (або есмінець, або підрозділ тощо).

Особливо це важливо, коли AI намагається збити кораблі, які стикаються один з одним. Я хочу попросити зміни в API:

ShotHit(Point shot, Ship ship);

Якщо судно не є нульовим, це означає, що постріл був затонулим, і ви знаєте, на якому судні ви потопили і як довго це було. Якщо постріл був пострілом, що не тоне, тоді корабель є нульовим, і ви не маєте більше інформації.


Будь ласка, розмістіть зразки коду, якщо ви вважаєте, що терміни можна зробити більш точно. Я зараз не хочу занадто сильно змінювати правила.
Джон Гітцен

Крім того, розміри корабля передаються під час PlaceShips (), який запускається рівно один раз за гру і також може використовуватися як етап налаштування. Будь ласка, не соромтеся розкрити судно для власного тестування, але я планую використовувати запечатаний для турніру.
Джон Гітцен

БУГ: @John Gietzen: Я визначив, що PlaceShips НЕ працює точно один раз за гру (як ви заявили). Якщо гравець розміщує свої кораблі неправильно (як це часто робить RandomOpponent), то PlaceShips викликається повторно, без втручання NewGame.
abelenky

5
Я завжди вважав стратегією розміщення двох кораблів у конфігурації L, щоб змусити мого опонента думати, що вони потопили лінкор, а насправді цього не зробили. Я ніколи не мав враження, що вам доведеться заявити, який човен затонув.
Джош Смітон

3
@DJ: Я йду за оригінальними правилами ручки та паперу. Пам'ятайте, що Hasbro - компанія з іграшками, і що ця гра передує Hasbro.
Джон Гітцен

5

Оновлено CrossFire. Я знаю, що він не може конкурувати з Фарнсворт або Дредноутом, але це набагато швидше, ніж останній, і легко грати з ним, якщо хтось захоче спробувати. Це покладається на поточний стан моїх бібліотек, включений сюди, щоб зробити його легким у використанні.

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

Це про найкраще, що я міг би зібрати у вільний час, а це про неіснуюче. Триває певна статистика гри та матчів, коли я налаштовував основну функцію на циклічне та безперервне запуск BattleshipComtification, поки не натиснув клавішу.

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

Ця логіка є найближчою, що мені довелося перемогти Дредноута, вигравши близько 41% окремих ігор. (Насправді він виграв один матч підрахунком від 52 до 49.) Як не дивно, цей клас не дуже добре проти FarnsworthOpponent як більш рання версія, яка була набагато менш розвиненою.


5

Зараз мій комп’ютер ремонтується Dell, але ось я був на минулому тижні:

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
Вітаю срібло. Ви не проти описувати свій алгоритм словами? Було б цікаво про це знати.
Томас Ейл

4

Якщо ви жорстоко змушуєте свій аналіз, то ви можете виявити механіку поставленого RandomOpponent дуже неефективною. Це дозволяє собі переосмислити вже націлені місця та дозволяє рамці змусити його повторюватись, поки не потрапить на той, якого він ще не торкнувся, або не закінчиться термін дії за крок.

У цього опонента схожа поведінка (ефективний розподіл місць розташування однаковий), він просто перевіряє правильність і споживає лише одне генерування випадкових чисел на виклик (амортизоване).

Для цього використовуються класи в моїх розширеннях / відповіді бібліотеки, і я надаю лише ключові методи / стан.

Перемішання знято з відповіді Джона Скіта тут

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

Я не можу брати участь, але ось алгоритм, який я б застосував, якби встиг:

По-перше, коли я виявляю удар, я не переслідую решту корабля негайно - будую таблицю місць розташування корабля і з'ясовую, чи вдарив я всіх п'яти хоча б раз, перш ніж почати повністю їх занурювати. (Зауважте, що це неправильна політика щодо варіанту з декількома знімками - див. Коментарі)

  1. Натисніть на центр (див. Заключну примітку нижче - "центр" - це лише зручність для опису)
  2. Вдарте місце 4 праворуч від центру
  3. Ударте місце 1 вниз і одне праворуч від центру
  4. Ударте місце чотири праворуч від попереднього хіта
  5. Продовжуйте цю схему (має закінчуватися діагональними лініями, розділеними на 3 пробіли, що заповнюють дошку). Це повинно вражати всі човни 4 і 5 довжини та статистично велику кількість 3 та 2 човна.

  6. Почніть випадково вдаряти плями між діагоналями, це підчепить човни довжиною 2 та 3, які ще не помітили.

Як тільки я виявив 5 звернень, я визначив, чи є 5 хітів на окремих човнах. Це порівняно легко, зробивши ще кілька пострілів поблизу місць, де два влучення знаходяться на одній горизонтальній чи вертикальній лінії та знаходяться в межах 5 розташування один одного (можливо, два удари в один човен). Якщо це окремі човни, то продовжуйте тонути всі кораблі. Якщо вони виявляться одним і тим же судном, продовжуйте схеми наповнення вище, поки не знайдуться всі 5 човнів.

Цей алгоритм є простим алгоритмом заповнення. Ключові особливості полягають у тому, що він не витрачає час на занурення суден, про які знає, коли ще є судна, про які він не знає, і він не використовує неефективну схему заповнення (тобто повністю випадкова модель буде марною).

Підсумкові зауваження:

А) "Центр" - випадкова відправна точка на дошці. Це усуває первинну слабкість цього алгоритму. Б) Хоча опис вказує на малювання діагоналей безпосередньо з самого початку, в ідеалі алгоритм просто стріляє в "випадкові" місця, розташовані уздовж цих діагоналей. Це допомагає не допустити часу, поки конкурент не вразить передбачувані схеми.

Це описує "ідеальний" алгоритм, в якому він отримуватиме всі кораблі під (9x9) / 2 + 10 пострілів.

Однак її можна значно покращити:

Після удару корабля визначте його розмір, перш ніж робити «внутрішні» діагональні лінії. Можливо, ви знайшли 2 корабля, і тоді внутрішні діагоналі можна спростити, щоб швидше знайти кораблі 3 розміру.

Визначте етапи гри та дійте відповідно. Цей алгоритм може бути хорошим до певного моменту гри, але інші алгоритми можуть принести кращі переваги у складі ендгра. Крім того, якщо інший гравець дуже близький до перемоги над вами, інший алгоритм може працювати краще - наприклад, алгоритм високого ризику може виходити з ладу частіше, але коли він працює, він працює швидко, і ви можете перемогти опонента, який ближче до перемоги, ніж ви .

Визначте стиль гри конкурента - він може дати вам підказки щодо того, як вони планують розміщення корабля (тобто, великі шанси, що їхній власний алгоритм найшвидше визначає, як вони розміщують власні кораблі - якщо єдиним інструментом у вас є молоток, все схоже на цвях)

-Адам


Стратегія очікування потоплення кораблів, поки всі знайдеться, значною мірою залежить від варіації "один постріл за поворот". За версією (кількість вижили кораблів) - удари за поворот, вигідніше топити кораблі якомога швидше, щоб уповільнити опонента.
Джейсон Оуен

4

Мій запис.

Нічого страшного особливого, і я не встиг додати всіх хороших ідей, які мав.

Але це, здається, грає досить добре. Ми побачимо, як це відбувається у конкуренції:

(помістіть це у файл Missouri.csта додайте до проекту.)

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

І тепер, коли я подав свою заявку, деякі грубі статистики: проти BP7 44% виграє. / проти Dreadnought 20% виграшів. / проти Фарнсворта 42% виграє. Це був веселий проект.
абеленький

2

Це не мінімакс. Насправді після розміщення кораблів, чи не може кожен гравець грати самостійно, в результаті чого кількість поворотів знадобилося йому затопити кожен супротивник? Виграє той, хто зайняв менше оборотів.

Я не думаю, що існують якісь хороші загальні стратегії, окрім того, що затоплюють уражені кораблі та намагаються мінімізувати кількість пострілів, щоб покрити інші можливі місця, де кораблі можуть ховатися.

Звичайно, можуть існувати контрстратегії для всього, що не є випадковим. Але я не думаю, що існують стратегії, які допомагають проти всіх можливих гравців.


1
Потенційно, так, вони могли грати самостійно. Це не буде так. Хоча чудова ідея. У цьому змаганні я хочу, щоб було можливо статистично уникнути пострілів опонента.
Джон Гітцен

2
Розумію. Використовуючи дані попередніх ігор проти того ж суперника, хтось може бути в змозі адаптуватися до нього?
ziggystar

2

Насправді, я думаю, що найбільша проблема головоломки полягає в тому, що це по суті два ходи. Один крок - розміщення ваших кораблів, другий - пошук кораблів противника (як би сегментована ця друга частина була, окрім спроб бити годинник випадковим фактором, його просто "запустіть алгоритм"). Немає механізму, який би намагався визначити, а потім протиставити ворожій стратегії, саме це робить подібні змагання, засновані на послідовних раундах "ножиць з паперової папери", досить цікавими.

Крім того, я думаю, було б крутіше, якби ви вказали гру як мережевий протокол, а потім надали основу для реалізації цього протоколу в C #, а не диктували, що всі рішення повинні бути C #, але це лише моя думка.

EDIT: Я відміняю свою початкову точку, оскільки я не прочитав досить уважно правила змагань.


Не всі рішення повинні бути в C #. Я можу скласти і посилання в окрему збірку. Крім того, ви повинні мати змогу статистично протидіяти опоненту.
Джон Гітцен

J #? може бути? Lol, jk. У мене є рамки TCP для цього, але цей турнір потрібно провести дуже швидко.
Джон Гітцен

Чому ви вважаєте, що зв'язок TCP між двома процесами на одній машині не буде надзвичайно швидким?
Jherico

@Jherico: Якби я використовував TCP, я би виділяв двигуни на своїх ПК, щоб вони могли використовувати будь-які ресурси CPU, які хотіли.
Джон Гітцен

Незважаючи на це, дві машини на одному і тому ж ланці могли легко завершити гру за секунду, а мережа накладних витрат мінімальна
Jherico

2

Мені завжди подобалося починати посередині і виходити спірально від тієї однієї точки, залишаючи не більше 1 порожнього простору між будь-якими іншими точками, щоб враховувати цей чортовий підвод ... простір між пострілами залежало від того, які кораблі були затонулі. якщо корабель B був останнім, постріли повинні були залишати лише 4 місця між ними, щоб мінімізувати витрачені постріли


1
Отже ... мені просто потрібно триматися подалі від середини? :)
darron

14
Вам також потрібно триматися подалі від країв, оскільки удар по краю містить більше інформації для опонента, ніж некрайковий удар. Тож вам слід розмістити всі ваші кораблі в несередині, некрайовому регіоні. Якщо тільки це вони не очікують від вас.
Jherico

1
Якщо ви почнете, залишивши 3 або 4 пробіли, вам, можливо, пощастить потрапити на підрядник. Якщо ні, поверніться і спробуйте заповнити прогалини. Більше за адресою: somethinkodd.com/oddthinking/2009/10/29/battleship-strategy
Відмінні думки

18
Судно з двома отворами НЕ проклятий до південь , це проклята PT човна . Підвод має три отвори. :)
ворон

2

Подібний конкурс проводив доктор Джеймс Хізер з Університету Суррей від імені Британського комп'ютерного товариства.

Обмеження були розміщені на ресурсах, а саме - максимальний час процесора на оборот, жоден стан не міг зберігатися між ходами, накладений максимальний розмір купи. Щоб обмежити час, AI міг подати хід у будь-яку точку протягом часового проміжку, і йому буде запропоновано переміститись після припинення повороту.

Дуже цікаво - детальніше дивіться на сайті: http://www.bcsstudentcontest.com/

Можливо, вам дадуть ще кілька ідей.


2

Як і раніше, рішення відкривається і працює без модифікації monodevelop в Linux Linux ubuntu 9.10


1

Ти написав:

  • Все, що буде визнано проти духу змагань, буде підставою для дискваліфікації.
  • Втручання суперника проти духу змагань.

будь ласка, визначте "проти духу змагань" та "втручання у опонента"?

Також для спрощення рекомендую вам:

  • заборонити взагалі використовувати процесор під час слота противника.
  • заборонити паралелізм потоку і замість цього дати більше CPU секунд на одному потоці. Це спростить програмування AI і не зашкодить нікому, хто пов'язаний з процесором / пам'яттю.

PS - питання до CS-документів, що тут ховаються: чи не вирішується ця гра (тобто є єдина, найкраща стратегія?). так, розмір дошки та кількість кроків робить minimax та ін обов'язковими, але все ж мені цікаво ... це далеко не Go і chess у складності.


Я мав на увазі роздуми, коли говорив "Втручаючись". Я не хочу, щоб конкуренти перемагали, тому що вони кусали ще один двигун до смерті.
Джон Гітцен

8
Я б припустив, що шпигунство є важливою частиною сучасної війни, тому розміщення цілей було б ідеальним - адже це був один із методів, які використовувались під час другої світової війни ...
Роуленд Шоу,

У мене є основа для ізоляції двигунів на різних ПК, спілкування через TCP / IP, що робить Reflection нічим не потрібним. Однак, через мою передбачувану кількість заявок, це призвело б до того, що змагання триватимуть надзвичайно довго.
Джон Гітцен

6
Тоді я не знав, що у них з'явилося віддзеркалення!
Маркус Нігбур

1

Я прогнозую, що переможе той, хто зуміє змінити інтенсивність своїх противників випадковою схемою насіння та виклику.

Не впевнений, наскільки це ймовірно.


У опонентів є можливість використання CSPRNG.
Джон Гітцен

Хороший момент, хоча я визнаю, що зворотна інженерія такого насіння все одно виходить за межі мого досвіду. Я думаю, що найбільш вразливим аспектом був би алгоритм вибору шаблону вогню, але навіть тоді ви не обов'язково отримаєте багато від його порушення, оскільки немає можливості пересунути свої кораблі, коли гра розпочнеться.
Triston Attridge

Коли я подавав заявку на науково-практичне стажування, ми писали програми лінкорів і змагалися. Встановивши випадкове насіння саме так, як я виграв X)
P Швед

1
Припускаючи досить простий алгоритм розміщення судна, я б міг уявити, що можна було б, після отримання кількох ударів на різних кораблях, почати використовувати більшу частину повороту, прокручуючи всі можливі випадкові насіння (можливо, починаючи з десь поблизу поточного часу і рухаючись вперед / назад на крок або близько того) і бачити, які з них генерують місця розташування судів, сумісні з спостережуваними зверненнями.
Доменік

1

Також, мабуть, можна було б запустити цілу серію з варіаціями в грі.

Додавання таких речей, як 3d-літак або можливість переміщення одного корабля, а не стріляти за поворот, певно змінить гру.


2
Існує варіація "залп". Де ви можете стріляти стільки пострілів за оборот, скільки у вас залишилися кораблі.
Джон Гітцен

Цікава варіація також. Я, здається, згадую, як грав у комп’ютерній версії, яка мала і літак. Він буде випадковим чином стріляти по місцях на протилежній дошці.
Гленн

інша варіація: розмір борту + кількість кораблів.
russau

1

Загальна тривалість гри на одну секунду залежить від машини. Один раз операції з процесором на моїй машині будуть відрізнятися порівняно з турнірною машиною. Якщо я оптимізую алгоритм Battle Ship для використання найбільшого часу процесора протягом 1 секунди, то він запускається на можливій повільній машині турніру, він завжди програє.

Я не впевнений, як обійти це обмеження рамки, але це слід вирішити.

...

Одна ідея - зробити те, що було зроблено на цьому конкурсі http://www.bcsstudentcontest.com /

Майте максимальний час за оборот, на відміну від максимального загального часу гри. Таким чином я міг би обмежити алгоритми, щоб вони вміщувались в межах часу обігу. Гра може тривати від 50 до 600+ оборотів, якщо мій алгоритм керує загальним ігровим часом, він може не дати достатньо часу, щоб зробити найкращу роботу, або він може дати занадто багато часу і програти. Дуже важко керувати загальним ігровим часом в алгоритмі Battleship.

Я б запропонував змінити правила, щоб обмежити час повороту, а не загальний час гри.

Редагувати

Якщо я написав алгоритм, який перераховує всі можливі кадри, а потім класифікує їх, то приймаю найвищий рейтинг. Генерування всіх можливих кадрів зайняло б занадто багато часу, тому я б дозволив алгоритму працювати протягом певного часу, а потім зупинити його.

Якщо був поворотний поріг, я міг би дозволити алгоритму працювати 0,9 секунди і повернути знімок найвищого рейтингу, і добре витримуючи межу часу повороту.

Якщо я обмежуюсь загальним ігровим часом однієї секунди, важко буде визначити, як довго алгоритм повинен працювати на кожному кроці. Я хочу, щоб максимум часу на процесор. Якщо гра тривала 500 раундів, я могла обмежити кожен хід 0,002 секунди, але якщо гра триватиме 100 раундів, я можу давати кожен поворот 0,01 секунди часу процесора.

Для алгоритму було б недоцільно використовувати напіввичерпний пошук місця зйомки, щоб знайти найкращий кадр із обмеженням струму.

Загальний час гри на 1 секунду обмежує тип алгоритмів, які можна ефективно використовувати для змагання в грі.


Він буде працювати на чотирьохядерному процесорі Intel Q9550SX, 8 Гб оперативної пам’яті, на версії Vista 64. Чи буде 1 секунда обмежуючим фактором?
Джон Гітцен

Я думаю, ви повинні зробити свій лінійний корабель AI багатопоточним, щоб обчислити макс. Кількість пострілів за цей проміжок часу.
Джефф Етвуд

хитрість полягає в тому, як обмежити інтервал часу повороту. Якщо я обмежую його 0,00005 секунд, я захищений від запуску через часовий ліміт, але значно обмежую пошуковий простір. Якщо я збільшую межу часу повороту, збільшується простір пошуку, але я ризикую втратити час.
TonyAbell

@TonyAbell: Якщо важливо встановити обмеження по черзі, чому б не почати з початкового значення, а потім відрегулювати його з раунду на раунд? Приблизно через половину раундів ви, швидше за все, знайшли оптимальну довжину повороту для опонента, з яким ви стикаєтесь.
kyokley

Ви повинні відслідковувати свій час, що залишився, і обмежити його на 1/2 доступного часу.
Джон Гітцен

1

Я тут підписую, не вводячи фактичний код - але я загрожу деяким загальним спостереженням:

  • Оскільки всі кораблі мають розмір щонайменше 2 комірок, ви можете використовувати оптимізацію, яку я бачив під час здійснення гри в Space Quest V - яка стріляє лише по чергових клітинках з алмазним малюнком, поки «шукає» ціль. Це виключає половину квадратів, при цьому все ж гарантуючи, що ви зрештою знайдете всі кораблі.
  • Випадкова схема стрільби при пошуку мішеней статистично дасть найкращі результати у багатьох іграх.

1

! [Щільність ймовірності] [1] введіть її опис зображення

! [введіть тут опис зображення] [2]

Я експериментував, порівнюючи результати стрільби з рендону з німим полюванням / ціллю і, нарешті, складним пошуком.

Найкращим рішенням є створення функції щільності ймовірності для того, наскільки ймовірно, що будь-який окремий квадрат використовується іншими кораблями, і націлити його на квадрат з найвищим значенням.

Ви можете побачити мої результати тут, введіть опис посилання тут


Не могли б ви зафіксувати свою відповідь і особливо свої зображення та посилання?
Барт

-2

"Лінковий корабель" - це те, що відоме як класична проблема інформатики NP - повна проблема.

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

(шукайте Лінкор - він там, під іграми та пазлами)


4
Яка головоломка броненосця ( en.wikipedia.org/wiki/Battleship_(puzzle) ), а не гра "Броненосець" ( en.wikipedia.org/wiki/Battleship_(game) ).
Джейсон Беркан

Так, як заявив Джейсон, це зовсім інша тварина.
Джон Гітцен

3
Хехехе. Наступне завдання я отримую на роботі, я скажу, що це NP-завершено, а потім довго обідаю. :-)
Борк Блатт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.