2
\$\begingroup\$

Outside of the jQuery DSL I've hardly ever written any JavaScript. I never bothered learning JavaScript until a couple of months ago. After a slow start I got stuck in and created this game today.

I usually program in Ruby, and this is proving to be an obstacle because I'm thinking in ruby while writing JS.

The code may come as a shocker to experienced JS programmers. My greatest challenge is to separate presentation from logic. I haven't figured that out yet. This is a case of knowing the syntax without knowing the language well.

I'm interested in comments about structure. I know this is hardly maintainable code. It's obvious. What is not obvious (to me) is how to make it maintainable.

I'm only adding the JS code here, but the entire game is included in the snippet below the code.

(function() {

    /*
     * Screens
     */

    var titleScreen = document.getElementById('title-screen');
    var gameScreen = document.getElementById('game-screen');
    var nameScreen = document.getElementById('name-screen');

    /*
     * Player name input
     */

    var name = document.getElementById('player-name');

    name.addEventListener('keyup', function() {
        if (name.value.length > 0 && name.value.length <= 8) {
            hideNameError();
        }
        else {
            showNameError();
        }
    });

    /*
     * Buttons
     */

    // Sends player to name screen
    var newGameButton = document.getElementById('btn__new-game');

    newGameButton.addEventListener('click', function() {
        toNameScreen();
    });

    // Quits the game at any moment
    var quitToTitleScreenButton = document.getElementById('quit-to-title-screen');

    quitToTitleScreenButton.addEventListener('click', function() {
        var quit = confirm("Are you sure you want to quit?");

        if (quit) {
            toTitleScreen();
        }
    });

    // Cancels game on name screen
    var cancelNewGameButton = document.getElementById('cancel-new-game');

    cancelNewGameButton.addEventListener('click', function(event) {
        event.preventDefault();
        toTitleScreen();
    });

    // Starts game
    var startGameButton = document.getElementById('start-game');

    startGameButton.addEventListener('click', function(event) {
        event.preventDefault();

        if (!name.value || name.value.length > 8) {
            showNameError();
            return false;
        }
        else {
            var game = new Game(name.value);

            // Change name labels to show player name
            var nameLabels = document.getElementsByClassName('name-label');

            for (var i = 0; i < nameLabels.length; i++) {
                nameLabels[i].innerHTML = name.value;
            }

            // Reset name input and transition to game screen
            name.value = '';
            toGameScreen();

            // Prime game play buttons
            var playButtons = document.getElementsByClassName('play-button');

            for (var i = 0; i < playButtons.length; i++) {
                var element = playButtons[i];

                element.addEventListener('click', function() {
                    var cpuChoice = game.computerChoice();
                    var playerChoice = this.id;
                    var result = game.resolve(playerChoice, cpuChoice);
                    refreshScreen(result, game, playerChoice, cpuChoice);
                });
            }
        }
    });

    /*
     * Show hide error if name is invalid
     */

     function showNameError() {
        document.getElementById('error').style.display = 'block';
     }

     function hideNameError() {
        document.getElementById('error').style.display = 'none';
     }

    /*
     * Screen transition functions
     */

     function toTitleScreen() {
        gameScreen.style.display = 'none';
        nameScreen.style.display = 'none';
        titleScreen.style.display = 'block';
     }

     function toGameScreen() {
        titleScreen.style.display = 'none';
        nameScreen.style.display = 'none';
        gameScreen.style.display = 'block';
     }

     function toNameScreen() {
        titleScreen.style.display = 'none';
        gameScreen.style.display = 'none';
        nameScreen.style.display = 'block';
     }

     /*
      * Updates game screen after each game
      */

     function refreshScreen(result, game, player, cpu) {
        // Hide messages
        var messages = document.getElementsByClassName('player-choices__result');

        for (var i = 0; i < messages.length; i++) {
            messages[i].style.display = 'none';
        }

        // Show player and computer choices
        document.getElementById('choice-computer').innerHTML = cpu.capitalize();
        document.getElementById('choice-player').innerHTML = player.capitalize();

        // Update scores and show message
        switch(result) {
            case 'playerWin':
                document.getElementById('player-score').innerHTML = game.playerScore;
                document.getElementById('result-win').style.display = 'block';
                break;
            case 'computerWin':
                document.getElementById('computer-score').innerHTML = game.computerScore;
                document.getElementById('result-loss').style.display = 'block';
                break;
            case 'tie':
                document.getElementById('ties-score').innerHTML = game.ties;
                document.getElementById('result-tie').style.display = 'block';
                break;
        }
     }

    /*
     * Game object
     */

     var possibleChoices = ['rock', 'paper', 'scissors'];

     function Game(player) {
        this.player = player;
        this.computerScore = 0;
        this.playerScore = 0;
        this.ties = 0;
     }

     Game.prototype.resolve = function(playerChoice, cpuChoice) {
        switch(playerChoice) {
            case 'rock':
                if (cpuChoice === 'rock') {
                    this.ties += 1;
                    return 'tie';                   
                }
                else if (cpuChoice == 'paper') {
                    this.computerScore += 1;
                    return 'computerWin';                   
                }
                else if (cpuChoice == 'scissors') {
                    this.playerScore += 1;
                    return 'playerWin';                 
                }
            case 'paper':
                if (cpuChoice === 'rock') {
                    this.playerScore += 1;
                    return 'playerWin';                 
                }
                else if (cpuChoice == 'paper') {
                    this.ties += 1;
                    return 'tie';                   
                }
                else if (cpuChoice == 'scissors') {
                    this.computerScore += 1;
                    return 'computerWin';                   
                }
            case 'scissors':
                if (cpuChoice === 'rock') {
                    this.computerScore += 1;
                    return 'computerWin';                   
                }
                else if (cpuChoice == 'paper') {
                    this.playerScore += 1;
                    return 'playerWin';                 
                }
                else if (cpuChoice == 'scissors') {
                    this.ties += 1;
                    return 'tie';                   
                }
        }
     };

     Game.prototype.computerChoice = function() {
        return possibleChoices[Math.floor(Math.random() * possibleChoices.length)];
     };

    String.prototype.capitalize = function() {
        return this.charAt(0).toUpperCase() + this.slice(1);
    }
})();

(function() {

	/*
	 * Screens
	 */

	var titleScreen = document.getElementById('title-screen');
	var gameScreen = document.getElementById('game-screen');
	var nameScreen = document.getElementById('name-screen');

	/*
	 * Player name input
	 */

	var name = document.getElementById('player-name');

	name.addEventListener('keyup', function() {
		if (name.value.length > 0 && name.value.length <= 8) {
			hideNameError();
		}
		else {
			showNameError();
		}
	});

	/*
	 * Buttons
	 */

	// Sends player to name screen
	var newGameButton = document.getElementById('btn__new-game');

	newGameButton.addEventListener('click', function() {
		toNameScreen();
	});

	// Quits the game at any moment
	var quitToTitleScreenButton = document.getElementById('quit-to-title-screen');

	quitToTitleScreenButton.addEventListener('click', function() {
		var quit = confirm("Are you sure you want to quit?");

		if (quit) {
			toTitleScreen();
		}
	});

	// Cancels game on name screen
	var cancelNewGameButton = document.getElementById('cancel-new-game');

	cancelNewGameButton.addEventListener('click', function(event) {
		event.preventDefault();
		toTitleScreen();
	});

	// Starts game
	var startGameButton = document.getElementById('start-game');

	startGameButton.addEventListener('click', function(event) {
		event.preventDefault();

		if (!name.value || name.value.length > 8) {
			showNameError();
			return false;
		}
		else {
			var game = new Game(name.value);

			// Change name labels to show player name
			var nameLabels = document.getElementsByClassName('name-label');

			for (var i = 0; i < nameLabels.length; i++) {
				nameLabels[i].innerHTML = name.value;
			}

			// Reset name input and transition to game screen
			name.value = '';
			toGameScreen();

			// Prime game play buttons
			var playButtons = document.getElementsByClassName('play-button');

			for (var i = 0; i < playButtons.length; i++) {
				var element = playButtons[i];

				element.addEventListener('click', function() {
	 				var cpuChoice = game.computerChoice();
	 				var playerChoice = this.id;
					var result = game.resolve(playerChoice, cpuChoice);
					refreshScreen(result, game, playerChoice, cpuChoice);
				});
			}
		}
	});

	/*
	 * Show hide error if name is invalid
	 */

	 function showNameError() {
	 	document.getElementById('error').style.display = 'block';
	 }

	 function hideNameError() {
		document.getElementById('error').style.display = 'none';
	 }

	/*
	 * Screen transition functions
	 */

	 function toTitleScreen() {
		gameScreen.style.display = 'none';
	 	nameScreen.style.display = 'none';
		titleScreen.style.display = 'block';
	 }

	 function toGameScreen() {
		titleScreen.style.display = 'none';
		nameScreen.style.display = 'none';
		gameScreen.style.display = 'block';
	 }

	 function toNameScreen() {
	 	titleScreen.style.display = 'none';
	 	gameScreen.style.display = 'none';
	 	nameScreen.style.display = 'block';
	 }

	 /*
	  * Updates game screen after each game
	  */

	 function refreshScreen(result, game, player, cpu) {
	 	// Hide messages
	 	var messages = document.getElementsByClassName('player-choices__result');

	 	for (var i = 0; i < messages.length; i++) {
	 		messages[i].style.display = 'none';
	 	}

	 	// Show player and computer choices
	 	document.getElementById('choice-computer').innerHTML = cpu.capitalize();
	 	document.getElementById('choice-player').innerHTML = player.capitalize();

	 	// Update scores and show message
	 	switch(result) {
	 		case 'playerWin':
	 			document.getElementById('player-score').innerHTML = game.playerScore;
	 			document.getElementById('result-win').style.display = 'block';
	 			break;
	 		case 'computerWin':
	 			document.getElementById('computer-score').innerHTML = game.computerScore;
	 			document.getElementById('result-loss').style.display = 'block';
	 			break;
	 		case 'tie':
			 	document.getElementById('ties-score').innerHTML = game.ties;
			 	document.getElementById('result-tie').style.display = 'block';
			 	break;
	 	}
	 }

	/*
	 * Game object
	 */

	 var possibleChoices = ['rock', 'paper', 'scissors'];

	 function Game(player) {
	 	this.player = player;
	 	this.computerScore = 0;
	 	this.playerScore = 0;
	 	this.ties = 0;
	 }

	 Game.prototype.resolve = function(playerChoice, cpuChoice) {
	 	switch(playerChoice) {
	 		case 'rock':
	 			if (cpuChoice === 'rock') {
	 				this.ties += 1;
	 				return 'tie';	 				
	 			}
	 			else if (cpuChoice == 'paper') {
	 				this.computerScore += 1;
					return 'computerWin';	 				
	 			}
	 			else if (cpuChoice == 'scissors') {
	 				this.playerScore += 1;
	 				return 'playerWin';	 				
	 			}
	 		case 'paper':
	 			if (cpuChoice === 'rock') {
	 				this.playerScore += 1;
	 				return 'playerWin';	 				
	 			}
	 			else if (cpuChoice == 'paper') {
	 				this.ties += 1;
	 				return 'tie';	 				
	 			}
	 			else if (cpuChoice == 'scissors') {
	 				this.computerScore += 1;
					return 'computerWin';	 				
	 			}
	 		case 'scissors':
	 			if (cpuChoice === 'rock') {
	 				this.computerScore += 1;
					return 'computerWin';	 				
	 			}
	 			else if (cpuChoice == 'paper') {
	 				this.playerScore += 1;
	 				return 'playerWin';	 				
	 			}
	 			else if (cpuChoice == 'scissors') {
	 				this.ties += 1;
	 				return 'tie';	 				
	 			}
	 	}
	 };

	 Game.prototype.computerChoice = function() {
	 	return possibleChoices[Math.floor(Math.random() * possibleChoices.length)];
	 };

	String.prototype.capitalize = function() {
    	return this.charAt(0).toUpperCase() + this.slice(1);
	}
})();
@import url(http://fonts.googleapis.com/css?family=Sigmar+One);

* {
  box-sizing: border-box;
  font-family: 'Sigmar One', sans-serif;
  margin: 0;
  padding: 0;
}

html,
body {
  height: 100%;
}

.frame {
  border: 12px solid blue;
  height: 100%;
}

/*
 * Title screen
 */
#title-screen {
  left: 50%;
  position: fixed;  
  text-align: center;
  top: 50%;
  transform: translate(-50%, -50%);
}

  #game-title {
    color: #fff;
    font-size: 4em;
    line-height: 1.2em;
    margin: 0 0 1em;
    text-shadow:
       2px  3px 0 red,
      -2px -3px 0 green,
      -2px  3px 0 blue,
       2px -2px 0 blue,
       6px  6px 0 black;
  }

  #btn__new-game {
    background-color: #fff;
    border: 2px solid #000;
    bottom: 10%;
    font-family: 'Sigmar One';
    font-size: 2em;
    position: relative;
    padding: 10px 20px;
  }
/*
 * END Title screen
 */

/*
 * New game splash screen
 */
#game-screen {
  display: none;
  text-align: center;
}

  .game-screen__title {
    font-size: 2.5em;
    margin: 0 0 .5em;
  }

  .btn {
    background-color: #fff;
    border: 3px solid #333;
    display: inline-block;
    font-size: 1.1em;
    margin: .3em 0;
    padding: 12px 20px;
    width: 200px;
  }

  /* Total running score */
  .game-score {
    border-collapse: collapse;
    margin: 1em 0;
    width: 100%;
  }

    .game-score__label {
      color: #666;
      width: 33.3%;
    }

    .game-score__score {
      font-size: 1.5em;
    }

  /* Player choices and result message */
  .player-choices {
    border-collapse: collapse;
    margin: 2em 0;
    text-align: center;
    width: 100%;
  }
 
    .player-choices__results {
      vertical-align: middle;
    }

      .player-choices__result {
        display: none;
        font-size: 2em;
      }

        #result-win {
          color: green;
        }

        #result-loss {
          color: red;
        }

    .player-choices__label {
      color: #777;
      font-size: 1em;
      width: 30%;
    }

    .player-choices__choice {
      font-size: 2em;
    }

  /* Game Over Menu */
  #game-over-menu {
    display: none;
    margin: 1em 0;
  }

    .game-over-menu__btn {
      font-size: .8em;
      padding: 4px 8px;
      width: 120px;      
    }

  /* Quick to title screen button, always visible */
  #quit-to-title-screen {
    bottom: 30px;
    padding: 5px 8px;
    position: absolute;
    right: 30px;
  }

/*
 * END New game splash screen
 */

/*
 * Name screen
 */

#name-screen {
  display: none;  
  left: 50%;
  position: fixed;  
  text-align: center;
  top: 50%;
  transform: translate(-50%, -50%);
}

  #player-name-label {
    font-size: 2em;
  }

  #player-name {
    box-sizing: border-box;
    display: block;
    font-size: 1.5em;
    margin: 1em 0;
    padding: 20px;
    width: 100%;
  }

  #error {
    display: none;
    color: red;
    margin: 1em;
  }
<!DOCTYPE html>
<html>
<head>
	<title>Rock Paper Scissors</title>

	<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>

	<main class="frame">

	  <section id="title-screen">
	    <h1 id="game-title">Rock Paper Scissors</h1>
	    <button id="btn__new-game">Play Game</button>
	  </section>

	  <section id="name-screen">
		<form>
			<label for="player-name" id="player-name-label">What's your name?</label>
			<input type="text" name="playerName" id="player-name" />
			<p id="error">Enter a name between 1 and 8 chars long!</p>
			<button class="btn" type="submit" id="start-game">Start Game</button>
			<button class="btn" type="cancel" id="cancel-new-game">Cancel</button>
		</form>	  	
	  </section>
	  
	  <section id="game-screen">
	    <table class="game-score">
	      <thead>
	        <tr>
	          <th class="game-score__label">Computer</th>
	          <th class="game-score__label">Tie</th>
	          <th class="game-score__label name-label">Player</th>
	        </tr>
	      </thead>

	      <tbody>
	        <tr>
	          <td class="game-score__score" id="computer-score">0</td>
	          <td class="game-score__score" id="ties-score">0</td>
	          <td class="game-score__score" id="player-score">0</td>
	        </tr>
	      </tbody>
	    </table>


	    <h1 class="game-screen__title">Choose</h1>
	    <div class="choices">
	      <button class="btn play-button" id="rock">Rock</button>
	      <br />
	      <button class="btn play-button" id="paper">Paper</button>
	      <br />
	      <button class="btn play-button" id="scissors">Scissors</button>
	    </div>
	    
	    <table class="player-choices">
	      <tbody>
	        <tr>
	          <th class="player-choices__label">Computer</th>
	          <th class="player-choices__results" rowspan="2">
	            <h2 class="player-choices__result" id="result-win">YOU WIN!</h2>
	            <h2 class="player-choices__result" id="result-loss">YOU LOSE!</h2>
				<h2 class="player-choices__result" id="result-tie">IT'S A TIE!</h2>
	          </th>
	          <th class="player-choices__label name-label">Player</th>
	        </tr>
	        <tr>
	          <td class="player-choices__choice" id="choice-computer">?</td>
	          <td class="player-choices__choice" id="choice-player">?</td>
	        </tr>
	      </tbody>
	    </table>
	    
	    <div id="game-over-menu">
	      <button class="game-over-menu__btn" id="play-new-game">Play Again</button>
	      <button class="game-over-menu__btn" id="quit-game">Quit Game</button>
	    </div>

	    <button id="quit-to-title-screen">Quit</button>
	  </section>
	</main>

	<script type="text/javascript" src="main.js"></script>
</body>
</html>

\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

I would separate the code into different files, each representing a class. You can take a look at module patterns like AMD or CommonJS to control files dependency


I recommend the creation of a Stage class, then you can control screen state using a simple state machine:

function Stage(element) {
    this.element = element;
    this.changeState(new TitleScreenState());
}

Stage.prototype.changeState = function(state) {
    this.state = state;
    state.enter(this);
}

function GameScreenState() {}

GameScreenState.prototype.enter = function(stage) {
    stage.element.style.color = 'red';
}

var stage = new Stage(document.getElementById('my-stage'));
// to change state
stage.changeState(new GameScreenState());
stage.changeState(new NameScreenState());

At:

if (!name.value || name.value.length > 8) {
    showNameError();
    return false;
}
else {

you don't need the else.


At:

if (name.value.length > 0 && name.value.length <= 8) {

you could cache the length to increase readability:

var nameSize = name.value.length;

if (nameSize === 0 || nameSize > 8) {
    // ...
}
  • Cache document.getElementById('error') as you did with the other elements
  • You may have different classes for each possible choice (RockChoice, PaperChoice and ScissorsChoice) that implement compare functions receiving the other choice (Ye, I also miss Interfaces in JavaScript)
  • Method computerChoice from the Game class is a static method, right? You could remove it from the prototype and call it like: Game.computerChoice();
  • Code style looks OK but take a look at Idiomatic.js
  • In order to have a decent presentation layer, you may check ReactJS that have properly defined component state and properties. This is hard stuff that they made incredibly easy
\$\endgroup\$
3
  • \$\begingroup\$ Great review thanks. I have a hard time following your state machine. Doesn't the state need to be attached to a dom element? Your example shows 'my-stage' being passed in. What does mystage refer to? The screen I want to show? So I would have to create 3 Stage objects for each element? Do you mind clarifying this a little? \$\endgroup\$
    – Mohamad
    Commented Jan 5, 2015 at 17:54
  • \$\begingroup\$ It's also not immediately clear how to a previous stage becomes hidden... \$\endgroup\$
    – Mohamad
    Commented Jan 5, 2015 at 18:14
  • \$\begingroup\$ @Mohamad, my-stage is an element that holds gameScreen, nameScreen and titleScreen. You only need one stage object with 3 different states, that's the point. Then, instead of calling toTitleScreen, you would change to the TitleScreenState and inside its enter method you would mutate styles (example): stage.element.getElementByClassName('game-screen').style.display = none; \$\endgroup\$ Commented Jan 6, 2015 at 13:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.