在 CSS 中生成(并求解!)数独

Avatar of Lee Meyer
Lee Meyer

DigitalOcean 为您的旅程的每个阶段提供云产品。 立即开始使用 $200 免费积分!

我喜欢让 CSS 做它不应该做的事情。 这是您在构建 我的世界中的计算器 时会得到的解决问题的脑力训练,除了您可能不会在与我的世界红石相关的任何工作中获得工作,无论您在这个领域多么出色,而 CSS 技能却值钱,而且许多通用程序员都害怕 CSS,因此学习它可以成为一种与众不同的方式。 此外,当您用 CSS 做到不可能的事情时,所有正常的 CSS 任务似乎都变得容易。

我在网上阅读了关于 CSS 是否是图灵完备语言 以及 CSS 和 HTML 是否算作编程语言 的有趣讨论。 我还没有决定,但我可以说,在寻求以标准方式支持常见 UI 模式方面,一些较新的 CSS 功能模糊了样式和功能之间的界限。

挑战我们自己只用 CSS 和 HTML 来解决逻辑问题,可以迫使我们花时间使用 CSS 的一些更新的、类似于编程的功能,例如 自定义属性逻辑函数。 仍然不清楚如何使用这些功能来构建一个只使用 CSS 的数独求解器,但这个想法听起来很疯狂,数独的基于约束的逻辑似乎可能与 CSS 的声明性相兼容,所以我不惊讶地发现其他人声称已经构建了一个“CSS3 数独解决方案求解器”。 事实证明,这更像是 CSS 中的数独验证器,而不是求解器。 它还使用了一点 JavaScript 来与文本框一起使用。

经过几天努力尝试在纯 CSS 中构建一个完整的数独求解器和生成器应用程序后,我学到了三件事。

  1. 您可以 对 Sass 函数进行单元测试s 和 mixin,这太棒了。 如果您大量使用和重用这些 Sass 功能,而这些功能就是它们存在的意义,那么它们将变得与您的代码库中的任何其他部分一样关键,也一样让人害怕更改。 它们周围应该有测试。
  2. 当您向 Chrome DevTools 投掷 50MB 的 Sass 生成的 CSS 时,它会显示一个无限的死亡旋转器。
  3. 也许将类似于 这个 Python 脚本 的东西转换为纯 CSS 是不可能的。 也许吧。

但是,我们可以实现一个 16 方格数独的数独求解器和生成器应用程序,您可以在下面玩,然后我们将分解其功能的工作原理。 你的上帝在哪里,简单的谜题,专为儿童设计的?

值选择器

由于我们正在尝试使用 CSS,因此我们有合同义务包含一些视觉上有趣的东西,尽管不要太过分,因为数独玩家似乎很欣赏一个不会妨碍他们的 UI。 我认为,在一些数独应用程序中选择数字的方式可以更加直观,因此我决定采用径向菜单 UI 模式,该模式 可以追溯到黑白 Macintosh 的时代,并且在现代电子游戏中仍然很流行。 实际上有人构建了一个 用于径向菜单的不错的纯 CSS 库,但我爱上了 React Planet,因为我喜欢它同时捕捉到用圆圈选择一个项目的方式,以及它如何吸引人地显示可用的操作。 我想看看我是否可以用 CSS 构建类似的效果。

我从 这个 Pen 中获取了一些虚线圆圈代码,然后用标签制作了数字,使用旧的 border-radius: 50% 技巧,然后我使用绝对定位使数字“粘”到虚线圆圈上的正确点,即使动画使其改变大小。

.context .number.top {
  background: green;
  margin-left: auto;
  margin-right: auto;
  left: 0;
  right: 0;
  top: -12.5px;
}

.context .number.left {
  background: orange;
  margin-top: auto;
  margin-bottom: auto;
  top: 0;
  bottom: 0;
  left: -12.5px;
}

动画在使数字选择器的 z-index 变得更高使其可点击的同时淡入数字选择器。 我们还在对顶部和左侧边距进行动画处理,从 50% 变为零,因此圆圈从中心扩展以填充可用空间。

@keyframes bounce-out {
  0% {
    z-index: -1;
    width: 35%;
    height: 35%;
    margin-left: 50%;
    margin-top: 50%;
    opacity: 0;
  }
  100% {
    z-index: 2;
    opacity: 1;
    width: var(--circle-radius);
    height: var(--circle-radius);
  }
}

然后,为了模拟类似于 React Planet 的弹性物理,我在动画上使用了一个 cubic-bezier() 函数。 网站 easings.net 在使缓动函数变得容易方面提供了很大的帮助。

.context {
  animation: bounce-out cubic-bezier(.68,-0.6,.32, 2.5) .5s forwards;        
}

值的选择和为选定方块打开值选择器的行为都使用单选按钮技巧,以记住选择了哪些值并实现互斥性。 CSS-Tricks 有一篇关于复选框和单选按钮技巧的 很棒的文章,所以这里不再重复这些信息,但我将展示如何在数独 CSS 网格级别根据复选框设置 CSS 变量,因为这是此实验工作方式的核心。

由于我们使用的是变量,因此无论何时设置值,我们都可以获得相同的效果,无论用户是选中一个框以指定值,还是谜题生成器为该方块设置相同的值。 方块和值的组合很多,所以我们使用 Sass 而不是手动编写所有组合。 我们还为每个值-方块组合创建单独的位值,以及另一个自定义属性来告诉我们该方块是否未解。 那是因为 CSS 给我们的能力有限,无法将一个值与另一个值进行比较(这是可能的,但可能很棘手)。 我们正在以乍一看可能有点奇怪的方式定义这些值,但在验证一组数独方块值是否可解时,这将使我们的生活变得更容易。

@for $i from 1 through 16 {
  @for $j from 1 through 4 {
    #select-#{$j}-value-square-#{$i}:checked ~ .sudoku  {
       --square-#{$i}-unsolved: 0; 
       --square-#{$i}-equals-#{$j}: 1;
    }
  }
}

验证网格

谷歌医生告诉我们,即使只有 16 个方块,也有 40 亿种可能的四位数组合。 但是,一个暴力破解所有这些组合并输出符合数独规则的有效组合的程序表明,在 4×4 数独中只有 288 个有效解决方案,这与 9×9 网格中可能的有效解决方案数量有很大差异。 只有 288 个可能的解决方案,这就是 Sass 真正发挥作用的地方。 我仍然不确定 CSS 是否是图灵完备语言,但 Sass 是,它为我们提供了一些适当的数据结构,例如列表。 使用一点正则表达式技巧,我们可以将上面链接的有效 4×4 谜题列表转换为 Sass 驱动的二维列表!

$solutions: ((1,2,3,4,3,4,1,2,2,1,4,3,4,3,2,1),(3,1,2,4,2,4,1,3,1,3,4,2,4,2,3,1),(1,2,3,4,3,4,1,2,2,3,4,1,4,1,2,3),/*...many lines later...*/(2,4,3,1,3,1,4,2,4,2,1,3,1,3,2,4),(4,3,2,1,2,1,4,3,3,4,1,2,1,2,3,4));

太棒了! 如果我们的 CSS 技巧是一个多层应用程序,那么这将是我们的数据库。 验证可以使用与我们引言中看到的 9×9 验证器相同的检查行和列值的方法,但由于我们知道答案,所以我们似乎不需要费心检查块、列和行。 相反,我们可以检查输入的数字是否仍然是一个有效的谜题。 在伪代码中,这可能看起来像

foreach (s in squares) 
{
  if (solutionsContains(s.value, s.index) or s.isUnsolved())
  {
    showValidationError();
  } 
} 

请记住,每当选择方块值时,我们都会创建那些奇怪的变量?

--square-#{$i}-unsolved: 0;        
--square-#{$i}-equals-#{$j}: 1;

所以现在我们在上面伪代码中的第 3 行的条件中得到了这两个问题的答案,但我们如何在 CSS 中执行逻辑 OR 运算符? 有一篇关于 CSS-Tricks 上使用 calc() 来模拟 CSS 中逻辑运算符的文章,我不确定如果没有它,我是否会想到数独求解器中的某些代码,但是文章中解释的某些公式变得有点笨拙,尤其是如果您想使用超过两个操作数来进行嵌套的 AND 和 OR。 例如,我们需要这个伪代码的 CSS 等效项

if ((squareOneEqualsOne and squareTwoEqualsTwo /*...*/ and squareSixteenEqualsOne) or (squareOneEqualsOne and squareTwoEqualsThree /*...*/ and squareSixteenEqualsOne))
  {
    sudokuIsValid();
  } 
}

好吧,那篇展示如何使用 calc() 进行逻辑运算的文章是在 2019 年写的。 如今,除了 calc() 之外,我们还有支持良好的 min()max() 数学函数,它们更好地满足了我们的需求。 如果您在 Google 上搜索“CSS min、max 和 clamp”(最后一个只是方便的 min()max() 组合的语法糖),您会发现许多示例展示了如何使用它们来 简化流体排版。 这是一个引人注目的用例,但您可以在任何使用数字的地方使用这些数学函数,这为 CSS 添加了很多功能。 例如,如果您将位标志变量传递给 CSS min(),则等效于 AND。 如果您将相同的标志传递给 CSS max(),则等效于 OR。 我们可以使用以下 真值表 来证明这一点。

ABA AND Bmin(A, B)
0000
1000
0100
1111
ABA OR Bmax(A, B)
0000
1011
0111
1111

我们可以用它来实现非常复杂的逻辑,尤其是在添加了我们可以执行 calc() 可以执行的任何操作的 min()max() 中的帮助事实的情况下。 CSS 距离成为自己的奇怪脚本语言又近了一步。 现在,我们可以用 CSS 实现上面验证伪代码中的条件。(在实践中,我们从 Sass 中生成它,因为它是非常重复的。)

.sudoku { 
  --square-1-matches-puzzle-1: max(var(--square-1-unsolved), var(--square-1-equals-1, 0));
  --square-2-matches-puzzle-1: max(var(--square-2-unsolved), var(--square-2-equals-2, 0));
  /*...*/
  --square-16-matches-puzzle-1: max(var(--square-16-unsolved), var(--square-16-equals-1, 0));
  --puzzle-1-found: min(var(--square-1-matches-puzzle-1), 
  /*...*/ 
  var(--square-16-matches-puzzle-1));
  --solution-found: max(var(--puzzle-1-found), /*...*/ var(--puzzle-288-found));
}

通过检查每个方块是否未解或具有与 Sass 2D 列表中预先计算的解决方案中的相同位置的值,我们可以生成一个变量来告诉我们当前定义的方块是否存在于有效的 4×4 数独谜题中。 现在,只要我们可以找到一些可以驱动 CSS 行为的数字,我们就可以将该 CSS 行为基于 --solution-found。 例如,为了使网格在无效时变为红色,我们可以在每个方块中放置以下内容

.square {
  color: rgb(calc(255 * (1 - var(--solution-found))), 0, 0);
}

并非所有 CSS 属性都能由数字驱动,但许多可以,而且 `z-index` 和 `opacity` 特别适合这种使用场景。其他行为可能更棘手,但通常也可以实现。例如,我一度卡在如何仅仅用一个数字位标志属性来触发无效网格的抖动动画,以便网格在任何时候变得无效时都会抖动,但这正是 CSS 黑客技术迫使你阅读规范并理解每个属性的边缘案例的绝佳例子。我在关于 `animation-duration` 的页面上找到了解决方案。

值为 `0s`(默认值)表示不应该进行任何动画。

因此,我们可以根据 `--solution-found` 设置抖动动画的动画时长,并使用 `:active` 伪类在每次点击数字时移除动画,以便在解决方案变得无效时重新播放动画,否则不执行任何操作。

#select-#{$j}-value-square-#{$i}:active  { 
  animation: none;
}

 #select-#{$j}-value-square-#{$i}:checked ~ .sudoku {
  animation: shake cubic-bezier(.36,.07,.19,.97) calc((clamp(0, 1 - var(--solution-found), 1)) * 1s) forwards;
}

如果没有 CSS 自定义属性,纯 CSS 数独应用程序可能是不可能的,而且它们比乍看之下更强大。它们在依赖属性发生改变时重新评估并更新 UI 的方式,就像来自 Vue 这样的高级 JavaScript 框架的响应式机制的简化版一样。可以说,响应式机制以 CSS 变量的形式内置在浏览器中!

现在,我们已经有了这种验证方法,并且我们的样式表在任何时候设置 Sudoku 中的有效值时都会在潜意识里知道解决方案,我们离实现求解器不远了!

解决每个 4×4 数独

还记得我们介绍这些中间变量的时候吗?

.sudoku { 
  --puzzle-1-found: min(var(--square-1-matches-puzzle-1), 
  /*...*/ 
  var(--square-16-matches-puzzle-1));
}

那不仅仅是为了使验证代码更容易编写和理解。知道 288 种可能的谜题中哪一种与之匹配,可以让我们编写求解器!

#no-solution {
  z-index: 1;
  color: red;
}
@for $solution-index from 1 through 288 {
label[for=solution-#{$solution-index}] {
  cursor: pointer;
  z-index: calc(var(--puzzle-#{$solution-index}-found) * #{$solution-index});
}

#solution-#{$solution-index}:checked ~ .sudoku  {
  @for $square from 1 through 16 {
    --square-#{$square}-solution:"#{nth(nth($solutions, $solution-index), $square)}";
    --square-#{$square}-color: grey;
    --auto-#{$square}: 1;
  }

我在上面单词“谜题(s)”中添加了可选的复数,因为如果用户没有填写很多方格,那么可能存在多个有效解。我挖出了像 这个 JavaScript 求解器 这样的求解器,它可以快速生成解决方案,即使你没有指定足够的值供人类在不猜测的情况下解决它。

我的 CSS 求解器的诀窍在于,虽然“求解”按钮看起来像一个按钮,但实际上是 288 个单选按钮标签叠放在一起,但它们看起来都一样。想象一堆卡片:它们背面都有相同的图案,但正面有不同的值。求解器逻辑使用 `z-index` 将带有解决方案的卡片放在最上面,所以如果你把它拿起来看看另一面,你总是会得到正确的解决方案。即使存在多个正确解决方案,它仍然有效,因为在我们的有效答案列表中出现的较晚的解决方案将被放在最上面,因为我们通过将标志乘以 `$solution-index` 来计算 `z-index`。如果没有任何解决方案匹配,所有求解按钮的 `z-index` 将为零,由于禁用按钮(带有“无效谜题”消息)的 `z-index` 为 1,因此它将出现在最上面。如果谜题 1 是解决方案,我们仍然会看到谜题 1 按钮,因为无效按钮在 HTML 中出现得更早。

堆叠上下文可能出现意外行为,如果你没有阅读过,那么这是一个很好的例子,说明了其中一种不显而易见的堆叠行为。

生成谜题

我们可以将生成谜题视为求解器的另一个版本,但具有额外的要求。

  1. 当按下谜题生成按钮时,一些随机的方格需要保持未解开状态。
  2. 每次按下生成按钮时,随机未解开的方格和正确解决方案的组合都应该不同。
  3. 按下求解按钮应该显示完整的解决方案。
  4. 如果用户手动解决了生成的谜题,我们希望用一个胜利画面来奖励他们,并提供关于他们解决速度的反馈。

CSS 没有 `random()` 函数(虽然 Sass 有),所以你可能不太清楚我们如何才能每次按下同一个按钮时都得到不同的行为。但是上面的求解器解释已经剧透了一部分,它已经对看起来像单个元素的按钮做了一些类似的事情,但实际上它根据当前的有效解决方案而有所不同。

关于“生成”按钮的问题是我们如何才能每次点击时都获得不可预测的结果。Alvaro Montoro 在 CSS-Tricks 上发表的文章关于 如何使用纯 CSS 生成看似随机的值,功不可没。单选按钮黑客和动画堆叠顺序的组合似乎很有效。我尽力尝试是否可以在没有额外标记的情况下做到这一点,但我得出结论,这种方法是最好的,也是最简单的。为了重用求解器解释中的纸牌牌组比喻,这就像纸牌牌组一直不可见地洗牌,所以每当你拿一张牌时,你都会发现它有不同的面。

我们可以将这种伪随机性与 Sass `random()` 函数提供的实际随机性结合起来,为我们的数独游戏提供重玩价值。

@for $j from 0 through 287 {
  label[for=generate#{$j}] { 
    animation-delay: #{$j * .35s}; 
  }
  label[for=generate#{$j}]:active:after {
    z-index: 300;
    width: 100%;
  }
  #generate#{$j}:checked ~ .sudoku { 
    $blockCounts: (1: 2, 2: 2, 3: 3, 4: 2);
    $shuffleSquares: (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16);

     @for $square from 1 through 16 {
       $index1: random(16);
       $index2: random(16);

       $temp: nth($shuffleSquares, $index1);
       $shuffleSquares: set-nth($shuffleSquares, $index1, nth($shuffleSquares, $index2));
       $shuffleSquares: set-nth($shuffleSquares, $index2, $temp);
     }

     @each $square in $shuffleSquares {
       $row: ceil($square/4);
      $column: 1 + ($square - 1) % 4;

       $block: if($row &lt; 3, 1, 3) + if($column < 3, 0, 1);
       $count: map-get($blockCounts, $block);
       $val: nth(nth($solutions, $j + 1), $square);

       --square-#{$square}-solution-#{$val}: 1;

       @if ($count > 0) {
         $blockCounts: map-merge($blockCounts, ($block: $count - 1));
        --square-#{$square}-unsolved: 0; 
        --square-#{$square}-equals-#{$val}: 1;

        @for $other-value from 1 through 4 {
          @if ($other-value != $val) {
            --square-#{$square}-equals-#{$other-value}: 0;   
          }
        }

        --square-#{$square}-color: grey;
        --auto-#{$square}: 1;
      }
    } 
  }
}

对于每个“区块”(这是数独的术语,指的是数独网格中带有粗边框的 4×4 部分),我们使用 Sass 随机选择四个方格中的两个来解决,除了一个“赠送”方格只有一个未解开的方格。由于验证逻辑和求解器逻辑使用变量而不是直接基于使用值选择器检查的值,因此验证和求解逻辑的行为都相同。这意味着生成的数值被视为用户单独选择每个数值的情况。

求解计时器

这是计时器在最初的十一次秒内的滴答声。

我们稍后会深入研究求解计时器的 CSS 代码,但首先让我们展示其中一位数字在没有 CSS `overflow` 设置为 `hidden` 的情况下,以及元素周围带有绿色边框时是什么样子,以便显示用户在动画的每个步骤中可见的部分。

我们使用一个无限重复的关键帧动画,以期望的间隔将可能的数字列表向左移动一位(我们使用等宽字体,以确保每个字符都占据完全相同的宽度)。秒位将从 0 到 9,下一位只应该到 5,每十秒增加一次,然后秒位的两位都需要重置为零。

每一位都在使用相同的技术进行动画,你可以用它在 CSS 中为 精灵图进行动画,不同的是,我们不是通过动画方式移动图像背景来实现动画效果,而是移动一个包含可能数字的伪元素。

与 CSS 中的许多任务一样,在 CSS 中制作动画计数器的方法不止一种。但有些方法不能跨浏览器工作,实际上需要一个预处理器来保持代码简洁。我喜欢我的方法,因为它相当简短。CSS 承担了大部分工作,来确定何时以及如何移动到下一位。所有标记需要做的只是创建一个占位符,让每一位都出现在其中,为我们提供了一些如何呈现计时器的自由度。

这是标记

<div class="stopwatch">  
  <div class="symbol"></div>
  <div class="symbol">:</div>
  <div class="symbol"></div>
  <div class="symbol"></div>
</div>

…以及 CSS

.stopwatch {
  text-align: center;
  font-family: monospace;
  margin-bottom: 10px;
}

.symbol {
  width: 1ch;
  overflow: hidden;
  display: inline-flex;
  font-size: 5ch;
}

.symbol:nth-child(1)::after {
  animation: tens 3600s steps(6, end) infinite;
  content: '012345';
}

.symbol:nth-child(2)::after {
  animation: units 600s steps(10, end) infinite;
  content: '0123456789';
}

.symbol:nth-child(4)::after {
  animation: tens 60s steps(6, end) infinite;
  content: '012345';
}

.symbol:nth-child(5)::after {
  animation: units 10s steps(10, end) infinite;
  content: '0123456789';
}

@keyframes units {
  to {
    transform: translateX(-10ch);
  }
}

@keyframes tens {
  to {
    transform: translateX(-6ch);
  }
}

你可能会注意到计数器在一小时后从头开始。这是因为所有迭代次数都设置为 `infinite`。我们可以解决它,但我认为,如果有人花了一个小时来解决其中一个谜题,那么他们比儿童数独难题有更大的问题。😛

然而,不公平的是,如果我们允许相同的计时器在用户生成新的谜题时继续滴答作响。我们可以让它重置吗?事实证明,我们在本文的第一步中已经解决了这个问题,我们使用 `:active` 伪类删除了数字选择器的动画,并有条件地重新添加了它。这一次,实际上更简单,因为每次我们点击“生成”按钮时,我们都想删除所有数字上的动画,将它们带回到零。然后,当单选按钮不再处于活动状态时,动画将重新开始。因此,我们只需要一行 CSS 来让计时器在每次生成时重置!

input[name=generate]:active ~ .stopwatch .symbol::after {
  animation: none;
}

数独计™️

即使谜题已解决,我也想通过向玩家提供关于他们时间表现的视觉反馈,并挑战他们更快地解决谜题,来提供重玩价值。我也想奖励你,因为你已经读到了这篇文章的这一部分,所以我会给你一个极简的圆形仪表盘,你可以在自己的项目中重复使用。这是一个包含圆形仪表盘的独立 Pen,供你进行实验。

我们应用了与游戏中的胜利画面中使用的相同原理,但在这个 Pen 中,显示的评级是使用单选按钮黑客控制的,而在游戏中,它是通过动画控制的,动画随着时间的推移缓慢地移动到较低的评级。游戏中的仪表盘使用零不透明度隐藏,只有在我们检测到谜题已被手动解决时才会显示(并暂停)。

让我们解释一下我们如何创造一个半圆的错觉,它被颜色分成两部分。它实际上是一个完整的 CSS 圆圈,其下半部分使用 `overflow: hidden` 隐藏。

我们使用一个伪元素来应用两种颜色,该伪元素填充了 `<div>` 的一半。

然后我们用另一个用游戏背景色填充的圆圈在中间开一个洞,做一个甜甜圈,并将它用 flexbox 放在更大的圆圈的中心。

接下来,通过将容器的高度设置为完整圆形高度的一半,并将 `overflow: hidden` 应用于容器,隐藏圆形的一半。

现在,如果我们旋转甜甜圈,看起来就像仪表盘正在用绿色填充或失去绿色,具体取决于我们是否以负度数或正度数旋转甜甜圈!

我们希望在仪表盘的两端添加标签,并在它们之间添加描述,事实证明 flexbox 是一个优雅的解决方案。

#rating {
  font-size: 30px;
  display: flex;
  width: 300px;
  justify-content: space-between;
}

这是标记

<div id="rating">
  <div id="turtle">🐢</div>
  <div id="feedback"></div>
  <div id="rabbit">🐇</div>
</div>

这就是我们定位标签所需的一切。如果评分 `<div>` 的宽度与我们圆形的直径相同,flexbox 将把表情符号标签定位在圆形的两端,并将描述定位在圆形的中心!

至于控制描述内容,这类似于我们用于计时器的技巧,只不过这次我们垂直执行而不是水平执行,因为反馈描述的长度可变。但它们始终保持相同的高度。

结论

我在文章开头提出了一个问题,即 CSS 是否是一种编程语言。很难反驳我们只使用 CSS 实现的逻辑不是编程,但其中一些至少说得上是不寻常的 CSS 使用方式。就像科技界中的许多事情一样,答案似乎是“视情况而定”,并且正如我们在这次实验中了解到有关 CSS 的内容一样,我们也证明了编程与技术一样重要,更重要的是思维方式。

没有 CSS 黑客文章是不带免责声明的,尽管我们已经证明可以在 CSS 中实现复杂的逻辑,并在过程中学到很多东西,但大多数情况下,我们可能不应该在生产环境中这样做,因为可维护性、可访问性和一些以“ility”结尾的词。

但我们也看到,有些事情(比如我认为的 CSS 变量带来的内置反应性)在 CSS 中非常方便,但可能需要我们在 JavaScript 中进行很多操作,甚至可能需要使用框架。通过挑战 CSS 的极限,我们最终创建了一个圆形仪表盘,我认为它可以合理地用于生产应用,甚至可能比使用一些可能很重并且执行我们真正需要的功能以外的其他功能的 JavaScript 小部件更好。

我希望 CSS 数独应用程序能够添加一个重置按钮。目前,如果您想开始一个新的数独游戏,您必须刷新页面。这是单选按钮黑客的固有局限性,这使得 CSS 黑客与传统编程不同。在某个阶段,我认为我找到了一个解决方案,当时我认为可以使用动画来设置 CSS 变量,但事实证明这是 CSS Houdini 的一部分,并且只在基于 Chromium 的浏览器中受支持。如果以及何时在所有浏览器中都支持它,它将打开一个潘多拉魔盒,并带来很多乐趣。在未来的文章中,我甚至可能会探讨为什么我们 Chrome 中拥有的这个看似无害的功能对于 CSS 黑客来说是一个游戏规则改变者。

对于是否可以在 CSS 中实现完整的 81 个方格数独求解器,目前还没有定论,但如果您好奇,请在评论中留下您的反馈。如果足够多的人想要它,我们可能会一起进入那个兔子洞,看看我们可以在这个过程中照亮 CSS 的哪些黑暗角落。