战舰人工智能 - 完全迷失

4
我正在使用C#.NET构建战舰游戏,它应该使用相当简单的得分机制。没有船只沉没,如果玩家或电脑得分17个命中,他们就赢了。如果你打中了,你可以再次轮到你。AI会随机攻击,直到它得分一个命中为止,然后它将在每个方向上攻击一个瓷砖,直到找到趋势,然后继续沿着直线攻击,直到找到死路(未占用空间或棋盘边缘)。如果在电脑攻击的相反方向上有未被攻击的空格,则它将攻击这些空格。它不会攻击已经攻击过的空格或遵循已经遵循的模式。

这是目前我的AI。

    int shipCounter = 0, trend = 0;
    static Random rnd = new Random();
    bool gameOver = false, playerTurn = false;
    int[] score = { 0, 0 };

    struct gameData
    {
        public bool occupied, hit, marked;
    }
    gameData[,,] data;



    public void computerMove()
    {
        Point target = seekTarget();

        try
        {
            if (data[1, target.X, target.Y].hit)
                computerMove();
            else
            {
                data[1, target.X, target.Y].hit = true;
                if (data[1, target.X, target.Y].occupied)
                {
                    attacking = true;
                    score[0]++;
                    computerMove();
                }
            }

            playerTurn = true;
        }
        catch (IndexOutOfRangeException)
        { computerMove(); }
    }

    public Point seekTarget()
    {
        Point origin = new Point(-1, -1);

        //find a point that's been hit.
        int x = 0, y = 0;

        while (x < gridSize && y < gridSize)
        {
            if (data[1, x, y].hit && data[1, x, y].occupied && !data[1, x, y].marked)
            {
                origin = new Point(x, y);
                break;
            }
            x++;
            if (x == gridSize && y != gridSize)
            {
                x = 0;
                y++;
            }
        }

        return findTargets(origin);            
    }

    public Point findTargets(Point origin) 
    {
        Point[] lim = { origin, origin, origin, origin };
        Point[] possibleTargets = { origin, origin, origin, origin };

        //Find the edges.

        while (lim[0].X >= -1 && ((!data[1, lim[0].X, lim[0].Y].hit && !data[1, lim[0].X, lim[0].Y].occupied) || (data[1, lim[0].X, lim[0].Y].hit && data[1, lim[0].X, lim[0].Y].occupied)))
        {
            lim[0].X--;
            if (lim[0].X == -1)
                break;
        }
        while (lim[1].Y >= -1 && ((!data[1, lim[0].X, lim[0].Y].hit && !data[1, lim[0].X, lim[0].Y].occupied) || (data[1, lim[0].X, lim[0].Y].hit && data[1, lim[0].X, lim[0].Y].occupied)))
        {
            lim[1].Y--;
            if (lim[1].Y == -1)
                break;
        }
        while (lim[2].X <= gridSize && ((!data[1, lim[0].X, lim[0].Y].hit && !data[1, lim[0].X, lim[0].Y].occupied) || (data[1, lim[0].X, lim[0].Y].hit && data[1, lim[0].X, lim[0].Y].occupied)))
        {
            lim[2].X++;
            if (lim[2].X == gridSize)
                break;
        }
        while (lim[3].Y <= gridSize && ((!data[1, lim[0].X, lim[0].Y].hit && !data[1, lim[0].X, lim[0].Y].occupied) || (data[1, lim[0].X, lim[0].Y].hit && data[1, lim[0].X, lim[0].Y].occupied)))
        {
            lim[3].Y++;
            if (lim[3].Y == gridSize)
                break;
        }

        //Cell targeting AI

        }
        return new Point(rnd.Next(10), rnd.Next(10));
    }

由于我无法确定出错的原因,导致问题变得非常混乱。如果我注释掉findTargets函数并让计算机随机攻击,那么它就可以正常工作。计算机和玩家轮流攻击,计算机击中后,游戏会交换回合。
然而,启用findTargets后,玩家只能进行一次攻击,而计算机不会接管回合。即使此时玩家的攻击准星仍然可见,游戏也没有返回到玩家回合。如果有人能提供帮助,将不胜感激。很抱歉没有包含PaintmouseDown方法,因为它们超出了字符限制。
没有使用findTargets的UI(玩家和计算机轮流攻击): 使用findTargets的UI(计算机无法接管回合,玩家只能攻击一次): 提前感谢任何帮助。
编辑:我已经找到了问题所在,似乎它不能跳出findTargets中的while循环。即使当我解决了当origin(-1,-1)时它无法停止循环的问题时,仍然会在第一次攻击中陷入循环。
编辑2:它进入了第一个循环,并无限循环。由于某种原因,它完全没有增加lim[0].X。当我在循环中插入一个消息框来显示一些数据时,它会显示两次,然后不再出现,即使它仍在循环中。有人知道为什么吗?

10
将你的演示和逻辑分开。 - ChaosPandion
1
你确定你已经给我们足够的代码了吗?我们能得到PNG文件吗? - Yuriy Faktorovich
1
你有很多复制/粘贴的代码,请尝试重构它,这将对你有很大帮助。 - ppetrov
3
首先,在findTargets的顶部设置一个断点,然后在第一次玩家回合后逐步执行。这样可以准确地指出程序卡住的位置。 - NotMe
1
当然这很令人困惑——当你浏览代码时,你应该看到3个重复的部分——通过检查代码结构,你通常可以发现重复的代码。如果你开始使用良好命名的方法来委派一些工作,可能还有一些更多的类,你会发现很多问题都会消失。我认为有一个重构的堆栈溢出网站,我会把它交给他们,看看他们能想出什么。一旦它被正确地布局和适当地分解,你会惊讶于修复问题变得多么容易,以及你的工作变得多么快速和轻松。 - Bill K
显示剩余5条评论
3个回答

3
您正在使用一种面向对象的语言 - 看起来像是Java。
因此,为了使编码、理解、维护和增强代码更容易,请尝试使用一些实际的对象。
例如,您应该肯定有一个Ship类,肯定应该有一个Grid类,可能有一个Shot类等。您的Ship类一定要与Grid类“插入”。您的Grid类不应该分配整个可能位置的网格,而是应该只分配有效的打击区域,因为船只实例被插入其中。每个没有被船只实例占据的位置显然是一个未命中,因此处理所有不包含船只的位置的方法是-。
Grid类将完成所有工作 - 它可以拥有addShipHorizontal(Ship, x, y)、addShipVertical(Ship, x, y)。它一定要有一个hitTest(x, y),它返回null或一个Ship。它应该维护一个Ship实例的集合,比如ArrayList,它将在hitTest(x, y)方法中迭代。
Ship应该有一个PointCount和一个Points集合,在将Ship传递到addShipH()或addShipV()方法时设置。Ship还应该有一个hitTest(x, y)方法,如果船只位于指定的x, y,则返回true。hitTest(x, y)将遍历Points的船只集合寻找匹配项。
当该轮攻击时,选择网格上的位置并进行命中测试 - 选定的位置是否包含船只参考,是,则执行ship.hit(location)并返回一个新的Hit() - 否则返回一个新的Miss()。
将其分解为您认为需要模拟的对象 - 这称为域模型。然后给每个对象类适当的方法,使得实际游戏只是域模型类之间的协作和交互。不要编写游戏代码 - 相反,请编写类,然后编写类的方法 - 随着类的构建,游戏将在类之间通过它们的方法的交互中出现。
从顶部开始,您将需要两个应用程序实例的哪个类?答案-Player。Player类管理什么?一个网格和一系列船只。Grid类管理什么?船只及其位置的列表以及用于射击的Hit测试。Ship维护什么?它在网格上的位置以及它所处的位置有/没有被击中。
如果您按照面向对象的风格进行编写,您会发现代码量减少了四分之一,灵活性提高了两倍。祝你好运!

我很想这样做,但我的任务规则是要保持无类别。这应该在几天前就完成了。 - user1576628
@user1576628 真的没有水准吗?我希望有一个非常好的理由。 - Yuriy Faktorovich
如果你的教练要求不使用类来完成,我建议你退出这门课程。类似于20世纪80年代早期的C语言中的结构化编程,类无关是等同于结构化编程的。面向对象编程是为了解决结构化编程的缺陷而创建的,并且是当前行业的标准(尽管从今天大多数主要系统的代码看不出来)。类无关就是结构化编程的缺陷,而面向对象编程是为了解决这些缺陷而创建的。此外,如果你正在使用Java,根据定义,它永远不能是类无关的。 - Rodney P. Barbati

1
如@jorge-Chibante所提到的,请将您的域代码和演示代码分开,并尝试编写一些单元测试来验证您期望的AI行为,然后再开发您的AI。
在Ships N' Battles战舰游戏中,我使用概率构建了AI,并制作了一个脑图,以确定每个方格有敌人战舰的概率。所有这些都经过了一些测试来验证其行为和任何未来的重构。
保持您的代码每部分只负责一个职责,这样您的生活会更轻松。

0

人工智能

在战舰游戏中,有三种不同的策略模式(这里我简化了一下)。

  • 随机轰炸一个方格
  • 以圆形轰炸
  • 以直线轰炸

在开始时,计算机只需要找到一个尚未被轰炸的随机方格。当它找到一个方格后,就需要在周围轰炸,试图找出船的位置。之后,它会假设这是船的方向,并朝两侧直线轰炸。

你的计算机应该有一个变量来指示它处于哪种模式。

代码

你的代码到处都是,很难阅读。你应该将逻辑分离成更多的函数。这样更容易测试、阅读和找出问题所在。同时确保你的游戏逻辑和UI逻辑分离得很好。

public bool IsOccuped(Point target)
public bool AlreadyBombed(Point target)
public bool BombLocation(Point target)
public Point FindNewTarget()

你的计算机移动将会更加顺畅

public void ComputerMove()
{
    Point target;

    target = FindNewTarget();

    BombLocation(target);
}

// There are more efficient way of doing this but it's an example.
public Point FindNewTarget()
{
    Point newTarget = new Point();

    do
    {
        newTarget.x = [get random location from 0 to grid width]
        newTarget.y = [get random location from 0 to grid height]   
    }while(AlreadyBombed(newTarget))
}

即使一个函数很小,代码的可读性也能帮助很多。
public bool IsOccuped(Point target)
{
    return data[1, target.X, target.Y].hit;
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接