|
1 | | -function initRockPaperScissors() { |
| 1 | +function initRockPaperScissorsMinusOne() { |
2 | 2 | const choices = ["rock", "paper", "scissors"]; |
3 | 3 | let userScore = 0; |
4 | 4 | let computerScore = 0; |
5 | 5 |
|
| 6 | + // State for a round |
| 7 | + let userSelections = []; |
| 8 | + let computerSelections = []; |
| 9 | + let userFinalChoice = null; |
| 10 | + let userFinalIndex = null; |
| 11 | + let computerFinalChoice = null; |
| 12 | + |
| 13 | + // Elements |
6 | 14 | const userScoreSpan = document.getElementById("user-score"); |
7 | 15 | const computerScoreSpan = document.getElementById("computer-score"); |
8 | 16 | const resultDiv = document.getElementById("result"); |
9 | 17 | const displayChoicesDiv = document.getElementById("display-choices"); |
| 18 | + const phaseDiv = document.getElementById("phase"); |
10 | 19 | const btns = document.querySelectorAll(".rps-btn"); |
| 20 | + const selectedTwoDiv = document.getElementById("selected-two"); |
| 21 | + const lockBtn = document.getElementById("lock-btn"); |
| 22 | + const computerReveal = document.getElementById("computer-reveal"); |
| 23 | + const computerTwoDiv = document.getElementById("computer-two"); |
| 24 | + const minusOneDiv = document.getElementById("minus-one"); |
| 25 | + const finalChoiceOptionsDiv = document.getElementById("final-choice-options"); |
| 26 | + const finalizeBtn = document.getElementById("finalize-btn"); |
| 27 | + const finalChoicesDiv = document.getElementById("final-choices"); |
| 28 | + const nextRoundBtn = document.getElementById("next-round-btn"); |
11 | 29 | const restartBtn = document.getElementById("restart-btn"); |
12 | 30 |
|
13 | | - function getComputerChoice() { |
14 | | - return choices[Math.floor(Math.random() * 3)]; |
| 31 | + function displayScores() { |
| 32 | + userScoreSpan.textContent = userScore; |
| 33 | + computerScoreSpan.textContent = computerScore; |
| 34 | + } |
| 35 | + |
| 36 | + function cap(s) { |
| 37 | + return s.charAt(0).toUpperCase() + s.slice(1); |
15 | 38 | } |
16 | 39 |
|
17 | | - function getResult(user, computer) { |
18 | | - if (user === computer) return "draw"; |
| 40 | + // RPS comparison from perspective of first argument |
| 41 | + function compare(a, b) { |
| 42 | + if (a === b) return 0; // draw |
19 | 43 | if ( |
20 | | - (user === "rock" && computer === "scissors") || |
21 | | - (user === "paper" && computer === "rock") || |
22 | | - (user === "scissors" && computer === "paper") |
| 44 | + (a === "rock" && b === "scissors") || |
| 45 | + (a === "paper" && b === "rock") || |
| 46 | + (a === "scissors" && b === "paper") |
23 | 47 | ) |
24 | | - return "win"; |
25 | | - return "lose"; |
| 48 | + return 1; // a wins |
| 49 | + return -1; // a loses |
26 | 50 | } |
27 | 51 |
|
28 | | - function displayScores() { |
29 | | - userScoreSpan.textContent = userScore; |
30 | | - computerScoreSpan.textContent = computerScore; |
| 52 | + function getRandomChoice() { |
| 53 | + return choices[Math.floor(Math.random() * choices.length)]; |
| 54 | + } |
| 55 | + |
| 56 | + // Computer picks two with replacement |
| 57 | + function getComputerTwo() { |
| 58 | + return [getRandomChoice(), getRandomChoice()]; |
| 59 | + } |
| 60 | + |
| 61 | + // Maximin strategy: pick comp keep that maximizes the worst-case vs user's two options |
| 62 | + function getComputerFinalChoice(userPair, compPair) { |
| 63 | + const candidates = compPair; |
| 64 | + // For each candidate, compute the worst (min) outcome against user's options |
| 65 | + const scored = candidates.map((c, idx) => { |
| 66 | + const vsUser = userPair.map((u) => compare(c, u)); |
| 67 | + const minOutcome = Math.min(...vsUser); // -1 < 0 < 1 |
| 68 | + const maxOutcome = Math.max(...vsUser); |
| 69 | + return { idx, choice: c, minOutcome, maxOutcome }; |
| 70 | + }); |
| 71 | + // Choose the one with best minOutcome; tie-break by best maxOutcome; then random |
| 72 | + let best = []; |
| 73 | + let bestMin = -2; |
| 74 | + scored.forEach((s) => { |
| 75 | + if (s.minOutcome > bestMin) { |
| 76 | + bestMin = s.minOutcome; |
| 77 | + best = [s]; |
| 78 | + } else if (s.minOutcome === bestMin) { |
| 79 | + best.push(s); |
| 80 | + } |
| 81 | + }); |
| 82 | + if (best.length > 1) { |
| 83 | + // tie-break on maxOutcome |
| 84 | + let bestMax = -2; |
| 85 | + let next = []; |
| 86 | + best.forEach((s) => { |
| 87 | + if (s.maxOutcome > bestMax) { |
| 88 | + bestMax = s.maxOutcome; |
| 89 | + next = [s]; |
| 90 | + } else if (s.maxOutcome === bestMax) { |
| 91 | + next.push(s); |
| 92 | + } |
| 93 | + }); |
| 94 | + best = next; |
| 95 | + } |
| 96 | + const pick = best[Math.floor(Math.random() * best.length)]; |
| 97 | + return pick.choice; |
| 98 | + } |
| 99 | + |
| 100 | + // UI helpers |
| 101 | + function clearChildren(el) { |
| 102 | + while (el.firstChild) el.removeChild(el.firstChild); |
| 103 | + } |
| 104 | + |
| 105 | + function renderChips(el, arr, { removable = false, selectable = false, selected = null, onRemove = null, onSelect = null } = {}) { |
| 106 | + clearChildren(el); |
| 107 | + arr.forEach((val, index) => { |
| 108 | + const chip = document.createElement("button"); |
| 109 | + chip.className = "chip" + (selectable ? " selectable" : "") + (selected === index ? " selected" : ""); |
| 110 | + chip.setAttribute("type", "button"); |
| 111 | + chip.dataset.index = String(index); |
| 112 | + chip.dataset.choice = val; |
| 113 | + chip.textContent = cap(val); |
| 114 | + if (removable) { |
| 115 | + chip.classList.add("removable"); |
| 116 | + chip.setAttribute("title", "Remove"); |
| 117 | + chip.addEventListener("click", () => { |
| 118 | + onRemove && onRemove(index); |
| 119 | + }); |
| 120 | + } else if (selectable) { |
| 121 | + chip.addEventListener("click", () => { |
| 122 | + onSelect && onSelect(index, val); |
| 123 | + }); |
| 124 | + } |
| 125 | + el.appendChild(chip); |
| 126 | + }); |
| 127 | + } |
| 128 | + |
| 129 | + function show(element) { |
| 130 | + element.classList.remove("hidden"); |
| 131 | + } |
| 132 | + function hide(element) { |
| 133 | + element.classList.add("hidden"); |
| 134 | + } |
| 135 | + |
| 136 | + function setPhase(text) { |
| 137 | + if (phaseDiv) phaseDiv.textContent = text; |
31 | 138 | } |
32 | 139 |
|
33 | | - function handleClick(e) { |
34 | | - const userChoice = e.target.dataset.choice; |
35 | | - const computerChoice = getComputerChoice(); |
36 | | - const outcome = getResult(userChoice, computerChoice); |
37 | | - displayChoicesDiv.textContent = `You: ${capitalize( |
38 | | - userChoice |
39 | | - )} | Computer: ${capitalize(computerChoice)}`; |
40 | | - if (outcome === "win") { |
| 140 | + function flashOutcome(outcome) { |
| 141 | + const body = document.body; |
| 142 | + body.classList.remove("win-bg", "lose-bg", "draw-bg"); |
| 143 | + if (outcome === "win") body.classList.add("win-bg"); |
| 144 | + else if (outcome === "lose") body.classList.add("lose-bg"); |
| 145 | + else body.classList.add("draw-bg"); |
| 146 | + // remove after a short time |
| 147 | + setTimeout(() => { |
| 148 | + body.classList.remove("win-bg", "lose-bg", "draw-bg"); |
| 149 | + }, 900); |
| 150 | + } |
| 151 | + |
| 152 | + function resetRoundUI() { |
| 153 | + userSelections = []; |
| 154 | + computerSelections = []; |
| 155 | + userFinalChoice = null; |
| 156 | + userFinalIndex = null; |
| 157 | + computerFinalChoice = null; |
| 158 | + renderChips(selectedTwoDiv, userSelections); |
| 159 | + hide(computerReveal); |
| 160 | + clearChildren(computerTwoDiv); |
| 161 | + hide(minusOneDiv); |
| 162 | + clearChildren(finalChoiceOptionsDiv); |
| 163 | + finalizeBtn.disabled = true; |
| 164 | + lockBtn.disabled = true; |
| 165 | + finalChoicesDiv.textContent = ""; |
| 166 | + displayChoicesDiv.textContent = ""; |
| 167 | + resultDiv.textContent = ""; |
| 168 | + setPhase("Step 1 — Pick any two symbols"); |
| 169 | + // Re-enable choice buttons for step 1 |
| 170 | + btns.forEach((b) => (b.disabled = false)); |
| 171 | + hide(nextRoundBtn); |
| 172 | + } |
| 173 | + |
| 174 | + // Step 1: user selects two (duplicates allowed) |
| 175 | + function onChoiceClick(e) { |
| 176 | + const btn = e.currentTarget; |
| 177 | + const choice = btn.dataset.choice; |
| 178 | + if (!choice) return; |
| 179 | + if (userSelections.length >= 2) { |
| 180 | + return; // already have two; allow removal via chips |
| 181 | + } |
| 182 | + userSelections.push(choice); |
| 183 | + const refreshSelectedChips = () => { |
| 184 | + renderChips(selectedTwoDiv, userSelections, { |
| 185 | + removable: true, |
| 186 | + onRemove: (idx) => { |
| 187 | + userSelections.splice(idx, 1); |
| 188 | + refreshSelectedChips(); |
| 189 | + updateLockState(); |
| 190 | + }, |
| 191 | + }); |
| 192 | + }; |
| 193 | + refreshSelectedChips(); |
| 194 | + updateLockState(); |
| 195 | + } |
| 196 | + |
| 197 | + function updateLockState() { |
| 198 | + lockBtn.disabled = userSelections.length !== 2; |
| 199 | + } |
| 200 | + |
| 201 | + // Lock in: reveal computer and move to minus-one phase |
| 202 | + function onLock() { |
| 203 | + // Disable choice buttons for now |
| 204 | + btns.forEach((b) => (b.disabled = true)); |
| 205 | + computerSelections = getComputerTwo(); |
| 206 | + renderChips(computerTwoDiv, computerSelections); |
| 207 | + show(computerReveal); |
| 208 | + setPhase("Step 2 — Minus One: choose one to keep"); |
| 209 | + // Prepare user's minus-one selection options |
| 210 | + const handleSelectFinal = (idx, val) => { |
| 211 | + userFinalIndex = idx; |
| 212 | + userFinalChoice = val; |
| 213 | + finalizeBtn.disabled = false; |
| 214 | + // re-render to keep current highlight, keep handler for future changes |
| 215 | + renderChips(finalChoiceOptionsDiv, userSelections, { |
| 216 | + selectable: true, |
| 217 | + selected: userFinalIndex, |
| 218 | + onSelect: handleSelectFinal, |
| 219 | + }); |
| 220 | + }; |
| 221 | + |
| 222 | + renderChips(finalChoiceOptionsDiv, userSelections, { |
| 223 | + selectable: true, |
| 224 | + selected: userFinalIndex, |
| 225 | + onSelect: handleSelectFinal, |
| 226 | + }); |
| 227 | + show(minusOneDiv); |
| 228 | + |
| 229 | + // Decide computer's final choice now (simultaneous selection) |
| 230 | + computerFinalChoice = getComputerFinalChoice(userSelections, computerSelections); |
| 231 | + } |
| 232 | + |
| 233 | + // Finalize showdown |
| 234 | + function onFinalize() { |
| 235 | + if (!userFinalChoice) return; |
| 236 | + // compute outcome for user |
| 237 | + const comp = computerFinalChoice; |
| 238 | + const user = userFinalChoice; |
| 239 | + const cmp = compare(user, comp); |
| 240 | + let outcomeText = ""; |
| 241 | + let outcome = "draw"; |
| 242 | + if (cmp > 0) { |
| 243 | + outcome = "win"; |
| 244 | + outcomeText = "You win!"; |
41 | 245 | userScore++; |
42 | | - resultDiv.textContent = "You win!"; |
43 | | - resultDiv.style.color = "#349946"; |
44 | | - } else if (outcome === "lose") { |
| 246 | + } else if (cmp < 0) { |
| 247 | + outcome = "lose"; |
| 248 | + outcomeText = "You lose!"; |
45 | 249 | computerScore++; |
46 | | - resultDiv.textContent = "You lose!"; |
47 | | - resultDiv.style.color = "#d43d29"; |
48 | 250 | } else { |
49 | | - resultDiv.textContent = "It's a draw!"; |
50 | | - resultDiv.style.color = "#4078b3"; |
| 251 | + outcome = "draw"; |
| 252 | + outcomeText = "It's a draw!"; |
51 | 253 | } |
| 254 | + |
52 | 255 | displayScores(); |
53 | | - } |
| 256 | + displayChoicesDiv.textContent = `You kept: ${cap(user)} | Computer kept: ${cap(comp)}`; |
| 257 | + resultDiv.textContent = outcomeText; |
| 258 | + finalChoicesDiv.textContent = `${cap(user)} vs ${cap(comp)}`; |
| 259 | + |
| 260 | + flashOutcome(outcome); |
54 | 261 |
|
55 | | - function capitalize(str) { |
56 | | - return str.charAt(0).toUpperCase() + str.slice(1); |
| 262 | + // Show next round button |
| 263 | + show(nextRoundBtn); |
| 264 | + // Disable finalize button and selection chips |
| 265 | + finalizeBtn.disabled = true; |
| 266 | + // Prevent further selection |
| 267 | + Array.from(finalChoiceOptionsDiv.children).forEach((c) => c.setAttribute("disabled", "true")); |
57 | 268 | } |
58 | 269 |
|
59 | | - btns.forEach((btn) => { |
60 | | - btn.addEventListener("click", handleClick); |
61 | | - }); |
| 270 | + // Next round: keep scores |
| 271 | + function onNextRound() { |
| 272 | + resetRoundUI(); |
| 273 | + } |
62 | 274 |
|
63 | | - restartBtn.addEventListener("click", function () { |
| 275 | + // Restart: reset scores and round |
| 276 | + function onRestart() { |
64 | 277 | userScore = 0; |
65 | 278 | computerScore = 0; |
66 | 279 | displayScores(); |
67 | | - resultDiv.textContent = ""; |
68 | | - displayChoicesDiv.textContent = ""; |
69 | | - }); |
| 280 | + onNextRound(); |
| 281 | + } |
| 282 | + |
| 283 | + // Wire events |
| 284 | + btns.forEach((btn) => btn.addEventListener("click", onChoiceClick)); |
| 285 | + lockBtn.addEventListener("click", onLock); |
| 286 | + finalizeBtn.addEventListener("click", onFinalize); |
| 287 | + nextRoundBtn.addEventListener("click", onNextRound); |
| 288 | + restartBtn.addEventListener("click", onRestart); |
70 | 289 |
|
71 | | - // Initial score display |
| 290 | + // Initial state |
72 | 291 | displayScores(); |
| 292 | + resetRoundUI(); |
73 | 293 | } |
74 | 294 |
|
75 | | -window.addEventListener("DOMContentLoaded", initRockPaperScissors); |
| 295 | +window.addEventListener("DOMContentLoaded", initRockPaperScissorsMinusOne); |
0 commit comments