
本文详解如何重构“石头剪刀布”游戏,解决因重复调用 `playround()` 导致的逻辑错乱与得分丢失问题,通过参数化设计、单次执行原则和局部变量合理作用域,实现清晰、可维护的回合得分统计机制。
在 javaScript 初学者开发回合制游戏(如石头剪刀布)时,一个典型误区是:在同一个逻辑上下文中多次调用同一函数,却未保存其返回值。你原始代码中 game() 函数内三次调用 playRound()(一次 console.log,两次用于条件判断),每次调用都会重新执行 getPlayerChoice() 和 getComputerChoice() —— 这不仅造成用户被反复提示输入、电脑选择被重生成,更导致前后不一致的比对结果,使得分判定完全失效。
根本原因在于:playRound() 原本是“自包含型”函数(内部获取输入并返回结果),但 game() 又自行获取了 playerSelection 和 computerSelection,却未将它们传入 playRound(),反而让 playRound() 重复执行——形成逻辑割裂与资源浪费。
✅ 正确解法是践行 “单一职责 + 显式传参 + 单次执行” 原则:
- playRound() 应专注裁决,不负责输入获取 → 改为接收 playerSelection 和 computerSelection 作为参数;
- game() 负责流程控制 → 每轮只调用一次 getPlayerChoice() 和 getComputerChoice(),并将结果传给 playRound();
- 得分变量保留在 game() 作用域内(或提升为模块级变量),避免全局污染,同时确保跨轮累积有效。
以下是优化后的核心实现(已修复拼写错误 scrissors → scissors,并增强健壮性):
立即学习“Java免费学习笔记(深入)”;
// ✅ 修正拼写:Scrissors → Scissors(全代码统一) function getPlayerChoice() { let playerInput = prompt("Choose rock, paper or scissors."); // ? 安全处理:用户点击「取消」时 prompt 返回 null if (playerInput === null) { alert("Game cancelled. Refresh to restart."); return null; // 中断后续流程 } let playerChoice = playerInput.trim().toLowerCase(); if (playerChoice === "rock") return "Rock"; if (playerChoice === "paper") return "Paper"; if (playerChoice === "scissors") return "Scissors"; alert("Invalid input! Please enter 'rock', 'paper', or 'scissors'."); return getPlayerChoice(); // 递归重试(仅限合法输入) } function getComputerChoice() { const choices = ["Rock", "Paper", "Scissors"]; return choices[Math.floor(Math.random() * choices.length)]; } // ✅ playRound 纯裁决函数:只依赖输入参数,返回结构化结果 function playRound(player, computer) { if (player === computer) return { result: "tie", message: "It's a tie!" }; const winConditions = [ ["Rock", "Scissors"], ["Paper", "Rock"], ["Scissors", "Paper"] ]; if (winConditions.some(([p, c]) => p === player && c === computer)) { return { result: "win", message: `You win! ${player} beats ${computer}.` }; } else { return { result: "lose", message: `You lose. ${computer} beats ${player}.` }; } } // ✅ game:主流程,本地维护 score,每轮只执行一次完整逻辑链 function game() { let playerScore = 0; let computerScore = 0; console.log("? Starting 5-round Rock-Paper-Scissors game...n"); for (let round = 1; round <= 5; round++) { console.log(`--- Round ${round} ---`); const playerSelection = getPlayerChoice(); if (playerSelection === null) return; // 用户取消,提前退出 const computerSelection = getComputerChoice(); const roundResult = playRound(playerSelection, computerSelection); console.log(`You chose: ${playerSelection}`); console.log(`Computer chose: ${computerSelection}`); console.log(roundResult.message); // ✅ 基于结构化返回值更新分数(更可靠,避免字符串硬编码匹配) if (roundResult.result === "win") { playerScore++; console.log(`→ Player score: ${playerScore}`); } else if (roundResult.result === "lose") { computerScore++; console.log(`→ Computer score: ${computerScore}`); } else { console.log(`→ Tie! No points awarded.`); } console.log(""); } // ? 终局判定 console.log("? Final Score:"); console.log(`Player: ${playerScore} | Computer: ${computerScore}`); if (playerScore > computerScore) { console.log("? You won the game!"); } else if (playerScore < computerScore) { console.log("? You lost the game."); } else { console.log("? Game ended in a draw!"); } console.log("? Game Over."); } // 启动游戏 game();
⚠️ 关键注意事项与进阶建议
- 避免字符串硬匹配:原方案用 response === "You win..." 判定胜负极易出错(空格、大小写、标点微小差异即失败)。改用对象返回 { result: "win" } 是更健壮、可扩展的设计。
- 输入容错增强:prompt() 返回 NULL 时必须显式处理,否则 .toLowerCase() 会抛出 TypeError: Cannot read Property 'toLowerCase' of null —— 这正是你遇到的报错根源。
- 不要滥用递归重试:getPlayerChoice() 中的递归虽能实现重试,但深层调用栈可能引发栈溢出。生产环境推荐用 while 循环替代。
- 为 HTML 集成预留接口:若后续迁移到网页界面,可将 prompt/alert 替换为 dom 操作(如 + button),而 playRound() 和得分逻辑完全无需修改 —— 这正是良好函数拆分的价值。
? 总结:函数不是“黑盒”,而是契约。明确每个函数的输入、输出与副作用,是写出可预测、可调试、可复用代码的第一步。得分不是凭空产生,而是由每一次确定的输入 → 确定的裁决 → 确定的状态变更所累积而成。