Skip to content

Commit 17824ae

Browse files
Merge pull request #121 from Hrishikesh-Dalal/rsp2
RSP 2.0
2 parents 95d588b + 3d0e9ba commit 17824ae

3 files changed

Lines changed: 368 additions & 45 deletions

File tree

projects/rock-paper-scissors/index.html

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,50 @@
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6-
<title>Rock-Paper-Scissors Game</title>
6+
<title>Rock-Paper-Scissors Minus One</title>
77
<link rel="stylesheet" href="styles.css">
88
</head>
99
<body>
10-
<h1>Rock-Paper-Scissors Game</h1>
10+
<h1>Rock-Paper-Scissors — Minus One</h1>
1111
<div id="rps-container">
12-
<div class="btn-group">
12+
<div id="phase" class="phase">Step 1 — Pick any two symbols</div>
13+
14+
<div class="btn-group" aria-label="Select two symbols">
1315
<button class="rps-btn" data-choice="rock">Rock</button>
1416
<button class="rps-btn" data-choice="paper">Paper</button>
1517
<button class="rps-btn" data-choice="scissors">Scissors</button>
1618
</div>
17-
<div id="display-choices"></div>
19+
20+
<div class="stack small-gap">
21+
<div class="label">Your two:</div>
22+
<div id="selected-two" class="chip-row" aria-live="polite"></div>
23+
</div>
24+
25+
<button id="lock-btn" class="primary" disabled>Lock In</button>
26+
27+
<div id="computer-reveal" class="hidden stack small-gap" aria-live="polite">
28+
<div class="label">Computer's two:</div>
29+
<div id="computer-two" class="chip-row"></div>
30+
</div>
31+
32+
<div id="minus-one" class="hidden stack small-gap">
33+
<div class="label">Step 2 — Minus One: choose one to keep</div>
34+
<div id="final-choice-options" class="chip-row"></div>
35+
<button id="finalize-btn" class="primary" disabled>Finalize</button>
36+
</div>
37+
38+
<div id="display-choices" class="muted"></div>
39+
<div id="final-choices" class="final-choices"></div>
1840
<div id="result"></div>
41+
1942
<div id="scoreboard">
2043
<span id="user-score">0</span> : <span id="computer-score">0</span>
2144
</div>
22-
<button id="restart-btn">Restart</button>
45+
46+
<div class="controls">
47+
<button id="next-round-btn" class="secondary hidden">Next Round</button>
48+
<button id="restart-btn">Reset Score</button>
49+
</div>
2350
</div>
2451
<script src="main.js"></script>
2552
</body>
Lines changed: 260 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,295 @@
1-
function initRockPaperScissors() {
1+
function initRockPaperScissorsMinusOne() {
22
const choices = ["rock", "paper", "scissors"];
33
let userScore = 0;
44
let computerScore = 0;
55

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
614
const userScoreSpan = document.getElementById("user-score");
715
const computerScoreSpan = document.getElementById("computer-score");
816
const resultDiv = document.getElementById("result");
917
const displayChoicesDiv = document.getElementById("display-choices");
18+
const phaseDiv = document.getElementById("phase");
1019
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");
1129
const restartBtn = document.getElementById("restart-btn");
1230

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);
1538
}
1639

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
1943
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")
2347
)
24-
return "win";
25-
return "lose";
48+
return 1; // a wins
49+
return -1; // a loses
2650
}
2751

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;
31138
}
32139

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!";
41245
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!";
45249
computerScore++;
46-
resultDiv.textContent = "You lose!";
47-
resultDiv.style.color = "#d43d29";
48250
} else {
49-
resultDiv.textContent = "It's a draw!";
50-
resultDiv.style.color = "#4078b3";
251+
outcome = "draw";
252+
outcomeText = "It's a draw!";
51253
}
254+
52255
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);
54261

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"));
57268
}
58269

59-
btns.forEach((btn) => {
60-
btn.addEventListener("click", handleClick);
61-
});
270+
// Next round: keep scores
271+
function onNextRound() {
272+
resetRoundUI();
273+
}
62274

63-
restartBtn.addEventListener("click", function () {
275+
// Restart: reset scores and round
276+
function onRestart() {
64277
userScore = 0;
65278
computerScore = 0;
66279
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);
70289

71-
// Initial score display
290+
// Initial state
72291
displayScores();
292+
resetRoundUI();
73293
}
74294

75-
window.addEventListener("DOMContentLoaded", initRockPaperScissors);
295+
window.addEventListener("DOMContentLoaded", initRockPaperScissorsMinusOne);

0 commit comments

Comments
 (0)