65.9K
CodeProject 正在变化。 阅读更多。
Home

Calcoolation: 一款数学谜题棋盘游戏

starIconstarIconstarIconstarIconstarIcon

5.00/5 (22投票s)

2009年7月26日

CPOL

5分钟阅读

viewsIcon

132738

downloadIcon

2942

数学谜题棋盘游戏演示

Cal-cool-ation

引言

“Calcoolation”是一款益智游戏,乍一看与流行的数独游戏很像。和数独一样,你需要在一个 N x N 的矩阵中,将数字 1 到 N 放置在每一列和每一行中,且不重复。不同之处在于, there are blocks of cells, named "cages", where specific operations involving the numbers in the cells must satisfy the specific result displayed in the cage。

背景

这款游戏由日本教师宫本哲也(Tetsuya Miyamoto)于 2004 年发明,原名“KenKen”(Ken 是日语中“聪明”的意思),尽管我直到今年年初才了解到它。我被它深深吸引(“困惑”),然后决定用 C# 来开发这款游戏。

最具挑战性的部分是找出一种正确的策略来随机选择不重复的行和列数字。我决定采用“裸对/裸三元组”策略,这是我从一些专注于数独解题的网站上借鉴来的。我将在本文后面讨论裸对。

picking all the numbers, I still had to randomly create the "cages". Cages are sets of contiguous cells in the board. I did this by randomly selecting pairs of cells in random directions, beginning from the top/left corner. Thus, initially the cages had two cells, but when a random cage cell superposes another cage cell, those cages are merged, so we could have 3-pieces and 4-pieces cages。

代码

代码分为两层:Core 和 WinUI。在 WinUI 层,我们有 Windows Forms 的表示逻辑。这是一个非常简单的用户界面,旨在让玩家的生活更轻松。这里需要注意的是,我创建了一个用户控件(“CellBox”)来保存单元格数据、功能和事件。在 Windows Forms 中,用户控件是 UI 端分离关注点的有用工具。

Core 层完成了大部分繁重的工作。它由代表游戏中三个主要实体的类组成:BoardCageCell。游戏中只能有一个 Board(这就是我决定使用单例模式的原因)。默认的 Board 尺寸为 4x4(之后用户可以更改)。棋盘上的每个位置都由一个单元格(即单元格数量 = 尺寸²)占据。在棋盘内部,单元格也排列成称为“Cages”的块(类似于传统谜题)。

我认为值得一提的代码部分是那些与随机数字选择、随机笼子生成和游戏完成度测试相关的部分。

随机数字选择

有关随机数字选择,请参见 GenerateNumbers() 方法。

private void GenerateNumbers()
{
    ResetBoard();

    Random rnd = new Random();

    string number = "0";

    int minSize = size;
    int maxSize = 0;
    bool someSolved = true;

    while (someSolved)
    {
        someSolved = false;

        //Search for naked pairs in rows
        if (!someSolved)
        {
           //code removed for better visibility
        }

        //Search for naked pairs in columns
        if (!someSolved)
        {
           //code removed for better visibility
        }

        //Search for naked triplets in rows
        for (int row = 0; row < size; row++)
        {
           //code removed for better visibility
        }

        //Search for cells with a unique solution possible
        for (int row = 0; row < size; row++)
        {
           //code removed for better visibility
        }

        //Random selection
        if (!someSolved)
        {
           //code removed for better visibility
        }
    }
}

请注意,根据上面的代码,裸对会在开始时被解决。然后是裸三元组,然后是具有唯一解的单元格,最后是随机选择。这样做是为了避免回溯。

结果是,我们现在有一个有效的棋盘,可以开始使用了。

Candidates

随机笼子生成

下一步是随机创建笼子,这是 GenerateCages 方法。

private void GenerateCages()
{
    cages = new List<Cage>();

    bool success = false;
    int orientation = 0;
    int c2 = 0;
    int r2 = 0;

    Random rnd = new Random();

    for (int r = 0; r < size; r++)
    {
        for (int c = 0; c < size; c++)
        {
            if (matrix[c, r].Cage == null)
            {
                success = false;
                while (!success)
                {
                    orientation = rnd.Next(1, 5);

                    switch (orientation)
                    {
                        case 1: // W
                            c2 = c - 1;
                            r2 = r;
                            break;
                        case 2: // E
                            c2 = c + 1;
                            r2 = r;
                            break;
                        case 3: // N
                            c2 = c;
                            r2 = r - 1;
                            break;
                        case 4: // S
                            c2 = c;
                            r2 = r + 1;
                            break;
                    }

                    if (c2 >= 0 && c2 < size && r2 >= 0 && r2 < size)
                    {
                        Cage cage = matrix[c2, r2].Cage;
                        if (cage == null)
                        {
                            cage = new Cage();
                            cage.CellList.Add(matrix[c2, r2]);
                            matrix[c2, r2].Cage = cage;
                        }
                        else
                        {
                            if (cage.CellList.Count > 3 && (c != size - 1 || r != size - 1))
                            {
                                continue;
                            }
                        }

                        cage.CellList.Add(matrix[c, r]);
                        matrix[c, r].Cage = cage;
                        cages.Add(cage);
                        success = true;
                    }
                }
            }
        }
    }

从棋盘的 {0,0} 位置开始,向右和向下移动,此函数以随机方向放置两个单元格的块,并测试是否存在与现有笼子的冲突。在这种情况下,笼子会被合并;否则,会创建一个新笼子。

Candidates

之后,PickOperation() 方法会选择一个可能的随机运算(在 +、-、x 和 ÷ 中选择),使用笼子内的数字。

public void PickOperation(Cage cage)
{
    bool success = false;

    while (!success)
    {
        if (currentOperation == 5)
            currentOperation = 1;

        switch (currentOperation)
        {
            case 1:
                cage.Operation = Operations.Plus;
                int sum = 0;
                foreach (Cell cell in cage.CellList)
                {
                    sum += Convert.ToInt32(cell.CellValue);
                }
                cage.Result = sum;
                success = true;
                break;
            case 2:
                cage.Operation = Operations.Minus;
                if (cage.CellList.Count == 2)
                {
                    int sub = Convert.ToInt32(cage.CellList[0].CellValue) - 
                              Convert.ToInt32(cage.CellList[1].CellValue);
                    if (sub > 0)
                    {
                        cage.Result = sub;
                        success = true;
                    }
                    else
                    {
                        sub = Convert.ToInt32(cage.CellList[1].CellValue) - 
                              Convert.ToInt32(cage.CellList[0].CellValue);
                        if (sub > 0)
                        {
                            cage.Result = sub;
                            success = true;
                        }
                    }
                }
                break;
            case 3:
                cage.Operation = Operations.Multiply;
                int mult = 1;
                foreach (Cell cell in cage.CellList)
                {
                    mult *= Convert.ToInt32(cell.CellValue);
                }
                cage.Result = mult;
                success = true;
                break;
            case 4:
                cage.Operation = Operations.Divide;
                if (cage.CellList.Count == 2)
                {
                    int quo = Convert.ToInt32(cage.CellList[0].CellValue) / 
                              Convert.ToInt32(cage.CellList[1].CellValue);
                    int rem = Convert.ToInt32(cage.CellList[0].CellValue) - quo * 
                              Convert.ToInt32(cage.CellList[1].CellValue);
                    if (rem == 0)
                    {
                        cage.Result = quo;
                        success = true;
                    }
                    else
                    {
                        quo = Convert.ToInt32(cage.CellList[1].CellValue) / 
                              Convert.ToInt32(cage.CellList[0].CellValue);
                        rem = Convert.ToInt32(cage.CellList[1].CellValue) - quo * 
                              Convert.ToInt32(cage.CellList[0].CellValue);
                        if (rem == 0)
                        {
                            cage.Result = quo;
                            success = true;
                        }
                    }
                }
                break;
        }

        currentOperation++;
    }
}

变得更聪明:玩转候选数

我必须承认,我觉得这款游戏对我来说很难。我可以应对 3 x 3 的棋盘,但从 4 x 4 开始,事情就会变得复杂得多。如果我手头有纸质版的这款游戏,我可能会记下哪些数字可以放在单元格里,哪些不可以。然后,如果我找到了某个单元格的正确数字,我就会拿起铅笔划掉同一行和同一列的其他单元格中的那个数字。然后我产生了一个奇怪的想法:如果我允许用户直接在应用程序中做笔记呢?于是,通过菜单“设置 -> 显示候选数”,这个新功能就诞生了,你可以打开/关闭候选数字。

Candidates

请注意,当您使用候选数进行游戏时,界面会发生一些变化。

Candidates

 

裸对

在为单元格选择随机数字之前,你应该始终寻找“裸对”。裸对意味着在某一行或某一列中,有两个单元格有两个可能的数值。在下面的图中,我们可以发现这些裸对,只有两个可能值 [3,4]。

Candidates

发现裸对的原因很简单:由于这两个单元格只能容纳这两个数字,该行中的任何其他单元格都不会有“3”或“4”。因此,我们可以从可能的数字中移除它们。

Candidates

关注点

我认为对我来说更难的部分是找出如何在一个有效的 N x N 棋盘中随机排列数字(也就是说,不重复行和列中的数字)。经过一些研究,我找到了前面提到的裸对/裸三元组技术。当然,我还可以使用许多其他技术,所以如果你对此感兴趣,不仅作为一名开发者,也作为一名玩家,这里有一些资源:

历史

  • 2009-07-26:首次发布
  • 2009-07-29:新功能:“显示候选数”
  • 2009-07-30:Visual Studio 2005 新版本
© . All rights reserved.