约束传播的乐趣
Constraint Propagation for Fun

原始链接: https://eli.li/constraint-propagation-for-fun

这篇帖子比较了两款绘图逻辑游戏(picross/nonogram)的设计:“Squeakross”在美观上令人愉悦,而作者自己的“bicross”则侧重于解题的确定性。作者在享受“Squeakross”魅力的同时,指出其谜题可能存在模棱两可的解法——特别是“Elementary Switches”,根据线索可能存在多种有效排列。 “Bicross”则采用独特的程序生成系统,旨在*保证*谜题的可解性。它采用三步流程:生成随机网格,计算提示,然后严格测试是否存在*唯一*解。这种测试涉及迭代应用约束传播——根据行和列的提示逻辑推断方块的位置——并丢弃无法通过这种方法明确解出的任何谜题。 这种方法虽然计算量大(生成和拒绝5-10个关卡),但确保玩家无需猜测。作者对这个“蛮力”生成循环的速度感到惊讶,即使限制在10x10网格内,也能实现快速的关卡切换。最终,帖子强调了一种设计权衡: “Squeakross”允许模糊性以提供更轻松的体验,而“bicross”则优先考虑逻辑上的确定性。

黑客新闻 新 | 过去 | 评论 | 提问 | 展示 | 招聘 | 提交 登录 约束传播的乐趣 (eli.li) 3点 由 rickcarlino 3小时前 | 隐藏 | 过去 | 收藏 | 讨论 帮助 指南 | 常见问题 | 列表 | API | 安全 | 法律 | 申请YC | 联系 搜索:
相关文章

原文

I’ve been playing the very good Squeakross this weekend. It is adorable and the aesthetics are absolutely immaculate, but I’ve found the actual picross puzzles to be a point of frustrating friction in the game when compared to the picross-style puzzles in my bicross game.

Picross puzzles, aka nonograms, can relatively easily have ambiguous solutions. Because the hints only tell you how many consecutive blocks are in a row/column, they don’t tell you where they are. If the crossing hints (the perpendicular rows/columns) don’t provide enough information to nail down the blocks, they can float or swap positions while satisfying the clues.

Option A (Diagonal \ ):

[X] [ ] < 1
[ ] [X] < 1
 ^   ^
 1   1

Option B (Diagonal / ):

[ ] [X] < 1
[X] [ ] < 1
 ^   ^
 1   1

This, I learned to write this blog post, is called an Elementary Switch.” Which is a 2 by 2 corner where the hints for both rows are 1 and the hints for both columns are 1. There are two valid ways to solve this, and the clues cannot distinguish between them.

The puzzles in Squeakross have this same problem, and the game accounts for this ambiguity by having a few different states that the hints can be in. I like this approach and find it compelling, but I kinda like the approach I took in bicross more.

A major difference in bicross from other picross-style games that I’ve seen is that the levels in bicross are procedurally generated and don’t correspond to some pixel-based image. In classic picross-style puzzles based on images, it is easy to have ambiguities like the elementary switch.

In games like Squeakross you can kinda guess your way around a situation like this because there’s no negative repercussion in a miss, but in bicross — especially the RPG and daily flavors — you can’t because of the HP system that punishes you for missing. For bicross, I needed provable solvability because I wanted to ensure that no matter the seed, the resulting level would never ask the player to make a random guess.

To fix this, I used a 3 step pipeline that generates a solution first, then tests it.

Step 1, generate a level

It starts with randomness. Using a seeded RNG, I generate a boolean grid. I immediately run a simple filter to make sure the game is in the fun-zone,” a kind of arbitrary density measurement I came up with while play testing. The fun-zone checks to see if the grid’s density ks between 20% and 70%? If not, it’s trash and I throw it out. If yes, it moves on to the next step.

Step 2, constraint propagation

I calculate the hints (the numbers on the side, like 2 . 1 or 5) from the grid, and then throw the grid away. I ask a function called hasUniqueSolution:

Can you reconstruct the grid using ONLY these hints?

This function is kind of the perfect logical player,” using iterative constraint propagation:

  • Permutation Generation: For every row and column, the solver calculates the complete search space—every possible valid arrangement of blocks that satisfies the hints.
  • Intersection: It stacks all those valid permutations on top of each other. If index 3 is filled in every single valid permutation for row 0, then (0,3) must be filled.
  • Pruning: If row 0 forces (0,3) to be filled, the solver zoots to the permutation list for column 3 and deletes any possibilities that don’t have a filled block at row 0.

Step 3, acceptance criteria

This pruning loop repeats, rows restricting columns, columns restricting rows, until it either solves the puzzle or gets stuck. Mathematically, a puzzle can have a unique solution that requires advanced look-ahead or backtracking, where if you place a block here, ten moves later you break the game, but that is some seriously big-brain-chess-playing-shit. Bicross’ solver verifies line solvability, so if it cannot force a solution using simple intersection logic it returns false. If it gets stuck, or if it finds ambiguity, like more than one valid permutation remaining, that level doesn’t get served up to the player. We burn it down and generate a new one.

A downside of this is that it uses CPU cycles. If you peek at the browser’s console, you can see that for every level played, the game usually generates and rejects 5-10 candidates in the background…which is also one of the reasons (the other one being mobile-friendliness) why I don’t generate levels larger than 10 by 10 grids. But I was honestly shocked that this pretty profoundly naive, action-heroically-brutish generate-and-solve loop is fast enough to run in the milliseconds during a level transition.

Proof in the pudding?

const propagate = (rows, cols, iteration) => {
    
    if (iteration >= maxIterations) return { rows, cols };

    
    const newRows = constrainRows(rows, cols);
    const rowsChanged = hasChanged(rows, newRows);
    
    
    const newCols = constrainCols(newRows, cols);
    const colsChanged = hasChanged(cols, newCols);

    
    if (isUnsolvable(newRows, newCols)) return null; 

    
    if (!rowsChanged && !colsChanged) return { rows: newRows, cols: newCols }; 

    
    return propagate(newRows, newCols, iteration + 1);
};

Published

Tags

联系我们 contact @ memedata.com