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

Mr. Crossworder - 瞬间创建填字游戏!

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2018年12月14日

CPOL

30分钟阅读

viewsIcon

31451

downloadIcon

2289

填字游戏生成器 - 带有 Unicode 逻辑!

版本 2.0

版本 1.0

引言

这是一个用 C#.NET 和 .NET Framework 4.5.2 编写的填字游戏创建器。它还扩展支持使用 Unicode 字母创建填字游戏。不同的人类语言使用不同的 Unicode 代码页,因此不同 Unicode 语言的编码方式也会不同。然而,本项目提供了一个如何扩展逻辑以适应不同人类语言的思路。

背景

Necessity is the mother of invention.(需求是发明之母。)当我在为儿子下载填字游戏时,我突然想到,为什么不自己编写一个呢?我已经在我的另一个项目中有了一个类似的设计,可以重用它来满足略有不同的需求。就这样开始了。

工作原理

  1. 一开始,它会自动加载常规(英语)单词和线索。
  2. 如果用户对单词的组合不满意,可以点击“重新排列板块”菜单项。可以根据需要进行多次。然而,从逻辑上讲,一个更好的组合应该由成功放置单词的数量来决定,这个数量显示在右下角的标签上(例如,6个失败的单词,6个孤立的单词;剩余的38个单词将填入填字游戏中)。
  3. 用户可以在listview中选择一个单词。相应的单词将在网格中高亮显示。
  4. 如果用户对某个单词不满意,想从字典中选择另一个随机单词,则需要选择listview中的单词并按ENTER键。
  5. 如果用户想修改单词和含义(线索),则需要双击该单词。会出现一个小的对话框,允许更改单词。
  6. 当用户满意后,点击“创建填字游戏”菜单项。将显示实际的填字游戏板。
  7. 在此板上,点击文件->保存填字游戏。板(bmp图像)、线索(文本文件)和答案(文本文件)将保存在当前可执行路径的“Crosswords”文件夹中。这些文件将带有当前日期时间戳作为后缀。
  8. 如果用户想创建孟加拉语 Unicode 填字游戏,则点击主板上的“加载孟加拉语 Unicode”菜单项。
  9. 如果 JSON 字典被篡改且格式不正确,则会显示错误消息。
  10. 必要的配置在app.config文件中。

逻辑

该逻辑是使用一个 JSON 字典,其中键值对是单词-线索对。例如,如果以下是 JSON 条目,那么思路是使用含义作为线索,单词作为填字游戏。

{

“BUS”: “A public transportation used to carry people from place to place”

}

单词“BUS”将横向或纵向放置在网格上,其含义将是找到填字游戏的线索。当所有单词都放置在板上并且用户对组合满意后,他们将继续生成填字游戏。

对于单词生成,从这里获取了一个开源 JSON 字典。为了减少带宽,项目中添加了字典的一小部分(约600个单词)。建议下载整个字典并使用它;效果将相同,但可用的单词更多。

高层逻辑

  1. 随机选择 (X, Y) 轴和方向
  2. 尝试将单词放置在板上
    1. 如果板上的稀疏单词不够,则找到板上的一个孤立轴并将其放置在那里。
    2. 或者,如果板上有足够的稀疏单词,则确保当前单词与现有单词相交叉。在此阶段,如果放置尝试达到最大次数,则中止该单词,继续处理下一个单词。

(2a)的解释是,前几个单词被放置为不相连的单词。这是为了确保单词散布在整个板上。

(2b)的解释是,所有其余的单词都应该与其他现有单词相交叉。当一个单词在大量尝试后找不到合适的位置时,可能会出现不幸的情况。在这种情况下,在达到阈值后,该单词将被标记为失败。

改进的高层逻辑

与随机选择单词的起始 (X, Y) 轴不同,应用了第二种更有效率的逻辑。第二种逻辑检查单词的每个字母,看是否有另一个单词包含该字母。

例如,如果要放置 (CART),则它会检查板上是否存在包含“C”或“A”或“R”或“T”的现有单词。例如,板上可能有 CARATTESTASTEROID 等单词。

此逻辑的伪代码如下

对于当前单词中的每个字母:(例如:CAT中的“C”)

  1. 获取字母,并在板上查找包含该字母的单词。(例如:COWARCSCATTER)。
  2. 检查字母是否可以放置在那里

  3. 但是,第二种逻辑仅应用于 Unicode 部分。将其留给读者作为练习,应用于常规英文字母。如果可以放置,则将单词(CAT)放置在那里,然后继续处理下一个单词。
  4. 如果无法放置(未能与板上的任何现有单词交叉),则从下一个字母(例如,“A”在CAT中)开始,并尝试查找板上包含“A”的类似单词(例如:CARASTEROIDPASCAL 等);从(2a)循环。

合法放置

有效放置的逻辑如下

  1. 首先检查单词(例如,CART)是否可以放置在板上——如果它(CAT)与另一个单词(例如,HAT)交叉,则交叉处的字母(例如,“A”)与板上已有的字母相同。

  2. 如果单词要横向放置,则

    1. 在任何情况下,单词的前后都不能有其他字母。例如:如果要横向放置CART,则其前一个和后一个单元格应为空;因为TRAINSTOP已在板上,所以CART不能放在这里。
    2. 如果在单词所在行的上方任何单元格中有字母,则该单词(已在板上)不能停在前一行,而只能穿过该单词。例如,如果CART是当前单词,则不能将其放置在HAT的下方,但可以沿着MARTACTORTRIMALONG放置。

    3. 类似地,如果在单词所在行的下方任何单元格中有字母,则该单词(已在板上)应停在前一行,而只能穿过该单词。例如,如果CAT是当前单词,则不能将其放置在HAT的上方,但可以沿着MARTACTORTRIMALONG放置。

  3. 如果单词要纵向放置,则

    1. 在任何情况下,单词的上方和下方都不能有其他字母。例如:如果要纵向放置CAT,则其顶部和底部的单元格应为空。
    2. 如果在单词左侧的任何单元格中有字母,则该单词(已在板上)不能停在前一列,而只能穿过该单词。例如,如果CAT是当前单词,则不能将其放置在HAT的下方,但可以沿着MANGOARCSTAYTHREAD放置。

    3. 类似地,如果在单词右侧的任何单元格中有字母,则该单词(已在板上)不能在前一列开始,而只能穿过该单词。例如,如果CAT是当前单词,则不能将其放置在HAT的前面,但可以沿着MANGOTRAINSCOOPSTAYTHREADSCANT放置。

项目结构

该项目有两个主窗体,一个辅助窗体,6个类文件。各元素的作用是

  1. 窗体 – MainBoard:这是主窗体。其活动包括
    1. 将 JSON 字典加载到集合中(例如:约86,000个单词)。
    2. 随机加载一定数量的单词和含义(例如:50个)。
    3. 填充listview,以便用户可以看到单词和含义。
    4. 调用GameEngine类来利用放置逻辑并填充单词矩阵。
    5. 绘制网格(水平线、垂直线)。
    6. 将矩阵映射到各个单元格。
    7. 更新图例(状态标签)。
    8. 用不同颜色更新listbox,以表示失败的单词、孤立的单词和线索过长的单词。
    9. 与不同的菜单选择进行交互
      1. 加载英文单词 - 加载英文单词字典
      2. 加载孟加拉语 Unicode - 加载孟加拉语 Unicode 单词字典
      3. 重新排列板块 - 尝试不同的单词组合
      4. 创建填字游戏 - 显示“FinalCrosswordBoard
      5. 关于 - 显示“关于”框
    10. 如果用户在板上选择了单词,则启用用户在板上高亮显示该单词。
    11. 通过在listview中选择单词并按ENTER键,启用用户更改单个单词。
    12. 通过双击单词,启用用户调整(更改)单个单词。这会显示“EditWord”窗体。
  2. 窗体 – EditWord:允许用户更改单词和含义(线索)。
  3. 窗体 – FinalCrosswordBoard:这是填字游戏窗体。其活动包括
    1. ACROSSDOWN文本框中排列线索。应用逻辑进行正确编号。
    2. 绘制网格(水平线、垂直线)。
    3. 用灰色填充空白单元格(矩阵中的NULL单元格)。
    4. 相应地在将出现单词的单个白色框中放置索引。
    5. 与不同的菜单选择进行交互:保存填字游戏。
  4. 接口 – IDetails, ICompositeUnicode:包含单词详细信息基本签名的接口——单词、含义、轴、方向、失败标志、重叠标志、孤立标志、输出顺序。“ICompositeUnicode”有一个额外的列表来保存复合 Unicode 字符。
  5. 类 – DetailsAndAxes:包含两个类(结构体)——一个用于常规单词,另一个用于 Unicode。Unicode 类有一个额外的元素“CompositeUnicodeLetters”用于单个复合元素。
  6. 类 – Globals:用于globalstatic变量。
  7. 类 – BanglaUnicodeParser:用于解析孟加拉语 Unicode 字符。输入:整个单词(例如:ভণ্ডুল),输出字符串列表(例如:individualLetters[0] = ভ,individualLetters[1] = ণ্ডু,individualLetters[2] = ল)。
  8. 类 – GameEngine:具有放置逻辑的类
    1. 方法 – PlaceWordsOnTheBoard():循环遍历列表中的所有单词,并尝试在板上找到它们的放置位置。
      1. GetRandomAxis() - 为单词生成随机轴。
      2. PlaceTheWord() - 尝试将单词放置在板上。遵循“高层逻辑”部分中指定的高层逻辑。
        1. 如果是向右(ACROSS)的单词
          1. 检查板上是否有不匹配的重叠。
          2. 检查左侧单元格是否为空。
          3. 检查右侧单元格是否为空。
          4. 检查此单词所有字母下方的所有单元格是否为空;如果不是,则检查这是否是一个合法的交叉。
          5. 检查此单词所有字母上方的所有单元格是否为空;如果不是,则检查这是否是一个合法的交叉。
          6. 如果所有这些都通过了,那么这就是单词的一个有效轴;将其放置在那里。
        2. 如果是向下(DOWN)的单词
          1. 检查板上是否有不匹配的重叠。
          2. 检查顶部单元格是否为空。
          3. 检查底部单元格是否为空。
          4. 检查此单词所有字母左侧的单元格是否为空;如果不是,则检查这是否是一个合法的交叉。
          5. 检查此单词所有字母右侧的单元格是否为空;如果不是,则检查这是否是一个合法的交叉。
          6. 如果所有这些都通过了,那么这就是单词的一个有效轴;将其放置在那里。
  9. 类 – BanglaUnicodeGameEngine:与前一个类类似。但是,它提供了更好的逻辑,而不是随机生成初始轴。请参阅“改进的高层逻辑”部分以了解高层逻辑概述。唯一的补充是,由于每个单元格代表一个复合 Unicode 字母,那么如何容纳一个复合字母为一个单元格?你猜对了!向二维矩阵添加第三个维度,第三个维度负责处理单个复合 Unicode 字母。

单词放置后,它们看起来会像这样

Unicode 的触碰

世界上每种语言都有自己的 Unicode 页面。在本项目中,应用了孟加拉语 Unicode。本节将阐述如何将逻辑扩展到其他 Unicode 语言。

除了常规英文字母外,Unicode 还用于表示其他语言。但是,Unicode 编码略有不同,因为字母通常由不同代码的组合表示。例如,单词“ভণ্ডুল”表示为

每个字母都表示为一个不同的代码,Unicode 字母可以表示为单个代码(例如:2477 代表‘ভ’),也可以表示为代码组合(例如:ণ্ডু = 2467 ‘ণ’ + 2509 ‘্’ + 2465 ‘ড’)。

以下是一个输出单词(ভণ্ডুল)的简单示例。这显示了一个显示单词(ভণ্ডুল)的消息框。

MessageBox.Show(((char)2477).ToString() +
                ((char)2467).ToString() +
                ((char)2509).ToString() +
                ((char)2465).ToString() +
                ((char)2497).ToString() +
                ((char)2482).ToString());

对于常规英文单词,字母是独立的,所以无论何时需要处理单个字母,都可以直接使用字母。但是,对于 Unicode 字母,需要一个字符串列表,其中列表中的每个字符串代表一个复合 Unicode 字母。

public List<string> CompositeUnicodeLetters { get; set; }

换句话说,单词(ভণ্ডুল)需要被分解成三个独立的复合字母并放入列表中。所以,列表看起来会像

CompositeUnicodeLetters[0] = ‘ভ’
CompositeUnicodeLetters[1] = ‘ণ্ডু’
CompositeUnicodeLetters[2] = ‘ল’

这在需要遍历单词长度时是必需的。为了比较,以下片段遍历单词长度以确定它是否是孤立的。

if (wrd.Y > 0)
    for (int x = wrd.X, y = wrd.Y - 1, i = 0; i < wrd.Word.Length; x++, i++)
        if (matrix[x, y] != '\0')
        {
            wrd.Isolated = false;
            return;
        }

word.length不能直接用于 Unicode。例如,单词(ভণ্ডুল)的长度将是 6,因为它包含 6 个 Unicode 数字。

这就是为什么需要进行分割,将单词分解成不同的值,以便列表能够正确地沿长度遍历,如下所示

if (wrd.Y > 0)
    for (int x = wrd.X, y = wrd.Y - 1, i = 0; i < wrd.CompositeUnicodeLetters.Count; x++, i++)
        if (matrix[x, y, 0] != '\0')
        {
            wrd.Isolated = false;
            return;
        }

现在的问题是,填字游戏需要独立的复合字母,每个复合字母都可以放在一个单元格中。当读取 Unicode 语言时,它可以按原样读取并进行解析。然而,问题在于分隔单个复合字母,因为在每个连续字母之间没有分隔符。相比之下,在英语中,每个字母都是独立的,不需要分隔符。例如:CAT 中的每个字母都是独立的,不需要分隔符;每个字母都可以放在网格的独立单元格中。

要为孟加拉语或其他 Unicode 语言执行相同的操作,需要一种逻辑来解析单个复合字母。解析逻辑显然因 Unicode 语言而异。此外,分隔符不是长度特定的。例如,单词(চন্দ্রিমা)中的字母(ন্দ্রি)本身就需要六个单独的 Unicode 代码来构成复合字母(ন্দ্রি)。

因此,没有硬性规定如何解析单个复合 Unicode 字母。已开发出一种用于解析单个孟加拉语 Unicode 字母的逻辑,该逻辑可在项目文件“BanglaUnicodeParser.cs”中找到。如前所述,分割逻辑因 Unicode 语言而异。它还需要特定语言的专业知识。因此,不同的 Unicode 语言需要开发自己的解析器,因为语言的语义和结构完全不同。孟加拉语 Unicode 填字游戏看起来会像这样

程序流程

从文件读取

使用NewtonSoft.Json解析 JSON 文件并将单词放入集合中

using (StreamReader reader = new StreamReader(fileName))
    jsonWords = reader.ReadToEnd();
JObject obj = (JObject)JsonConvert.DeserializeObject(jsonWords);
wordsAndMeaning = obj.ToObject<Dictionary<string, string>>();

在集合中拍摄快照

之后,将一些单词的快照放入列表中。这是将要填入填字游戏中的单词列表。单词会去除任何空格和连字符。另外,不允许重复。

用快照中的单词填充 Listview

获得快照后,将单词放入列表中供用户查看。列宽通过比例因子和列表视图中最大单词长度动态维护。用户可以通过双击单词来更改单词和含义。此外,如果用户想选择一个新单词而不是列表中的单词,只需按ENTER键,就会从集合中随机选择另一个单词。

启动游戏引擎

现在是时候进行关键逻辑,找到单词在板上的正确位置了。逻辑在本篇文章的“逻辑”部分进行了描述。

引擎成功运行后,它会公开两个public变量供其他窗体使用

  1. wordDetails:单词详细信息列表,包含单词的信息——轴、方向、最大尝试次数、失败标志和孤立标志、以及序列(稍后将填充到填字游戏板)。
  2. matrix:表示板上字母的字符矩阵。在编程语言学中,这是一个二维char数组。

单词的孤立性在引擎主要活动结束时进行检查。单词CROSSWORD的意思是,每个WORD都相互CROSS。本项目不符合所有单词都应连接的传统观点。这留给读者作为练习。本项目可能存在孤立的单词组。但是,它不允许单词完全不相连而独立存在。这些单词将被标记为孤立,并将从最终的填字游戏板中移除。

将单词放置在板上

从游戏引擎返回后,主板开始将矩阵中的字符绘制到游戏板上。现在用户可以从列表中选择一个单词,主板将指示该单词在板上的位置。

此时,图例会更新为相应的状态。有三个状态标签——一个用于失败的单词,一个用于孤立的单词,一个用于长线索的单词。它们会相应更新。

生成填字游戏

当用户对组合满意后,他们选择创建填字游戏。当前的单词列表、字母矩阵和单词详细信息将被发送到窗体的构造函数。

维持单词的正确顺序是一个挑战,因为主板上有一个单词列表,而现在需要将其分成两组——ACROSS(横向)和DOWN(纵向)。

一开始,具有相同起始轴的单词会同时放入ACROSSDOWN字符串中。对原始单词详细信息集合进行克隆。然后,将具有相同起始轴的单词放入ACROSSDOWN字符串中。当这些单词完成后,其余单词将根据其方向放入ACROSSDOWN字符串中。所有单词都处理完毕后,会将克隆复制回原始集合。文本框也会用相应的线索填充。

线索成功解析后,就可以在板上放置数字了。使用相同的线条绘制功能,只是这次要在单元格中放置数字而不是单词。数字放置后,唯一剩下的就是用块状颜色填充其他单元格,以便CROSSWORD的单元格更加醒目。

最后,当用户选择文件->保存时,填字游戏将以图像形式保存在根文件夹中。除了图像,答案和线索也将写入单独的文本文件中。为了简单起见,不会询问用户文件名,但应用程序会自动添加日期时间戳以区分未来的CROSSWORD

代码一瞥

接口:IDetails

这包含单词详细信息的签名——轴、方向、最大尝试次数、失败标志和孤立标志。

常规单词类实现了这个接口。基本上,常规单词具有完全相同的属性——不多也不少。

接口:ICompositeUnicode

这包含一个用于保存分割的复合 Unicode 字符的额外字段的基本签名。Unicode 单词类实现了这个接口以及IDetails接口。

从文件读取

单词从文件读取并解析为dictionary对象,作为键值对。这在以下代码中完成

using (StreamReader reader = new StreamReader(fileName))
    jsonWords = reader.ReadToEnd();
JObject obj = (JObject)JsonConvert.DeserializeObject(jsonWords);
wordsAndMeaning = obj.ToObject<Dictionary<string, string>>();

放置逻辑

单词可以有两种方向——ACROSSDirection.Right)和DOWNDirection.Down)。首先,它检查单词是否可以放置在板上。对于单词的每个字母,它检查矩阵中相应的单元格(即,板上相应的单元格)是空白('\0')还是非空白。如果不是空白(不是'\0'),那么至少当前字母应该与板上已有的字母相同。这在以下代码中完成

for (int i = 0, xx = x; i < word.Length; i++, xx++) // First we check if the word 
                                                    // can be placed in the array. 
                                                    // For this, it needs blanks there 
                                                    // or the same letter (of another word) 
                                                    // in the cell.
{
    if (xx >= Globals.gridCellCount) return false;  // Falling outside the grid. 
                                                    // Hence placement unavailable.
    if (matrix[xx, y] != '\0')
    {
        if (matrix[xx, y] != word[i])               // If there is an overlap, then we see if 
                                                    // the characters match. If matches, 
                                                    // then it can still go there.
        {
            placeAvailable = false;
            break;
        }
        else overlapped = true;
    }
}

DOWN单词也进行了类似的检查,只是对于它们,我们需要向下移动(即 x 保持不变,y 改变)。

对于 Unicode,我们需要在此逻辑中添加一行。这是因为,对于 Unicode,单元格中不再是单个字母,而是几个 Unicode 字母组合成一个复合代码(字母)。此外,对于 Unicode,我们有一个三维矩阵。因此,以下行

if (matrix[xx, y] != '\0')

更改为

if (matrix[xx, y, 0] != '\0')

以及对非空白单元格的相同字母检查从

if (matrix[xx, y] != word[i])
{
    placeAvailable = false;
    break;
}

to

string compositeUnicodeLetter = Globals.GetCompositeLetterFromTheMatrix(xx, y, matrix);
if (compositeUnicodeLetter != unicodeLetters[i])
{
    placeAvailable = false;
    break;
}

在初始空白单元格检查和相同字母检查满足后,然后使用*“重叠”*标志和最大非重叠单词计数阈值来确定单词是单独放置还是应该重叠。提醒一下,前几个单词不应重叠,以使单词稀疏地分布在整个板上,而其余单词必须与其他现有单词重叠。这些在以下部分中进行了检查

if (currentWordCount < Globals.MAX_NON_OVERLAPPING_WORDS_THRESHOLD && overlapped)
    return false;

else if (currentWordCount >= Globals.MAX_NON_OVERLAPPING_WORDS_THRESHOLD && !overlapped)
    return false;

在满足这些条件后,现在是时候检查单词是否真的可以在给定方向的当前轴上放置了。

这部分讨论了ACROSS单词的逻辑,称为leftFreetopFreebottomFreerightMostFree

有两种检查——一种是ACROSS单词的开头和结尾不能有任何字母。leftFreerightMostFree标志通过它们调用的方法确认这一点。例如,leftFree标志由方法“LeftCellFreeForRightDirectedWord”确定,该方法具有以下代码

if (x == 0) return true;
if (x - 1 >= 0)
    return matrix[x - 1, y] == '\0';
return false;

在这里,(x, y) 是单词要横向放置的轴。现在,如果它是最左边的列(x = 0),则无需检查左侧单元格是否为空,因为没有左侧单元格。否则,它会检查 x 的左侧单元格是否为空。

类似地,对这个ACROSS单词最右侧单元格的空闲检查由“RightMostCellFreeForRightDirectedWord”方法中的以下代码确定

if (x + word.Length == Globals.gridCellCount) return true;
if (x + word.Length < Globals.gridCellCount)
    return matrix[x + word.Length, y] == '\0';
return false;

首先,它检查单词的最后一个字母是否到达矩阵的最右列。如果到达最右单元格,则无需进一步检查最右字母,因为没有更右边的单元格。否则,它检查单词的下一个最右单元格是否为空。

对于ACROSS单词,顶部和底部单元格空闲的检查要复杂得多。让我们看看“TopCellFreeForRightDirectedWord”方法中发生了什么。

if (y == 0) return true;
bool isValid = true;
if (y - 1 >= 0)
{
    for (int i = 0; i < word.Length; x++, i++)
    {
        if (matrix[x, y - 1] != '\0')
            isValid = LegitimateOverlapOfAnExistingWord(x, y, word, Direction.Up);
        if (!isValid) break;
    }
}
return isValid;

首先,它检查单词是否要放置在矩阵最顶部的单元格(y = 0)上。如果是这种情况,则不再需要检查顶单元格。否则,对于单词的每个字母检查,顶单元格是否为空(matrix[x, y - 1] != '\0')。如果不为空,则检查上方的字母是否是另一个单词的一部分,该单词必须满足三个条件

  1. 该字母属于板上的现有单词。
  2. 该板上的其他单词也不能是ACROSS
  3. 该上方的字母不是板上现有单词的最后一个字母。

现在让我们检查“LegitimateOverlapOfAnExistingWord”方法中的Up情况

while (--y >= 0)
    if (matrix[x, y] == '\0') break; // First walk upwards until you reach 
                                     //the beginning of the word that is already on the board.
++y;

for (int i = 0; y < Globals.gridCellCount && 
     i < Globals.MAX_WORD_LENGTH; y++, i++) // Now walk downwards until you reach the end 
                                            // of the word that is already on the board.
{
    if (matrix[x, y] == '\0') break;
    chars[i] = matrix[x, y];
}

str = new string(chars);
str = str.Trim('\0');
wordOnBoard = (RegularWordDetails)wordDetails.Find
              (a => a.Word == str);     // See if the characters form a valid word 
                                        //that is already on the board.
if (wordOnBoard == null) return false;  // If this is not a word on the board, 
                                        // then this must be some random characters, 
                                        // hence not a legitimate word, 
                                        // hence this is a wrong placement.
if (wordOnBoard.WordDirection == Direction.Right) return false;  // If the word on the board 
                                        // is in parallel to the word on to be placed, 
                                        // then also this is a wrong placement as 
                                        // two words cannot be placed side by side 
                                        // in the same direction.
if (wordOnBoard.Y + wordOnBoard.Word.Length == originalY) return false; // The word on the 
                                        // board starts right below the y-coordinate 
                                        // for the current word to place. Hence illegitimate.
return true;                            // Else, passed all validation checks for a 
                                        // legitimate overlap, hence return true.

第一个WHILE循环向上移动以查找板上现有单词的开头。

然后FOR循环从该起点向下遍历,并在chars中构建一个单词。

然后从chars数组创建一个string str。它还会截断空白('\0')。

然后它检查该单词是否是板上的合法现有单词(上面 3 个条件中的第 1 个)。如果不是,则返回false

它检查该单词是否也是ACROSS单词。如果是ACROSS,那么当前单词也不能放在那里(上面 3 个条件中的第 2 个)。

它检查板上的现有单词是否恰好停在当前放置索引y的顶部单元格上方(上面 3 个条件中的第 3 个)。

如果所有三个条件都满足,那么这就是当前单词与现有单词合法交叉重叠。

进行类似的检查,以确保如果ACROSS单词的底部单元格有字母,那么它们共同形成一个有效的交叉。这在“BottomCellFreeForRightDirectedWord”方法中完成。

在满足四个标志后,这意味着当前单词可以在给定的轴(x, y)和给定的方向上放置。因此,它被放置在单词矩阵中,并且详细信息通过方法“SaveWordDetailsInCollection”保存在“RegularWordDetails”对象中。这在“GameEngine”类中的“PlaceTheWord”方法的以下部分中完成。

for (int i = 0, j = x; i < word.Length; i++, j++)
    matrix[j, y] = word[i];
SaveWordDetailsInCollection(word, wordMeaning, x, y, direction, attempts, false);

记住,对于 Unicode,我们在字符矩阵中还有一个维度。对于常规单词,我们在矩阵中放置单个字母,而对于 Unicode,我们需要放置复合字母(由几个 Unicode 组成)。这在“BanglaUnicodeGameEngine”类中的“PlaceTheWord”方法的以下部分中完成。

SaveWordDetailsInCollection(word, wordMeaning, x, y, direction, attempts, false);
for (int i = 0; i < unicodeLetters.Count; i++, x++)
{
    char[] atomElements = unicodeLetters[i].ToArray();
    int z = 0;
    foreach (char c in atomElements)
        matrix[x, y, z++] = c;
}               

对于DOWN单词,逻辑类似,因此不进行讨论以减少文章长度。

标记孤立单词

最低要求是,矩阵中不应有孤立的单词,因为每个单词都应该与*至少*另一个WORDCROSS。因此,在放置完成后,还会进行另一项检查,以标记“RegularWordDetails”*object*的Isolated标志。这在*“CheckIfTheWordIsIsolatedAndFlagAccordingly”*方法中完成。对于ACROSS单词,它只是沿着单词的顶部和底部单元格进行扫描;如果沿单词的任何顶部/底部单元格中至少有一个字母,则标志为false(因为这意味着单词不是孤立的)。

TOP单元格的空白检查在以下部分中完成。首先检查当前单词的 Y 轴是否不是第一行(如果是第一行,则无需检查上一行,因为没有上一行)。然后,它从左到右(x 递增)沿着单词进行扫描,并检查每个顶部单元格是否为空。如果在任何点发现顶部单元格中有字母,则将flag设置为false并立即返回。

if (wrd.Y > 0)                                    // If there is a row of cells 
                                                  // to the top of the right-directed word.
    for (int x = wrd.X, y = wrd.Y - 1, 
         i = 0; i < wrd.Word.Length; x++, i++)    // Walk rightwards along the top row 
                                                  // of the word.
        if (matrix[x, y] != '\0')                 // And see if there is any character 
                                                  // to any cell of that row.
        {                                         // Which would mean another word 
                                                  // passed through; 
                                                  // hence this is not isolated.
            wrd.Isolated = false;
            return;
        }

类似地,底部单元格的空白检查在以下部分中完成。首先,它检查当前单词的 Y 轴是否不是最后一行(如果是最后一行,则无需检查上一行,因为没有上一行)。然后它从左到右(x 递增)沿着单词进行扫描,并检查每个底部单元格是否为空。如果在任何点发现底部单元格中有字母,则将标志设置为false并立即返回。

if (wrd.Y < Globals.gridCellCount - 1)            // If there is a row of cells to 
                                                  // the bottom of the right-directed word.
    for (int x = wrd.X, y = wrd.Y + 1, 
         i = 0; i < wrd.Word.Length; x++, i++)    // Walk rightwards along the bottom row 
                                                  // of the word.
        if (matrix[x, y] != '\0')                 // And see if there is any character 
                                                  // to any cell of that row.
        {                                         // Which would mean another word 
                                                  // passed through; 
                                                  // hence this is not isolated.
            wrd.Isolated = false;
            return;
        }

如果两个扫描都完成并且代码没有从中返回,则意味着单词的顶部和底部单元格中没有字母。所以这绝对是一个孤立的单词。因此,它在“RegularWordDetails”对象中被相应地标记,并且单词在单词矩阵中被擦除(设置为'\0')以防止渲染它们(不显示它们)。这在以下部分中完成

if (!wrd.FailedMaxAttempts)
    wrd.Isolated = true;

if (wrd.WordDirection == Direction.Right)
    for (int i = 0, x = wrd.X, y = wrd.Y; i < wrd.Word.Length && 
                                          i < Globals.gridCellCount; i++, x++)
        matrix[x, y] = '\0';

对于 Unicode,逻辑相同。但是还有一点需要牢记。是什么?你猜对了——还有一个三维需要考虑。这部分不讨论以减少文章长度,并且应该对读者来说很容易理解。

一些 LINQ 查询

LINQ 在项目中得到了广泛使用——用于在字典集合中搜索键值对或在列表中查找元素。以下是一个 LINQ 查询,用于获取具有相同起始轴的单词列表

var wordsStartingAtSameAxes = from j in detailsCopy
                              group j by new { j.X, j.Y } into d
                              where d.Count() > 1
                              select (d).ToList();

LINQ 也用于克隆现有列表

detailsCopy = new List<IDetails>(wordDetails.Select(x => x).ToList());

自动窗口缩放和调整大小

自动窗口调整大小可以在加载事件或重置事件中完成。这两个事件都在不同的窗体中使用,以证明它们都可以使用。

应用了自动窗口缩放,使其与分辨率无关。设计时分辨率为 1680x1050。然而,分辨率越高,打印质量越好。自动窗口缩放的技巧超出了本文的范围,请参考这里

检查常规和 Unicode 的混合

2.0 版本提供了输入和保存自定义单词的选项。但是,混合使用常规单词和 Unicode 单词显然没有意义。通常,用户不会这样做,但它仍然确保用户没有这样做。这在“CreateAndSaveOwnWords”类的“GetEncoding”方法中进行了检查。

首先,它将单词的每个代码分开——无论是常规的还是 Unicode 的。对于常规字母,代码必须在 65 到 255 之间(含)。因此,如果第一个代码是常规的,那么其他字母(以及所有单词)中的所有其他代码都应该是常规的。类似地,如果第一个代码是孟加拉语 Unicode(在 0x0980 和 0x09fe 之间,含),那么其他字母(以及所有单词)的后续代码都应该在此范围内。值得注意的是,对于其他 Unicode 单词,范围会有所不同,编码器需要根据相应的 Unicode 页面进行更改。

WordTypes type = WordTypes.Unknown;
WordTypes prevType = WordTypes.Unknown;
foreach (KeyValuePair<string, string> kvp in wordAndClue)
{
    char[] ch = kvp.Key.ToCharArray();
    if (ch[0] >= 65 && ch[0] <= 255)
        prevType = WordTypes.Regular;
    else if (ch[0] >= 0x0980 && ch[0] <= 0x09fe)  // Refer to Bangla Unicode chart: 
        // http://www.unicode.org/charts/PDF/U0980.pdf, modify the code range for 
        // other unicode letters.
        prevType = WordTypes.Unicode;

    for (int i = 1; i < ch.Length; i++)
    {
        if (ch[i] >= 65 && ch[i] <= 255)
            type = WordTypes.Regular;
        else if (ch[i] >= 0x0980 && ch[i] <= 0x09fe)    // Refer to Bangla Unicode chart: 
                                          // http://www.unicode.org/charts/PDF/U0980.pdf, 
                                          // modify the code range for other unicode letters.
            prevType = WordTypes.Unicode;

        if (type != prevType) return WordTypes.Mix;
        prevType = type;
    }
}
return type;

关注点

如果我们考虑工作流程,则序列如下

  1. 代码加载一个包含大约 86,000 个单词的 JSON 单词字典
  2. 将它们解析到一个集合中
  3. 从中随机选取单词
  4. 将它们放置在矩阵中
  5. 一些单词在尝试 200,000 次后仍未找到位置;它们被标记为失败
  6. 进行另一次扫描以标记孤立的单词
  7. 最后,图形渲染器将矩阵渲染到显示器上

所有这些活动都在眨眼之间完成。感谢处理器、编译器以及最终的技术。

显而易见,Unicode 逻辑比常规单词花费的时间稍长,因为 Unicode 逻辑处理一个额外的维度。

故障

如果发现任何问题,请在评论中提出。

限制

有一些严格的填字游戏规则,例如板上的所有单词都应相互连接;不应有孤立的单词组。Mr. Crossworder 不符合此规则,因此板上可能存在孤立的单词组。

免责声明

我不是性别歧视者,女士们不应该讨厌我起这个名字,哈哈。我只是在听 Steve Perry(Journey 乐队)的《Trial by Fire》时,偶然看到了这句歌词

“Hello Mr. Moon,
Can I have some time with you?”

只是为了模仿

“Hello Mr. Crossworder,
Can I have some time with you?”

未来工作

软件永远不会达到顶峰;总有改进的空间。此外,这只是一个原型。有很多事情可以做。

  1. 逻辑本身可以被修改和优化。事实上,大学里的老师可以将它作为优化问题交给学生。目前存在分散的组,更好的算法可能会将它们拉得更近。尤其是对于 Unicode 语言,单词似乎比预期更稀疏。
  2. 该应用程序可以扩展为一个 Web 应用程序,以使用在线 Web 字典。有一些在线 Web 字典通过 API 公开单词和含义。
  3. 可以有一个单独的 GUI,让用户创建自己的预设单词并将其保存在磁盘上。GUI 还应支持加载这些预设。(这在第二个版本中已实现。)
  4. 对于孟加拉语 Unicode,线索的索引和板上的数字仍然是英文的;我将把它留给用户作为输出孟加拉语的练习。
  5. 这不是按照最高设计理念编码的。我更关注逻辑并将其作为一个初始原型实现。有很多编码标准和最佳实践可以也应该被实现。
  6. 该项目以非规范化的形式编码——有很多代码可以压缩。这种非规范化的目的是为了理解正在发生的事情。在达到目的后,代码库可以进一步压缩。例如,检查DOWN单词左侧或右侧单元格的空闲情况大部分是相似的,可以通过少量调整和参数压缩到一个方法中。但是这种压缩会剥夺读者对目的的理解。因此,它就这样保留着,压缩留给他们作为练习。
  7. 这听起来可能过于乐观,但何不应用机器学习或人工智能算法来更有效地工作呢?
  8. 该项目一直处理到三维。添加第四维怎么样?(没关系,开玩笑!)

摘要

这是一个基于预定义词典单词集的填字游戏创建器。它还尝试了另一种人类语言(孟加拉语),该语言有自己的 Unicode。不同的语言有自己的 Unicode 页面,每种语言在语义和结构上都与其他语言不同。然而,本项目提供了一个关于如何将分割逻辑扩展到不同人类语言的思路。

参考文献

历史

  • 2018年12月14日:首次发布
  • 2019年1月7日:第二次发布
    • 添加了用于创建自己的单词-线索的菜单,以及加载先前保存的单词-线索 JSON 文件。
    • 在创建最终填字游戏板时存在一个错误,它会从列表中移除孤立和失败的单词。这通过克隆列表来完成。更改位于*MainBoard.cs*文件中的“createCrosswordToolStripMenuItem_Click()”方法。
    • 在文章中添加了“工作原理”部分
    • 代码一瞥”部分增加了对代码的更多解释
    • 添加了更多参考资料
© . All rights reserved.