战舰!
早在2003年(当时我17岁),我参加了一场《战舰》AI编码比赛。尽管我输了那场比赛,但我从中获得了很多乐趣,也学到了很多东西。
现在,我想恢复这个比赛,在搜索最好的战舰AI。
下面是这个框架,现在托管在Bitbucket上。
获胜者将获得+450声望奖励!比赛将于2009年11月17日开始。17号零时之前的投稿和编辑将不被接受。(中央标准时间)
尽早提交你的作品,这样你就不会错过机会!
为了保持这个目标,请遵循比赛的精神。
游戏规则:
游戏在10x10的网格上进行。
每个参赛者将5艘船(长度为2、3、3、4、5)中的每一艘放在他们的网格上。
没有船只可以重叠,但它们可以相邻。
然后选手们轮流向对手射击。
游戏的一个变体允许每次齐射多次,每艘幸存的船一次。
如果击球沉、命中或未命中,对手将通知选手。
当任何一名玩家的所有船只都沉没时,游戏就结束了。
比赛规则:
The spirit of the competition is to find the best Battleship algorithm.
Anything that is deemed against the spirit of the competition will be grounds for disqualification.
Interfering with an opponent is against the spirit of the competition.
Multithreading may be used under the following restrictions:
No more than one thread may be running while it is not your turn. (Though, any number of threads may be in a "Suspended" state).
No thread may run at a priority other than "Normal".
Given the above two restrictions, you will be guaranteed at least 3 dedicated CPU cores during your turn.
A limit of 1 second of CPU time per game is allotted to each competitor on the primary thread.
Running out of time results in losing the current game.
Any unhandled exception will result in losing the current game.
Network access and disk access is allowed, but you may find the time restrictions fairly prohibitive. However, a few set-up and tear-down methods have been added to alleviate the time strain.
Code should be posted on stack overflow as an answer, or, if too large, linked.
Max total size (un-compressed) of an entry is 1 MB.
Officially, .Net 2.0 / 3.5 is the only framework requirement.
Your entry must implement the IBattleshipOpponent interface.
得分:
Best 51 games out of 101 games is the winner of a match.
All competitors will play matched against each other, round-robin style.
The best half of the competitors will then play a double-elimination tournament to determine the winner. (Smallest power of two that is greater than or equal to half, actually.)
I will be using the TournamentApi framework for the tournament.
The results will be posted here.
If you submit more than one entry, only your best-scoring entry is eligible for the double-elim.
好运!玩得开心!
编辑1:
多亏弗里德,他在飞船上发现了一个错误。是否是可用的函数。问题已经解决了。请下载框架的更新版本。
编辑2:
由于人们对将统计数据持久化到磁盘等非常感兴趣,所以我添加了一些非计时设置和删除事件,它们应该能够提供所需的功能。这是一个半突破性的变化。也就是说:修改了接口,添加了功能,但不需要body。请下载框架的更新版本。
编辑3:
错误修复1:GameWon和GameLost只在超时的情况下被调用。
错误修复2:如果引擎在每一款游戏中都暂停计时,那么竞争将永远不会结束。
请下载框架的更新版本。
编辑4:
比赛结果:
我的电脑现在正在戴尔维修,但这是我上周在哪里:
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; }
}
}
这是一个供人们玩的对手:
http://natekohl.net/files/FarnsworthOpponent.cs
与其使用固定的几何启发策略,我认为尝试估计任何特定的未探索空间拥有一艘船的潜在概率会很有趣。
为了做到这一点,你需要探索所有符合你当前世界观的船的可能配置,然后基于这些配置计算概率。你可以把它想象成探索一棵树:
扩大可能的战列舰国家http://natekohl.net/media/battleship-tree.png
在考虑了所有与你所了解的世界相冲突的树叶后(游戏邦注:例如,船只不能重叠,所有被击中的方块都必须是船只等),你可以计算船只在每个未探索位置出现的频率,从而估算船只位于那里的可能性。
这可以可视化为热图,其中热点更有可能包含船只:
每个未开发位置的概率热图http://natekohl.net/media/battleship-probs.png
我喜欢这个战列舰比赛的一个原因是上面的树几乎小到可以强制使用这种算法。如果这5艘船每艘都有150个可能的位置,那就是1505 = 750亿种可能性。这个数字只会越来越小,特别是如果你可以排除整艘船。
我上面链接的对手并没有探索整棵树;750亿美元仍然太大,无法在一秒钟内进入。不过,在一些启发式的帮助下,它确实试图估计这些概率。