全部文章
项目 约 3 分钟

用 React 复刻小丑牌:状态机、算分和一些数学

在浏览器里做了一个 Balatro 风格的小游戏:useReducer 管状态,chips × mult × xMult 算分,五十多张小丑牌负责让数字失控。

用 React 复刻小丑牌:状态机、算分和一些数学

玩《小丑牌》(Balatro)上头之后,我特别想弄明白一件事:为什么几张扑克牌加几个小丑,就能让人一边算分一边傻笑。于是就有了这个浏览器版的小项目——小丑牌。🃏

技术栈很简单:React 18 + Vite。为了能直接挂在博客的静态页里,构建时用 vite-plugin-singlefile 把 JS 和 CSS 全部内联成一个 balatro.html,丢到哪儿都能跑。

状态交给 useReducer

游戏状态整个放在一个 reducer 里,UI 只负责 dispatch。核心阶段有:

  • blind_select:选择小盲注、大盲注或 Boss 盲注;
  • playing:选牌、出牌、弃牌;
  • round_win / round_lose:盲注达成或手牌用尽;
  • shop:买小丑牌、买强化牌、刷新商店;
  • win:设计上打完第 8 轮 Boss 后通关(目前还有 bug,文末会说)。

初始状态会创建一副标准 52 张牌,用 Fisher–Yates 洗牌,再抽 8 张作为手牌。reducer 基本都返回新对象、不改旧 state,配合 React 的不可变更新,调试时能很清楚地知道是哪一步把分数推上去的。

算分:chips × mult × xMult

小丑牌的爽感来自乘算。核心公式可以简化成:

let chips = handDef.baseChips;
let mult = handDef.baseMult;
let xMult = 1;

for (const card of playedCards) {
  chips += getCardChips(card);
  if (card.enhancement === 'MULT') mult += 4;
  if (card.enhancement === 'GLASS') xMult *= 2;
  if (card.edition === 'FOIL') chips += 50;
  if (card.edition === 'POLYCHROME') xMult *= 1.5;
}

const totalScore = Math.floor(chips * mult * xMult);

基础分与倍率

牌型决定基础筹码与基础倍率,比如高牌是 5×1、一对是 10×2、葫芦是 40×4,同花顺能到 100×8。每张打出的牌还会贡献自己的筹码,A 是 11,人头牌是 10。小丑牌则在计分时修改同一个上下文:加筹码、加倍率,或者直接乘 xMult。

当你以为 100×8 已经很大时,一张「纸牌高手」会把每张黑色牌变成 ×3——数字就是这么失控的。

五十多张小丑牌

jokers.js 里目前定义了五十多张,按稀有度分 common / uncommon 两档。效果大致几类:

类型例子作用
固定加成小丑+4 倍率
花色计数贪婪小丑每张♦ +3 倍率
牌型条件欢乐小丑含一对时 +8 倍率
乘算三人组含三条时 ×3 倍率
经济黄金小丑回合结束 +4 元

小丑牌怎么接进来

每张牌用一个 effect(context) 函数描述效果,计分时依次执行。这样加新小丑只要往数组里塞一个对象,不用改计分主流程。

难度与商店

游戏共 8 轮(ante),盲注目标从 300 一路涨到 150000;Boss 盲注会带来额外限制,比如弃牌次数 -1、手牌上限 -1。过关奖励由底注奖励、剩余出牌次数和利息组成,利息最多 5 元。

商店每次刷 2 张小丑牌和 2 张强化牌,刷新费用 5 元、每次 +2。买不起的时候只能看着「多彩」流口水,这种穷感倒是很还原原作。💸

还差什么

目前商店的 voucher 还是空实现,个别小丑(比如「错视」「模糊小丑」)只有描述、没有真正生效;移动端布局也只做了基础适配。还有一个更尴尬的 bug:设计上打完第 8 轮 Boss 就通关,但 reducer 里的胜利判定写错了条件,win 阶段其实触发不了,只能一直往后打。这些都得慢慢补。

不过作为一个周末项目,它已经能连着玩很多轮了。如果你也玩过原作,欢迎来试玩之后告诉我哪里最不像。😉

返回文章列表 RSS 订阅