Search code examples
javascripthtmlcssaddeventlistenerevent-listener

Event Listeners Not Reattaching After "Play Again" in Rock-Paper-Scissors Game


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/

  • I've tried removing and reattaching event listeners, but it doesn't seem to work as expected.
  • The game UI elements do reset correctly, but the click events on the choices are not being reattached.
  • I've inspected the browser console for errors, but no errors are displayed.
  • I've checked the HTML structure, CSS classes, and made sure the event listener code is being executed.

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.


Solution

  • 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.