【译】Calcoolation:一个数字难题游戏
原文地址:http://www.codeproject.com/Articles/38454/Calcoolation-A-Math-Puzzle-Board-Game
版权:作者版权声明为http://www.codeproject.com/info/cpol10.aspx,可以翻译
一、简介
Calcoolation是一个益智游戏,初看起来像流行的数独游戏。就像数独一样,你有一个N * N矩阵,数字1到N必须放置在每一列和每一行并且没有重复。它和数独的区别在于它存在一个区域概念叫做笼子,具体的操作包括细胞的数量必须满足笼子里面显示的特定结果
二、背景
这个游戏是由一位名为Tetsuya Miyamoto的日本老师在2004年发明的,名字是KenKen(Ken在日语中是聪明的意思),虽然我在今年年初的时候才知道。这个游戏给我留下了非常深刻的印象,我决定用C#重新做一个出来。
最具有挑战的部分是在行和列的数字不重复的情况下来发现正确的策略。我决定使用NakedPairs/Naked Triplets策略,这是我从一些数独网站上借鉴来的策略。我会在后面的内容中对这个策略进行解释
选择所有数字后,我还得随机创建“笼子”。 笼子是一系列连续的区域。我的做法是在任意方向上随机选择两个格子,从左上角开始。因为,最初的笼子只有两个格子,但是当两个笼子相邻的时候,它们会合并,因此我们也有3个格子或者4个格子的笼子。
三、代码
代码分成2层:核心层和UI层。在UI层,我们使用了Windows Forms。它是个非常简单的UI,让开发变得更加容易。需要说明的是我创建了一个自定义控件CellBox来装格子数据。在Windows Forms,自定义控件对于分离关注点是很有帮助的
核心层承担了大部分困难的工作。由三个代表了游戏主体的类来构成:棋盘、笼子和格子。一个游戏只能有一个棋盘(所以我使用了单例模式)。默认的棋盘是4*4(用户可以更改),棋盘的每个位置都有一个格子。在棋盘内部,格子也被重新组成笼子。
代码里头我觉得值得提的是随机数字选择,随机笼子形成以及游戏整体测试
四、随机数字选择
随机数字选择部分,请看GenerateNumbers()函数
private voidGenerateNumbers()
{
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 uniquesolution possible
for (int row = 0; row < size; row++)
{
//code removed for better visibility
}
//Random selection
if (!someSolved)
{
//code removed for better visibility
}
}
}
注意,根据上面的代码,nakedpairs是一开始就被确定的。然后是the naked triplet,再然后是随机选择,这样做是为了避免回溯。
现在我们有了有了一个有效的棋盘可以使用:
五、随机笼子生成
下一个比较重要的步骤是随机创建笼子,这是GenerateCages方法:
private voidGenerateCages()
{
cages = new List();
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}位置开始,然后往右往下移动,这个函数把任意方向的两个格子用来测试是否与现在存在的笼子存在冲突,如果冲突,则合并,否则,创建一个新的笼子。
然后,PickOperation()会通过笼子里面的数字选择一个可能的随机位置。
public voidPickOperation(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 incage.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起,事情变得更加复杂。如果这个游戏是在纸上玩,我可能会记笔记,记录数字可以或不能放置在格子里。然后我会找到那个正确的数字。所以我有这种奇怪的想法:我怎么样允许用户直接在应用程序中做笔记吗?这个新特性通过菜单“设置- >显示候选人”设置,在那里你可以打开/关闭候选数字。
注意,你现在可以使用候选数字了,这个UI会有一点变化
六、NakedPairs
在给格子设定一个随机数字之前,你最好找一下Naked Pairs。Naked pairs意味着在某些行或者某些列,有两个格子可以是两个数的组合。在下图,我们可以看到格子就是一个Naked Pairs,值只能是[3,4]:
要找Naked Pairs的原因很简单:因为这两个格子智能是这两个数字,因此我们可以把这两个数字从集合里面排除
七、感兴趣的点
对我来说比较苦难的部分是在N*N的棋盘里面找到一个放置数字的方法(没有重复数字在行和列中)。经过一些研究,我发现了提过的naked pairs / naked triplets技术。当然它只是很多可能方案的一种,如果你感兴趣,可以看下以下资料
· · How to SolveSudoku Puzzles: Video Series (怎么解数独)
· · Techniques ForSolving Sudoku (解数独的技术)
· · Can You Use Some Sudoku Tips? (你会使用一些数独的提示么?)