如果给你一列单词,你会如何将它们排列成一个填字游戏的表格呢?
这个填字游戏的表格并不需要像传统的填字游戏那样对称或者其他特殊要求。你只需要输出每个单词的起始位置和方向即可。
如果给你一列单词,你会如何将它们排列成一个填字游戏的表格呢?
这个填字游戏的表格并不需要像传统的填字游戏那样对称或者其他特殊要求。你只需要输出每个单词的起始位置和方向即可。
我想到了一种解决方案,可能不是最高效的,但足够好用。基本上:
这样可以制作出一个可用但通常相当差的纵横填字游戏。我对上述基本方法进行了一些修改,以得到更好的结果。
我最近用Python写了一个交叉字谜生成器。你可以在这里找到它:http://bryanhelmig.com/python-crossword-puzzle-generator/。它不会创建密集的纽约时报风格的交叉字谜,而是像你在孩子的谜题书中发现的那种。
与我发现的一些实现随机暴力布局单词方法的算法不同,我试图实现一种稍微更加聪明的暴力方法来布置单词。以下是我的过程:
到最后,你会得到一个不错的交叉字谜或者文字搜索谜题,因为它们几乎是一样的。它倾向于运行得相当不错,但如果你有任何改善建议,请告诉我。更大的网格运行速度呈指数级下降;更大的单词列表线性下降。更大的单词列表也有更高的机会获得更好的单词布局。
array.sort(key=f)
是稳定的,这意味着(例如)仅通过长度对字母单词列表进行排序将保持所有8个字母单词按字母顺序排序。 - Lynn大约十年前,我编写了一个填字游戏生成程序(它是加密的,但同样的规则也适用于普通填字游戏)。
程序中有一个单词列表(以降序存储使用频率),其中包含与之相关的提示,这些单词和提示存储在文件中。从客户端提供的池中随机选择一个模板(基本上是代表黑色方块和空白方块的位掩码)。
然后,对于谜题中每个非完整单词(基本上是找到第一个空白方块,并查看右侧(横向)或下方(竖向)是否也为空白方块),会在文件中进行搜索,寻找第一个符合条件的单词,考虑已经存在该单词中的字母。如果没有符合条件的单词,则将整个单词标记为不完整并继续下一步。
最后,会有一些未完成的单词需要手动填入(如果需要,还要将单词和提示添加到文件中)。如果他们想不出任何主意,可以手动编辑填字游戏,修改限制条件或者请求完全重新生成。
一旦单词/提示文件达到一定大小(对于这个客户端,每天添加50-100个提示),每个填字游戏很少需要超过两到三个手动修正。
这个算法可以在60秒内创建50个密集的6x9 箭头填字游戏。它使用一个单词数据库(包含单词和提示)和一个棋盘数据库(带有预配置的棋盘)。
1) Search for all starting cells (the ones with an arrow), store their size and directions
2) Loop through all starting cells
2.1) Search a word
2.1.1) Check if it was not already used
2.1.2) Check if it fits
2.2) Add the word to the board
3) Check if all cells were filled
一个更大的单词数据库可以显著减少生成时间,而某些类型的游戏板更难填充!更大的游戏板需要更多时间才能正确填充!
示例:
预配置的6x9板:
(# 表示一个单元格中有一个提示,% 表示一个单元格中有两个提示,箭头未显示)
# - # # - % # - #
- - - - - - - - -
# - - - - - # - -
% - - # - # - - -
% - - - - - % - -
- - - - - - - - -
生成的6x9棋盘:
# C # # P % # O #
S A T E L L I T E
# N I N E S # T A
% A B # A # G A S
% D E N S E % W E
C A T H E D R A L
提示 [行,列]:
[1,0] SATELLITE: Used for weather forecast
[5,0] CATHEDRAL: The principal church of a city
[0,1] CANADA: Country on USA's northern border
[0,4] PLEASE: A polite way to ask things
[0,7] OTTAWA: Canada's capital
[1,2] TIBET: Dalai Lama's region
[1,8] EASEL: A tripod used to put a painting
[2,1] NINES: Dressed up to (?)
[4,1] DENSE: Thick; impenetrable
[3,6] GAS: Type of fuel
[1,5] LS: Lori Singer, american actress
[2,7] TA: Teaching assistant (abbr.)
[3,1] AB: A blood type
[4,3] NH: New Hampshire (abbr.)
[4,5] ED: (?) Harris, american actor
[4,7] WE: The first person of plural (Grammar)
加载可用的单词列表(按单词长度排序,例如2,3,..,20)
在用户构建的网格上找到单词插槽(即网格中的单词)(例如x,y处具有长度L的单词,水平或垂直)(复杂度为O(N))
计算需要填充的网格单词的交点(复杂度为O(N^2))
计算单词列表中的单词与所使用的各个字母的交集(这允许通过使用模板(例如Sik Cambon thesis as used by cwc)搜索匹配的单词)(复杂度为O(WL*AL))
步骤.3和.4允许完成此任务:
a. 网格单词之间的交叉点可以创建一个“模板”,以尝试在此网格单词的关联可用单词列表中查找匹配项(通过使用已经在算法某个步骤中填充的其他交叉单词的字母)。
b. 单词表中的单词与字母表的交集可以找到与给定“模板”相匹配的匹配(候选)单词(例如,第1个位置为'A',第3个位置为'B'等)。因此,实现了这些数据结构后,使用的算法类似于:
注意:如果网格和单词数据库是恒定的,则可以仅执行上述步骤一次。
算法的第一步是随机选择一个空白单元格(网格单词),并从其关联的单词列表中填入候选单词(随机化使得在算法的连续执行中产生不同的解决方案)(复杂度为O(1)或O(N))。
对于每个仍为空的单词槽(与已填充单词槽有交叉),计算一个约束比率(这可以变化,简单的方法是在该步骤可用的解决方案数量),并按此比率对空单词槽进行排序(复杂度为O(NlogN)或O(N))。
循环遍历上一步计算出的空白单词槽,并为每个单词尝试多个候选解(确保“弧一致性”保持不变,即如果使用该单词,则网格在此步骤后具有解决方案),并根据下一步的最大可用性进行排序(即如果该单词在那个时间和地点使用,则下一步具有最大可能的解决方案等)(复杂度为O(N*MaxCandidatesUsed))。
填写该单词(标记为填写并转到步骤2)。
如果没有找到符合第3步标准的单词,则尝试回溯到先前某个步骤的其他候选解决方案(此处标准可能会有所变化)(复杂度为O(N))。
如果找到回溯,则使用替代方案,并可选择重置可能需要重置的任何已填充单词(将它们标记为未填充)(复杂度为O(N))。
如果没有找到回溯,则找不到解决方案(至少是在该配置、初始种子等情况下)。
否则,当所有单词槽都被填满时,就有一个解决方案。
为什么不先使用随机概率方法呢?从一个单词开始,然后反复选择一个随机单词,并尝试将其适配到当前的谜题状态中,而不会破坏大小等约束条件。如果失败了,就重新开始。
您会惊讶地发现,像这样的蒙特卡罗方法经常奏效。
这是基于nickf和Bryan Python代码的JavaScript代码,如果有其他人需要js代码,我将其发布在此。
function board(cols, rows) { //instantiator object for making gameboards
this.cols = cols;
this.rows = rows;
var activeWordList = []; //keeps array of words actually placed in board
var acrossCount = 0;
var downCount = 0;
var grid = new Array(cols); //create 2 dimensional array for letter grid
for (var i = 0; i < rows; i++) {
grid[i] = new Array(rows);
}
for (var x = 0; x < cols; x++) {
for (var y = 0; y < rows; y++) {
grid[x][y] = {};
grid[x][y].targetChar = EMPTYCHAR; //target character, hidden
grid[x][y].indexDisplay = ''; //used to display index number of word start
grid[x][y].value = '-'; //actual current letter shown on board
}
}
function suggestCoords(word) { //search for potential cross placement locations
var c = '';
coordCount = [];
coordCount = 0;
for (i = 0; i < word.length; i++) { //cycle through each character of the word
for (x = 0; x < GRID_HEIGHT; x++) {
for (y = 0; y < GRID_WIDTH; y++) {
c = word[i];
if (grid[x][y].targetChar == c) { //check for letter match in cell
if (x - i + 1> 0 && x - i + word.length-1 < GRID_HEIGHT) { //would fit vertically?
coordList[coordCount] = {};
coordList[coordCount].x = x - i;
coordList[coordCount].y = y;
coordList[coordCount].score = 0;
coordList[coordCount].vertical = true;
coordCount++;
}
if (y - i + 1 > 0 && y - i + word.length-1 < GRID_WIDTH) { //would fit horizontally?
coordList[coordCount] = {};
coordList[coordCount].x = x;
coordList[coordCount].y = y - i;
coordList[coordCount].score = 0;
coordList[coordCount].vertical = false;
coordCount++;
}
}
}
}
}
}
function checkFitScore(word, x, y, vertical) {
var fitScore = 1; //default is 1, 2+ has crosses, 0 is invalid due to collision
if (vertical) { //vertical checking
for (i = 0; i < word.length; i++) {
if (i == 0 && x > 0) { //check for empty space preceeding first character of word if not on edge
if (grid[x - 1][y].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
} else if (i == word.length && x < GRID_HEIGHT) { //check for empty space after last character of word if not on edge
if (grid[x+i+1][y].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
if (x + i < GRID_HEIGHT) {
if (grid[x + i][y].targetChar == word[i]) { //letter match - aka cross point
fitScore += 1;
} else if (grid[x + i][y].targetChar != EMPTYCHAR) { //letter doesn't match and it isn't empty so there is a collision
fitScore = 0;
break;
} else { //verify that there aren't letters on either side of placement if it isn't a crosspoint
if (y < GRID_WIDTH - 1) { //check right side if it isn't on the edge
if (grid[x + i][y + 1].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
if (y > 0) { //check left side if it isn't on the edge
if (grid[x + i][y - 1].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
}
}
}
} else { //horizontal checking
for (i = 0; i < word.length; i++) {
if (i == 0 && y > 0) { //check for empty space preceeding first character of word if not on edge
if (grid[x][y-1].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
} else if (i == word.length - 1 && y + i < GRID_WIDTH -1) { //check for empty space after last character of word if not on edge
if (grid[x][y + i + 1].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
if (y + i < GRID_WIDTH) {
if (grid[x][y + i].targetChar == word[i]) { //letter match - aka cross point
fitScore += 1;
} else if (grid[x][y + i].targetChar != EMPTYCHAR) { //letter doesn't match and it isn't empty so there is a collision
fitScore = 0;
break;
} else { //verify that there aren't letters on either side of placement if it isn't a crosspoint
if (x < GRID_HEIGHT) { //check top side if it isn't on the edge
if (grid[x + 1][y + i].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
if (x > 0) { //check bottom side if it isn't on the edge
if (grid[x - 1][y + i].targetChar != EMPTYCHAR) { //adjacent letter collision
fitScore = 0;
break;
}
}
}
}
}
}
return fitScore;
}
function placeWord(word, clue, x, y, vertical) { //places a new active word on the board
var wordPlaced = false;
if (vertical) {
if (word.length + x < GRID_HEIGHT) {
for (i = 0; i < word.length; i++) {
grid[x + i][y].targetChar = word[i];
}
wordPlaced = true;
}
} else {
if (word.length + y < GRID_WIDTH) {
for (i = 0; i < word.length; i++) {
grid[x][y + i].targetChar = word[i];
}
wordPlaced = true;
}
}
if (wordPlaced) {
var currentIndex = activeWordList.length;
activeWordList[currentIndex] = {};
activeWordList[currentIndex].word = word;
activeWordList[currentIndex].clue = clue;
activeWordList[currentIndex].x = x;
activeWordList[currentIndex].y = y;
activeWordList[currentIndex].vertical = vertical;
if (activeWordList[currentIndex].vertical) {
downCount++;
activeWordList[currentIndex].number = downCount;
} else {
acrossCount++;
activeWordList[currentIndex].number = acrossCount;
}
}
}
function isActiveWord(word) {
if (activeWordList.length > 0) {
for (var w = 0; w < activeWordList.length; w++) {
if (word == activeWordList[w].word) {
//console.log(word + ' in activeWordList');
return true;
}
}
}
return false;
}
this.displayGrid = function displayGrid() {
var rowStr = "";
for (var x = 0; x < cols; x++) {
for (var y = 0; y < rows; y++) {
rowStr += "<td>" + grid[x][y].targetChar + "</td>";
}
$('#tempTable').append("<tr>" + rowStr + "</tr>");
rowStr = "";
}
console.log('across ' + acrossCount);
console.log('down ' + downCount);
}
//for each word in the source array we test where it can fit on the board and then test those locations for validity against other already placed words
this.generateBoard = function generateBoard(seed = 0) {
var bestScoreIndex = 0;
var top = 0;
var fitScore = 0;
var startTime;
//manually place the longest word horizontally at 0,0, try others if the generated board is too weak
placeWord(wordArray[seed].word, wordArray[seed].displayWord, wordArray[seed].clue, 0, 0, false);
//attempt to fill the rest of the board
for (var iy = 0; iy < FIT_ATTEMPTS; iy++) { //usually 2 times is enough for max fill potential
for (var ix = 1; ix < wordArray.length; ix++) {
if (!isActiveWord(wordArray[ix].word)) { //only add if not already in the active word list
topScore = 0;
bestScoreIndex = 0;
suggestCoords(wordArray[ix].word); //fills coordList and coordCount
coordList = shuffleArray(coordList); //adds some randomization
if (coordList[0]) {
for (c = 0; c < coordList.length; c++) { //get the best fit score from the list of possible valid coordinates
fitScore = checkFitScore(wordArray[ix].word, coordList[c].x, coordList[c].y, coordList[c].vertical);
if (fitScore > topScore) {
topScore = fitScore;
bestScoreIndex = c;
}
}
}
if (topScore > 1) { //only place a word if it has a fitscore of 2 or higher
placeWord(wordArray[ix].word, wordArray[ix].clue, coordList[bestScoreIndex].x, coordList[bestScoreIndex].y, coordList[bestScoreIndex].vertical);
}
}
}
}
if(activeWordList.length < wordArray.length/2) { //regenerate board if if less than half the words were placed
seed++;
generateBoard(seed);
}
}
}
function seedBoard() {
gameboard = new board(GRID_WIDTH, GRID_HEIGHT);
gameboard.generateBoard();
gameboard.displayGrid();
}
我在使用填字游戏生成引擎时,发现了以下最重要的内容:
0.!/usr/bin/python
a. allwords.sort(key=len, reverse=True)
b. make some item/object like cursor which will walk around matrix for easy orientation unless you want to iterate by random choice later on.
the first, pick up first pair and place them across and down from 0,0 ; store the first one as our current crossword 'leader'.
move cursor by order diagonal or random with greater diagonal probability to next empty cell
iterate over the words like and use free space length to define max word length :
temp=[]
for w_size in range( len( w_space ), 2, -1 ) :
# t
for w in [ word for word in allwords if len(word) == w_size ] :
#
if w not in temp and putTheWord( w, w_space ) :
#
temp.append( w )
to compare word against free space I used i.e. :
w_space=['c','.','a','.','.','.'] # whereas dots are blank cells
# CONVERT MULTIPLE '.' INTO '.*' FOR REGEX
pattern = r''.join( [ x.letter for x in w_space ] )
pattern = pattern.strip('.') +'.*' if pattern[-1] == '.' else pattern
prog = re.compile( pattern, re.U | re.I )
if prog.match( w ) :
#
if prog.match( w ).group() == w :
#
return True
after each successfully used word, change direction. Loop while all cells are filled OR you run out of words OR by limit of iterations then :
# 更改所有单词列表
leading_w的索引 = allwords.index( leading_w )
allwords = allwords[:inexOf1stWord+1][:] + allwords[inexOf1stWord+1:][:]
...然后再次迭代新的填字游戏。
通过易于填充和一些估算计算成绩系统。 如果当前填字游戏得分符合您的评分系统,则为其打分并将其添加到制作的填字游戏列表中,以缩小后续选择范围。
在第一个迭代会话之后,从制作的填字游戏列表中再次迭代以完成工作。
使用更多参数可以大幅提高速度。
LEMON ERASE
如果“LE”,“ER”和“MA”等不是您列表中的单词,则此处错误。另一方面,直接拒绝这种相邻可能会丢弃真正好的网格,例如:W
LEMON ERASE NEXUS T T - George Armhold