Description: I'm building a simple Rock-Paper-Scissors game using HTML, CSS, and JavaScript. The game works fine initially, where users can make their choice and play against the computer. However, after a game ends and the "Play Again" button is clicked, the event listeners on the game choices (rock, paper, scissor) don't seem to reattach properly. As a result, users cannot play another round after clicking "Play Again."
function getComputerChoice(){
const choice = ['rock','paper','scissor']
const length = choice.length
return choice[Math.floor(Math.random() * length)]
}
const choicesImage = document.querySelectorAll('.choice img')
const choices = document.querySelectorAll('.choice')
const container = document.querySelector('.container')
const playerResult = document.querySelector('.playerChoice')
const computerResult = document.querySelector('.computerChoice')
const innerContent = container.innerHTML
const status = document.querySelector('.status')
let playerCurrentScore = 0 , computerCurrentScore = 0
function declareResult(){
if(playerCurrentScore === 5 || computerCurrentScore === 5 ){
container.classList.add('new')
const newContent = document.querySelector('.new')
newContent.textContent = playerCurrentScore === 5 ? 'Player Wins!':'Computer Wins!'
newContent.style.fontSize = "35px"
newContent.style.textAlign = "center"
const btn = document.createElement('button')
btn.textContent = "Play Again"
newContent.append(btn)
btn.addEventListener('click',changeContent)
}
}
function changeContent(){
container.classList.remove('new')
container.innerHTML = innerContent
attachEvents();
}
function attachEvents(){
choicesImage.forEach((choice) => {
choice.addEventListener('click',game,true);
});
}
attachEvents();
function game(e){
let index = -Infinity
if(e.target.alt === 'rock') index = 0
else if(e.target.alt === 'paper') index = 1
else if(e.target.alt === 'scissor') index = 2
choices[index].classList.add('active')
choicesImage.forEach((others,indices) => {
if(index !== indices) choices[indices].classList.remove('active')
});
playerResult.src = 'images/rock.svg'
computerResult.src = 'images/rock.svg'
container.classList.add('start')
status.textContent = "Loading.."
setTimeout(() => {
container.classList.remove('start')
let user = e.target.alt
let computer= getComputerChoice()
if(user === 'scissor'){
playerResult.src = `images/${user}.png`
}else{
playerResult.src = `images/${user}.svg`
}
if(computer === 'scissor'){
computerResult.src = `images/${computer}.png`
}else{
computerResult.src = `images/${computer}.svg`
}
let playerScore = document.querySelector('.playerScore')
let computerScore = document.querySelector('.computerScore')
if(user === 'rock' && computer === 'scissor' || user === 'paper' && computer === 'rock' ||
user === 'scissor' && computer === 'paper'){
status.textContent = `You win! ${user} beats ${computer}`
playerCurrentScore++
playerScore.textContent = playerCurrentScore
computerScore.textContent = computerCurrentScore
}else if(user === computer){
status.textContent = `Draw Match...`
playerScore.textContent = playerCurrentScore
computerScore.textContent = computerCurrentScore
}else{
status.textContent = `You Lose! ${computer} beats ${user}`
computerCurrentScore++
playerScore.textContent = playerCurrentScore
computerScore.textContent = computerCurrentScore
}
declareResult()
},500)
}
@import url('https://fonts.googleapis.com/css2?family=Poppins&display=swap');
:root{
--gradient: linear-gradient(
to right,
#8B008Ba1,
#800080b2,
slateblue,
darkslateblue
);
--bodyFont:'Poppins', sans-serif;
}
*{
box-sizing: border-box;
margin: 0;
padding: 0;
}
body{
background-image: var(--gradient);
background-repeat: no-repeat;
background-attachment: fixed;
animation: bg-animation 20s ease infinite 2s alternate;
background-size: 300%;
font-size: 62.5%;
font-family: var(--bodyFont);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
overflow-y: hidden;
}
@keyframes bg-animation{
0%{
background-position: left;
}
50%{
background-position: right;
}
100%{
background-position: left;
}
}
.container{
background-color: white;
padding: 2.5rem;
width: 90%;
max-width: 500px;
border-radius: 1.5rem;
box-shadow: 2px 2px 30px 4px rgba(60, 103, 108, 0.593);
}
.container.start .choice{
pointer-events: none;
}
.game-container{
display: flex;
justify-content: space-around;
}
.playerChoice,.computerChoice{
width: 6rem;
height: 6rem;
transform: rotate(90deg);
}
.container.start .playerChoice{
animation: userShake .5s ease infinite;
}
@keyframes userShake{
0%{
transform: rotate(85deg);
}
50%{
transform: rotate(99deg);
}
}
.container.start .computerChoice{
animation: computerShake .5s ease infinite;
}
@keyframes computerShake{
0%{
transform: rotate(265deg) rotateY(175deg);
}
50%{
transform: rotate(279deg) rotateY(190deg);
}
}
.computerChoice{
transform: rotate(270deg) rotateY(180deg);
}
.status{
text-align: center;
margin: 2.5rem auto;
font-size: 1.5rem;
}
.choice-container{
display: flex;
justify-content: space-around;
align-items: center;
}
.choice{
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
opacity: 0.5;
transition: opacity 0.1s;
}
.choice:hover{
cursor:pointer;
opacity: 1;
}
.active{
opacity: 1;
}
.choice img{
width: 4rem;
height: 4rem;
}
.desc{
font-size: 1.2rem;
font-weight: bold;
}
.score{
display:flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.player,.computer{
font-size: 1.2rem;
margin-top: 2.0rem;
}
.playerScore,.computerScore{
font-size: 1.5rem;
vertical-align: middle;
font-weight: bold;
margin-left: 1.0rem;
}
.container.new p{
font-size: 35px;
text-align: center;
}
.container.new button{
display: block;
width: 250px;
height: 50px;
border-radius: 10px;
font-size: 15px;
margin: auto;
}
<div class="container">
<div class="game-container">
<img src="images/rock.svg" alt="player" class="playerChoice">
<img src="images/rock.svg" alt="computer" class="computerChoice">
</div>
<p class="status"></p>
<div class="choice-container">
<span class="choice">
<img src="images/rock.svg" alt="rock">
<p class="desc">Rock</p>
</span>
<span class="choice">
<img src="images/paper.svg" alt="paper">
<p class="desc">Paper</p>
</span>
<span class="choice">
<img src="images/scissor.png" alt="scissor">
<p class="desc">Scissor</p>
</span>
</div>
<div class="results">
<div class="score">
<p class="player">Player Score: <span class="playerScore">0</span></p>
<p class="computer">Computer Score: <span class="computerScore">0</span></p>
</div>
<p class="announce"></p>
</div>
</div>
Link: https://ms-softwareengineer.github.io/odin-RPS-Game/
What could be causing the event listeners not to reattach properly after clicking "Play Again"? Are there any potential pitfalls or common mistakes I might be overlooking? Any insights or suggestions on how to debug and resolve this issue would be greatly appreciated.
Testing this actually got me rate-limited by Stack Overflow, so this is making a lot of requests to the server.
There are probably a wide variety of improvements to be made here. But specifically with regards to the click event handlers...
The code is replacing the entire HTML here:
container.innerHTML = innerContent
But it's not updating any of these variables:
const choicesImage = document.querySelectorAll('.choice img')
So even when it tries to re-attach click handlers, it's attaching them to stale element references. You can test this by updating this one reference. First use let
so it can be re-assigned:
let choicesImage = document.querySelectorAll('.choice img')
Then re-assign it here:
function attachEvents(){
choicesImage = document.querySelectorAll('.choice img');
choicesImage.forEach((choice) => {
choice.addEventListener('click',game,true);
});
}
Those particular click events will then start working again.
Taking a step back to the bigger picture though, I'd highly recommend not replacing the HTML at all. It may seem like a simple shortcut to "reset" the game in that way, but it's causing more problems than it solves. You'd likely be better off in the long run to go back and re-work a bit of this (or even start again from scratch) to keep the same HTML on the page and just show/hide/modify that same overall markup as the state of the game changes.