Effect preview
Press the "click preview" button on the right to preview the current page, and click the link to preview the whole screen.
https://codepen.io/comehope/pen/mQYobz
Interactive Video
This video is interactive, you can pause the video at any time, edit the code in the video.
Please open it with chrome, safari, edge.
Part 1:
https://scrimba.com/p/pEgDAM/c7Q86ug
Part 2:
https://scrimba.com/p/pEgDAM/ckgBNAD
Part 3:
https://scrimba.com/p/pEgDAM/cG7bWc8
Part 4:
https://scrimba.com/p/pEgDAM/cez34fp
Source code download
The full source code of the daily front-end combat series is downloaded from github:
https://github.com/comehope/front-end-daily-challenges
Code interpretation
One of the basic skills of solving Sudoku is to quickly determine which numbers are missing in a row, a column or a Nine-palace grid. This project is a game for training and judging which numbers are missing in the Nine-palace grid. The process of the game is: first choose the difficulty of the game, there are Easy, Normal and Hard, which correspond to the lack of one, two and three numbers in the nine palaces. After the beginning of the game, use the keyboard to enter the missing numbers in the nine palaces. If all the answers come out, they will enter the next game, a total of five games. After the end of the five games, the game is over. In the course of the game, the upper left corner of the Nine Palaces will be accounted for, while the upper right corner will be accounted for.
The whole game is divided into four steps: static page layout, program logic, score timing and animation effect.
Page layout
Define the dom structure,. app is the container of the whole application, h1 is the title of the game, and. game is the main interface of the game. The sub-elements in. game include. message and. digits,. Message to indicate the game time. time, game number. round, score. score,. Digits is nine numbers:
<div class="app"> <h1>Sudoku Training</h1> <div class="game"> <div class="message"> <p> Time: <span class="time">00:00</span> </p> <p class="round">1/5</p> <p> Score: <span class="score">100</span> </p> </div> <div class="digits"> <span>1</span> <span>2</span> <span>3</span> <span>4</span> <span>5</span> <span>6</span> <span>7</span> <span>8</span> <span>9</span> </div> </div> </div>
Centralized display:
body { margin: 0; height: 100vh; display: flex; align-items: center; justify-content: center; background: silver; overflow: hidden; }
Define the width of the application and the vertical layout of the child elements:
.app { width: 300px; display: flex; flex-direction: column; align-items: center; justify-content: space-between; user-select: none; }
The title is brown:
h1 { margin: 0; color: sienna; }
The hint information is horizontal layout, the key content is bold:
.game .message { width: inherit; display: flex; justify-content: space-between; font-size: 1.2em; font-family: sans-serif; } .game .message span { font-weight: bold; }
The nine palaces are laid out with grid s, with brown frames and apricot white backgrounds.
.game .digits { box-sizing: border-box; width: 300px; height: 300px; padding: 10px; border: 10px solid sienna; display: grid; grid-template-columns: repeat(3, 1fr); grid-gap: 10px; } .game .digits span { width: 80px; height: 80px; background-color: blanchedalmond; font-size: 30px; font-family: sans-serif; text-align: center; line-height: 2.5em; color: sienna; position: relative; }
So far, the layout of the game area is completed, and then the layout chooses the interface of the difficulty of the game.
Add. select-level dom structure to the html file, which contains a list of difficulty levels and a button to start the game. play. The difficulty of the game is divided into. easy,. normal and. hard levels:
<div class="app"> <h1>Sudoku Training</h1> <div class="game"> <!-- slightly --> </div> <div class="select-level"> <div class="levels"> <input type="radio" name="level" id="easy" value="easy" checked="checked"> <label for="easy">Easy</label> <input type="radio" name="level" id="normal" value="normal"> <label for="normal">Normal</label> <input type="radio" name="level" id="hard" value="hard"> <label for="hard">Hard</label> </div> <div class="play">Play</div> </div> </div>
To select the difficulty container of the game, draw a circular frame with the vertical layout of the sub-elements:
.select-level { z-index: 2; box-sizing: border-box; width: 240px; height: 240px; border: 10px solid rgba(160, 82, 45, 0.8); border-radius: 50%; box-shadow: 0 0 0 0.3em rgba(255, 235, 205, 0.8), 0 0 1em 0.5em rgba(160, 82, 45, 0.8); display: flex; flex-direction: column; align-items: center; font-family: sans-serif; }
Layout 3 difficulty options, horizontal arrangement:
.select-level .levels { margin-top: 60px; width: 190px; display: flex; justify-content: space-between; }
Hide the input controls and display only their corresponding label:
.select-level .levels { position: relative; } .select-level input[type=radio] { visibility: hidden; position: absolute; left: 0; }
Set the label style to a circular button:
.select-level label { width: 56px; height: 56px; background-color: rgba(160, 82, 45, 0.8); border-radius: 50%; text-align: center; line-height: 56px; color: blanchedalmond; cursor: pointer; }
When an input corresponding to a label is selected, the background color of the label is darkened to show the difference:
.select-level input[type=radio]:checked + label { background-color: sienna; }
Set the Start Game button. play style and interaction effect:
.select-level .play { width: 120px; height: 30px; background-color: sienna; color: blanchedalmond; text-align: center; line-height: 30px; border-radius: 30px; text-transform: uppercase; cursor: pointer; margin-top: 30px; font-size: 20px; letter-spacing: 2px; } .select-level .play:hover { background-color: saddlebrown; } .select-level .play:active { transform: translate(2px, 2px); }
So far, select the difficulty of the game interface layout to complete, and then layout the end of the game interface.
game-over contains an h2 title, a two-line paragraph p showing the final result, and a play-again button. Final results include final-time and final-score:
<div class="app"> <h1>Sudoku Training</h1> <div class="game"> <!-- slightly --> </div> <div class="select-level"> <!-- slightly --> </div> <div class="game-over"> <h2>Game Over</h2> <p> Time: <span class="final-time">00:00</span> </p> <p> Score: <span class="final-score">3000</span> </p> <div class="again">Play Again</div> </div> </div>
Because the layout of the end-of-game interface is similar to that of the difficulty-of-game interface, borrow the code of. select-level:
.select-level, .game-over { z-index: 2; box-sizing: border-box; width: 240px; height: 240px; border: 10px solid rgba(160, 82, 45, 0.8); border-radius: 50%; box-shadow: 0 0 0 0.3em rgba(255, 235, 205, 0.8), 0 0 1em 0.5em rgba(160, 82, 45, 0.8); display: flex; flex-direction: column; align-items: center; font-family: sans-serif; }
The title and final results are in brown letters:
.game-over h2 { margin-top: 40px; color: sienna; } .game-over p { margin: 3px; font-size: 20px; color: sienna; }
The "play Again" button. again's style is similar to the start of the game. play's style is similar, so it also borrows. play's code:
.select-level .play, .game-over .again { width: 120px; height: 30px; background-color: sienna; color: blanchedalmond; text-align: center; line-height: 30px; border-radius: 30px; text-transform: uppercase; cursor: pointer; } .select-level .play { margin-top: 30px; font-size: 20px; letter-spacing: 2px; } .select-level .play:hover, .game-over .again:hover { background-color: saddlebrown; } .select-level .play:active, .game-over .again:active { transform: translate(2px, 2px); } .game-over .again { margin-top: 10px; }
Choose the game difficulty interface. select-level and end-game interface. Play-over in the middle of the game container:
.app { position: relative; } .select-level, .game-over { position: absolute; bottom: 40px; }
So far, the game interface, game selection, game difficulty interface, select-level and end-game interface, game-over have been laid out. Next, do some preparation for dynamic programs.
Hide the selection difficulty interface, selection-level and end-game interface. game-over, and set their visibility properties in the script when they need to be rendered:
.select-level, .game-over { visibility: hidden; }
In the game, when you choose the difficulty interface, select-level and end interface. game-over appears, you should make the game interface. game blurred and add a slowdown time. game.stop will be called in the script:
.game { transition: 0.3s; } .game.stop { filter: blur(10px); }
In the game, when the wrong number is filled in, draw a red edge on the wrong number; when the number is filled in correctly, change the background color of the number to chocolate color. Game. digits span. wrong and. game. digits span. correct are called in the script:
.game .digits span.wrong { border: 2px solid crimson; } .game .digits span.correct { background-color: chocolate; color: gold; }
So far, complete all layout and style design.
II. Procedural Logic
After introducing the lodash toolkit, we will use some array functions provided by lodash:
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
Before writing program logic, define several constants that store business data. ALL_DIGITS stores all the alternative numbers, i.e. from 1 to 9; ANSWER_COUNT stores the number of numbers to be answered with different difficulty, easy difficulty needs to answer one number, normal difficulty needs to answer two numbers and hard difficulty needs to answer three numbers; ROUND_COUNT stores the number of games per game, the default is five games; SCORE_RULE stores the correct and wrong time. Number change, answer right plus 100 points, answer wrong deduct 10 points. The advantage of defining these constants is to avoid magic numbers in programs and improve program readability:
const ALL_DIGITS = ['1','2','3','4','5','6','7','8','9'] const ANSWER_COUNT = {EASY: 1, NORMAL: 2, HARD: 3} const ROUND_COUNT = 5 const SCORE_RULE = {CORRECT: 100, WRONG: -10}
A DOM object is defined to refer to the DOM element, each attribute of which is a DOM element, and the key value is consistent with the class class class name. Most of the DOM elements are an element object, only dom.digits and dom.levels are arrays containing multiple element objects. In addition, dom.level is used to obtain the selected difficulty, because its value varies with the user's choice, so it uses functions to return real-time results:
const $ = (selector) => document.querySelectorAll(selector) const dom = { game: $('.game')[0], digits: Array.from($('.game .digits span')), time: $('.game .time')[0], round: $('.game .round')[0], score: $('.game .score')[0], selectLevel: $('.select-level')[0], level: () => {return $('input[type=radio]:checked')[0]}, play: $('.select-level .play')[0], gameOver: $('.game-over')[0], again: $('.game-over .again')[0], finalTime: $('.game-over .final-time')[0], finalScore: $('.game-over .final-score')[0], }
In the course of the game, we need to modify the content of dom elements at any time according to the progress of the game. These modifications are also defined in the render object, so that the main logic of the program does not need to care about specific dom operations. Each attribute of the render object is a dom operation, structured as follows:
const render = { initDigits: () => {}, updateDigitStatus: () => {}, updateTime: () => {}, updateScore: () => {}, updateRound: () => {}, updateFinal: () => {}, }
Now let's write down these dom operations one by one.
render.initDigits is used to initialize the nine palaces. It receives an array of text. According to different difficulty levels, the length of the array may be 8 (easy difficulty), 7 (normal difficulty) or 6 (hard difficulty). Firstly, it is filled up with nine arrays of length and insufficient number of elements to fill empty characters, and then they are randomly assigned to nine palace lattices:
const render = { initDigits: (texts) => { allTexts = texts.concat(_.fill(Array(ALL_DIGITS.length - texts.length), '')) _.shuffle(dom.digits).forEach((digit, i) => { digit.innerText = allTexts[i] digit.className = '' }) }, //... }
render.updateDigitStatus is used to update the status of a single grid in the nine palaces. It takes two parameters, text.
It's the number in the grid, and is Answer indicates whether it's the answer. The default style of the lattice is dark text with light background. If the number is not the answer, that is, wrong answer, wrong style will be added to the lattice, and the lattice will be marked with red edge. If the number is the answer, that is, the number will be displayed in a blank space and correct ed style will be added to the lattice, the lattice style will be changed to light text with dark background:
const render = { //... updateDigitStatus: (text, isAnswer) => { if (isAnswer) { let digit = _.find(dom.digits, x => (x.innerText == '')) digit.innerText = text digit.className = 'correct' } else { _.find(dom.digits, x => (x.innerText == text)).className = 'wrong' } }, //... }
render.updateTime is used to update the time and render.updateScore is used to update the score:
const render = { //... updateTime: (value) => { dom.time.innerText = value.toString() }, updateScore: (value) => { dom.score.innerText = value.toString() }, //... }
render.updateRound is used to update the current number of bureaux in the format of "n/m":
const render = { //... updateRound: (currentRound) => { dom.round.innerText = [ currentRound.toString(), '/', ROUND_COUNT.toString(), ].join('') }, //... }
render.updateFinal is used to update the final score in the end-of-game interface:
const render = { //... updateFinal: () => { dom.finalTime.innerText = dom.time.innerText dom.finalScore.innerText = dom.score.innerText }, }
Next, we define the overall logical structure of the program. When the page is loaded and the init() function is executed, the init() function initializes the whole game by calling the startGame() function when the start game button dom.play is clicked, so that the playAgain() function is called when the button dom.again is clicked again, and the event handler pressKey() is triggered when the keyboard is pressed. () Function to start a new game:
window.onload = init function init() { dom.play.addEventListener('click', startGame) dom.again.addEventListener('click', playAgain) window.addEventListener('keyup', pressKey) newGame() } function newGame() { //... } function startGame() { //... } function playAgain() { //... } function pressKey() { //... }
When the game starts, it blurs the interface of the game and exhales the interface of choosing the difficulty of the game:
function newGame() { dom.game.classList.add('stop') dom.selectLevel.style.visibility = 'visible' }
When the difficulty of the game is selected and the dom.play button is clicked, the interface of selecting the difficulty of the game is hidden, and the interface of the game returns to normal. Then the number of answers calculated according to the difficulty of the game selected by the user is stored in the global variable answerCount, and a game is started by calling new Round ():
let answerCount function startGame() { dom.game.classList.remove('stop') dom.selectLevel.style.visibility = 'hidden' answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()] newRound() }
When a game begins, all candidate numbers are scrambled to generate a global array variable digits. Each element of digits contains three attributes. Text attributes represent digital text, isAnswer attributes indicate whether the number is the answer, isPressed indicates whether the number has been pressed or not. The initial value of isPressed is false. Then, digits are rendered into the Nine Palaces:
let digits function newRound() { digits = _.shuffle(ALL_DIGITS).map((x, i) => { return { text: x, isAnwser: (i < answerCount), isPressed: false } }) render.initDigits(_.filter(digits, x => !x.isAnwser).map(x => x.text)) }
When the user presses the keyboard, if the key pressed is not a candidate text, the key event is ignored. Through the text of the key, the corresponding element digit is found in the digits array to determine whether the key has been pressed, and if it has been pressed, it also exits the event processing. Next, for the key that has not been pressed, mark the key that has been pressed on the corresponding digit object, and update the display status of the key. If the user is not pressing the answer number, the grid of the number will be red. If the user is pressing the answer number, the number will be highlighted:
function pressKey(e) { if (!ALL_DIGITS.includes(e.key)) return; let digit = _.find(digits, x => (x.text == e.key)) if (digit.isPressed) return; digit.isPressed = true render.updateDigitStatus(digit.text, digit.isAnwser) }
When the user has pressed all the answer numbers, the game ends and a new one begins:
function pressKey(e) { if (!ALL_DIGITS.includes(e.key)) return; let digit = _.find(digits, x => (x.text == e.key)) if (digit.isPressed) return; digit.isPressed = true render.updateDigitStatus(digit.text, digit.isAnwser) //Determine whether the user has pressed all the answer numbers let hasPressedAllAnswerDigits = (_.filter(digits, (x) => (x.isAnwser && x.isPressed)).length == answerCount) if (!hasPressedAllAnswerDigits) return; newRound() }
Add a global variable round to record the current number of games. At the beginning of the game, its initial value is 0. At the beginning of each game, its value is 1. Update the number of games dom.round in the game interface:
let round function newGame() { round = 0 //Initialization Bureau Number dom.game.classList.add('stop') dom.selectLevel.style.visibility = 'visible' } function startGame() { render.updateRound(1) //The number of bureaus in the initialization page dom.game.classList.remove('stop') dom.selectLevel.style.visibility = 'hidden' answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()] newRound() } function newRound() { digits = _.shuffle(ALL_DIGITS).map((x, i) => { return { text: x, isAnwser: (i < answerCount), isPressed: false } }) render.initDigits(_.filter(digits, x => !x.isAnwser).map(x => x.text)) //The number of games plus 1 at the beginning of each game round++ render.updateRound(round) }
Current rounds increase to the number of game directories defined by constant ROUND_COUNT. At the end of the game, call the gameOver() function, otherwise call the new Round () function to start a new round:
function pressKey(e) { if (!ALL_DIGITS.includes(e.key)) return; let digit = _.find(digits, x => (x.text == e.key)) if (digit.isPressed) return; digit.isPressed = true render.updateDigitStatus(digit.text, digit.isAnwser) let hasPressedAllAnswerDigits = (_.filter(digits, (x) => (x.isAnwser && x.isPressed)).length == answerCount) if (!hasPressedAllAnswerDigits) return; //Judge whether you have played enough for the General Administration let hasPlayedAllRounds = (round == ROUND_COUNT) if (hasPlayedAllRounds) { gameOver() } else { newRound() } }
At the end of the game, make the game interface blurred, call up the end of the game interface, show the final results:
function gameOver() { render.updateFinal() dom.game.classList.add('stop') dom.gameOver.style.visibility = 'visible' }
In the end of the game interface, the user can click the dom.again button, if clicked on this button, the end of the game interface will be hidden, start a new game, which returns to the process of new Game ():__________
function playAgain() { dom.game.classList.remove('stop') dom.gameOver.style.visibility = 'hidden' newGame() }
So far, the whole process of the game has run through, at this time the script is as follows:
const ALL_DIGITS = ['1','2','3','4','5','6','7','8','9'] const ANSWER_COUNT = {EASY: 1, NORMAL: 2, HARD: 3} const ROUND_COUNT = 3 const SCORE_RULE = {CORRECT: 100, WRONG: -10} const $ = (selector) => document.querySelectorAll(selector) const dom = { game: $('.game')[0], digits: Array.from($('.game .digits span')), time: $('.game .time')[0], round: $('.game .round')[0], score: $('.game .score')[0], selectLevel: $('.select-level')[0], level: () => {return $('input[type=radio]:checked')[0]}, play: $('.select-level .play')[0], gameOver: $('.game-over')[0], again: $('.game-over .again')[0], finalTime: $('.game-over .final-time')[0], finalScore: $('.game-over .final-score')[0], } const render = { initDigits: (texts) => { allTexts = texts.concat(_.fill(Array(ALL_DIGITS.length - texts.length), '')) _.shuffle(dom.digits).forEach((digit, i) => { digit.innerText = allTexts[i] digit.className = '' }) }, updateDigitStatus: (text, isAnswer) => { if (isAnswer) { let digit = _.find(dom.digits, x => (x.innerText == '')) digit.innerText = text digit.className = 'correct' } else { _.find(dom.digits, x => (x.innerText == text)).className = 'wrong' } }, updateTime: (value) => { dom.time.innerText = value.toString() }, updateScore: (value) => { dom.score.innerText = value.toString() }, updateRound: (currentRound) => { dom.round.innerText = [ currentRound.toString(), '/', ROUND_COUNT.toString(), ].join('') }, updateFinal: () => { dom.finalTime.innerText = dom.time.innerText dom.finalScore.innerText = dom.score.innerText }, } let answerCount, digits, round window.onload = init function init() { dom.play.addEventListener('click', startGame) dom.again.addEventListener('click', playAgain) window.addEventListener('keyup', pressKey) newGame() } function newGame() { round = 0 dom.game.classList.add('stop') dom.selectLevel.style.visibility = 'visible' } function startGame() { render.updateRound(1) dom.game.classList.remove('stop') dom.selectLevel.style.visibility = 'hidden' answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()] newRound() } function newRound() { digits = _.shuffle(ALL_DIGITS).map((x, i) => { return { text: x, isAnwser: (i < answerCount), isPressed: false } }) render.initDigits(_.filter(digits, x => !x.isAnwser).map(x => x.text)) round++ render.updateRound(round) } function gameOver() { render.updateFinal() dom.game.classList.add('stop') dom.gameOver.style.visibility = 'visible' } function playAgain() { dom.game.classList.remove('stop') dom.gameOver.style.visibility = 'hidden' newGame() } function pressKey(e) { if (!ALL_DIGITS.includes(e.key)) return; let digit = _.find(digits, x => (x.text == e.key)) if (digit.isPressed) return; digit.isPressed = true render.updateDigitStatus(digit.text, digit.isAnwser) let hasPressedAllAnswerDigits = (_.filter(digits, (x) => (x.isAnwser && x.isPressed)).length == answerCount) if (!hasPressedAllAnswerDigits) return; let hasPlayedAllRounds = (round == ROUND_COUNT) if (hasPlayedAllRounds) { gameOver() } else { newRound() } }
III. Scoring and Timing
Next deal with the score and time, first deal with the score.
First, we declare a global variable score for storing scores, set its initial value to 0 before the start of the new game, and initialize the score in the page at the beginning of the game:
let score function newGame() { round = 0 score = 0 //Initialization score dom.game.classList.add('stop') dom.selectLevel.style.visibility = 'visible' } function startGame() { render.updateRound(1) render.updateScore(0) //Score in Initialization Page dom.game.classList.remove('stop') dom.selectLevel.style.visibility = 'hidden' answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()] newRound() }
Record different scores in user keystroke events according to whether the keys pressed are answers:
function pressKey(e) { if (!ALL_DIGITS.includes(e.key)) return; let digit = _.find(digits, x => (x.text == e.key)) if (digit.isPressed) return; digit.isPressed = true render.updateDigitStatus(digit.text, digit.isAnwser) //Cumulative score score += digit.isAnwser ? SCORE_RULE.CORRECT : SCORE_RULE.WRONG render.updateScore(score) let hasPressedAllAnswerDigits = (_.filter(digits, (x) => (x.isAnwser && x.isPressed)).length == answerCount) if (!hasPressedAllAnswerDigits) return; let hasPlayedAllRounds = (round == ROUND_COUNT) if (hasPlayedAllRounds) { gameOver() } else { newRound() } }
Next, the processing time. First, create a timer class Timer, whose parameter is a function to render time to the page. In addition, Timer has two methods, start() and stop (), to open and stop the timer. The timer executes tickTock() function once a second:
function Timer(render) { this.render = render this.t = {}, this.start = () => { this.t = setInterval(this.tickTock, 1000); } this.stop = () => { clearInterval(this.t) } }
Define a variable time for recording time, which has an initial value of 0 minutes and 0 seconds, add seconds to 1 in the tickTock() function, and call the rendering function to write the current time to the page:
function Timer(render) { this.render = render this.t = {} this.time = { minute: 0, second: 0, } this.tickTock = () => { this.time.second ++; if (this.time.second == 60) { this.time.minute ++ this.time.second = 0 } render([ this.time.minute.toString().padStart(2, '0'), ':', this.time.second.toString().padStart(2, '0'), ].join('')) } this.start = () => { this.t = setInterval(this.tickTock, 1000) } this.stop = () => { clearInterval(this.t) } }
Initialize the time in the page at the start of the game:
function startGame() { render.updateRound(1) render.updateScore(0) render.updateTime('00:00') //Initialize the time in the page dom.game.classList.remove('stop') dom.selectLevel.style.visibility = 'hidden' answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()] newRound() }
Define a global variable timer that stores timers. Initialize the timer when creating the game, start the timer at the beginning of the game, and stop the timer at the end of the game:
let timer function newGame() { round = 0 score = 0 timer = new Timer(render.updateTime) //create-timer dom.game.classList.add('stop') dom.selectLevel.style.visibility = 'visible' } function startGame() { render.updateRound(1) render.updateScore(0) render.updateTime('00:00') dom.game.classList.remove('stop') dom.selectLevel.style.visibility = 'hidden' answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()] newRound() timer.start() //Start timing } function gameOver() { timer.stop() //Stop timing render.updateFinal() dom.game.classList.add('stop') dom.gameOver.style.visibility = 'visible' }
At this point, the clock is ready to run, starting at 0 minutes and 0 seconds at the beginning of the game, and stopping at the end of the game.
The last step, when the game is over, should not respond to the user's key events. To this end, we define a variable canPress that indicates whether a key can be pressed. When creating a new game, its state is not pressing. After the game starts, it becomes pressing. After the game ends, it becomes pressing:
let canPress function newGame() { round = 0 score = 0 time = { minute: 0, second: 0 } timer = new Timer() canPress = false //Initialize keypable flags dom.game.classList.add('stop') dom.selectLevel.style.visibility = 'visible' } function startGame() { render.updateRound(1) render.updateScore(0) render.updateTime(0, 0) dom.game.classList.remove('stop') dom.selectLevel.style.visibility = 'hidden' answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()] newRound() timer.start(tickTock) canPress = true //After the game starts, you can press the button } function gameOver() { canPress = false //No more keystrokes after the game is over timer.stop() render.updateFinal() dom.game.classList.add('stop') dom.gameOver.style.visibility = 'visible' }
In the keystroke event handler, the first step is to determine whether the keystroke is allowed or not, and if not, to exit the event handler:
function pressKey(e) { if (!canPress) return; //Determine whether keystrokes are allowed if (!ALL_DIGITS.includes(e.key)) return; let digit = _.find(digits, x => (x.text == e.key)) if (digit.isPressed) return; digit.isPressed = true render.updateDigitStatus(digit.text, digit.isAnwser) score += digit.isAnwser ? SCORE_RULE.CORRECT : SCORE_RULE.WRONG render.updateScore(score) let hasPressedAllAnswerDigits = (_.filter(digits, (x) => (x.isAnwser && x.isPressed)).length == answerCount) if (hasPressedAllAnswerDigits) { newRound() } }
So far, the design of scoring timing has been completed, and the script at this time is as follows:
const ALL_DIGITS = ['1','2','3','4','5','6','7','8','9'] const ANSWER_COUNT = {EASY: 1, NORMAL: 2, HARD: 3} const ROUND_COUNT = 3 const SCORE_RULE = {CORRECT: 100, WRONG: -10} const $ = (selector) => document.querySelectorAll(selector) const dom = { //Slightly, the same code as before } const render = { //Slightly, the same code as before } let answerCount, digits, round, score, timer, canPress window.onload = init function init() { //Slightly, the same code as before } function newGame() { round = 0 score = 0 timer = new Timer(render.updateTime) canPress = false dom.game.classList.add('stop') dom.selectLevel.style.visibility = 'visible' } function startGame() { render.updateRound(1) render.updateScore(0) render.updateTime(0, 0) dom.game.classList.remove('stop') dom.selectLevel.style.visibility = 'hidden' answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()] newRound() timer.start() canPress = true } function newRound() { //Slightly, the same code as before } function gameOver() { canPress = false timer.stop() render.updateFinal() dom.game.classList.add('stop') dom.gameOver.style.visibility = 'visible' } function playAgain() { //Slightly, the same code as before } function pressKey(e) { if (!canPress) return; if (!ALL_DIGITS.includes(e.key)) return; let digit = _.find(digits, x => (x.text == e.key)) if (digit.isPressed) return; digit.isPressed = true render.updateDigitStatus(digit.text, digit.isAnwser) score += digit.isAnwser ? SCORE_RULE.CORRECT : SCORE_RULE.WRONG render.updateScore(score) let hasPressedAllAnswerDigits = (_.filter(digits, (x) => (x.isAnwser && x.isPressed)).length == answerCount) if (!hasPressedAllAnswerDigits) return; let hasPlayedAllRounds = (round == ROUND_COUNT) if (hasPlayedAllRounds) { gameOver() } else { newRound() } }
IV. Animation effects
Introduce gsap animation library:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/2.0.2/TweenMax.min.js"></script>
There are six animation effects in the game. They are the exit and entry of the Nine Palaces, the display and hiding of the difficulty interface of the game, and the display and hiding of the end interface of the game. In order to centralize the management of animation effects, we define a global constant animation. Each attribute of animation is a function to achieve an animation effect. The structure is as follows. Note that because the selection of game difficulty interface is similar to that of game end interface, they share the same animation effect. When calling the function, we need to pass in a dom object whose parameter element specifies the animation. :
const animation = { digitsFrameOut: () => { //Nine Palaces Come on }, digitsFrameIn: () => { //Nine Gongge Entrance }, showUI: (element) => { //Display and Select Game Difficulty Interface and Game End Interface }, frameOut: (element) => { //Hidden Choice of Game Difficulty Interface and Game End Interface }, }
Determine the timing of these animations:
function newGame() { round = 0 score = 0 timer = new Timer(render.updateTime) canPress = false //Choose Game Difficulty Interface - Display dom.game.classList.add('stop') dom.selectLevel.style.visibility = 'visible' } function startGame() { render.updateRound(1) render.updateScore(0) render.updateTime('00:00') //Choose Game Difficulty Interface - Hide dom.game.classList.remove('stop') dom.selectLevel.style.visibility = 'hidden' answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()] newRound() timer.start() canPress = true } function newRound() { //Nine Palaces - Exit digits = _.shuffle(ALL_DIGITS).map((x, i) => { return { text: x, isAnwser: (i < answerCount), isPressed: false } }) render.initDigits(_.filter(digits, x => !x.isAnwser).map(x => x.text)) //Nine Palaces - Entrance round++ render.updateRound(round) } function gameOver() { canPress = false timer.stop() render.updateFinal() //Game End Interface - Display dom.game.classList.add('stop') dom.gameOver.style.visibility = 'visible' } function playAgain() { //Game End Interface - Hide dom.game.classList.remove('stop') dom.gameOver.style.visibility = 'hidden' newGame() }
Move the code of the location of the current animation opportunity to the animation object, and the animation of the Nine Palaces Exit and Entry is currently empty:
const animation = { digitsFrameOut: () => { //Nine Palaces Come on }, digitsFrameIn: () => { //Nine Gongge Entrance }, showUI: (element) => { //Display and Select Game Difficulty Interface and Game End Interface dom.game.classList.add('stop') element.style.visibility = 'visible' }, hideUI: (element) => { //Hidden Choice of Game Difficulty Interface and Game End Interface dom.game.classList.remove('stop') element.style.visibility = 'hidden' }, }
The animation function corresponding to animation is called in the position of the animation time, because the animation is executed for a long time, and the next animation will start after the last animation is finished, so we use async/await grammar to let the adjacent animations execute sequentially:
async function newGame() { round = 0 score = 0 timer = new Timer(render.updateTime) canPress = false // Choose Game Difficulty Interface - Display await animation.showUI(dom.selectLevel) } async function startGame() { render.updateRound(1) render.updateScore(0) render.updateTime('00:00') // Choose Game Difficulty Interface - Hide await animation.hideUI(dom.selectLevel) answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()] newRound() timer.start() canPress = true } async function newRound() { //Nine Palaces - Exit await animation.digitsFrameOut() digits = _.shuffle(ALL_DIGITS).map((x, i) => { return { text: x, isAnwser: (i < answerCount), isPressed: false } }) render.initDigits(_.filter(digits, x => !x.isAnwser).map(x => x.text)) //Nine Palaces - Entrance await animation.digitsFrameIn() round++ render.updateRound(round) } async function gameOver() { canPress = false timer.stop() render.updateFinal() // Game End Interface - Display await animation.showUI(dom.gameOver) } async function playAgain() { // Game End Interface - Hide await animation.hideUI(dom.gameOver) newGame() }
Next, we begin to design animation effects.
The animation. digits FrameOut is an exit animation of the nine palaces, each of which rotates and disappears. Note that in order to match the async/await syntax, we have the function return a Promise object:
const animation = { digitsFrameOut: () => { return new Promise(resolve => { new TimelineMax() .staggerTo(dom.digits, 0, {rotation: 0}) .staggerTo(dom.digits, 1, {rotation: 360, scale: 0, delay: 0.5}) .timeScale(2) .eventCallback('onComplete', resolve) }) }, //... }
The animation.digits FrameIn is a Nine-palace entry animation. Its animation effect is that each grid rotates and appears, and the occurrence time of each grid is slightly delayed:
const animation = { //... digitsFrameIn: () => { return new Promise(resolve => { new TimelineMax() .staggerTo(dom.digits, 0, {rotation: 0}) .staggerTo(dom.digits, 1, {rotation: 360, scale: 1}, 0.1) .timeScale(2) .eventCallback('onComplete', resolve) }) }, //... }
Animation. show UI is an animation that shows the difficulty interface and the end interface of the game. Its effect is to fall from a high place and rebound slightly at the bottom to simulate the effect of falling objects:
const animation = { //... showUI: (element) => { dom.game.classList.add('stop') return new Promise(resolve => { new TimelineMax() .to(element, 0, {visibility: 'visible', x: 0}) .from(element, 1, {y: '-300px', ease: Elastic.easeOut.config(1, 0.3)}) .timeScale(1) .eventCallback('onComplete', resolve) }) }, //... }
The animation.hideUI is an animation that hides the choice of the difficulty interface and the end interface of the game. It moves the screen from its normal position to the right:
const animation = { //... hideUI: (element) => { dom.game.classList.remove('stop') return new Promise(resolve => { new TimelineMax() .to(element, 1, {x: '300px', ease: Power4.easeIn}) .to(element, 0, {visibility: 'hidden'}) .timeScale(2) .eventCallback('onComplete', resolve) }) }, }
So far, the animation effect of the whole game has been completed, the whole code is as follows:
const ALL_DIGITS = ['1','2','3','4','5','6','7','8','9'] const ANSWER_COUNT = {EASY: 1, NORMAL: 2, HARD: 3} const ROUND_COUNT = 3 const SCORE_RULE = {CORRECT: 100, WRONG: -10} const $ = (selector) => document.querySelectorAll(selector) const dom = { //Slightly, the same as before adding animation } const render = { //Slightly, the same as before adding animation } const animation = { digitsFrameOut: () => { return new Promise(resolve => { new TimelineMax() .staggerTo(dom.digits, 0, {rotation: 0}) .staggerTo(dom.digits, 1, {rotation: 360, scale: 0, delay: 0.5}) .timeScale(2) .eventCallback('onComplete', resolve) }) }, digitsFrameIn: () => { return new Promise(resolve => { new TimelineMax() .staggerTo(dom.digits, 0, {rotation: 0}) .staggerTo(dom.digits, 1, {rotation: 360, scale: 1}, 0.1) .timeScale(2) .eventCallback('onComplete', resolve) }) }, showUI: (element) => { dom.game.classList.add('stop') return new Promise(resolve => { new TimelineMax() .to(element, 0, {visibility: 'visible', x: 0}) .from(element, 1, {y: '-300px', ease: Elastic.easeOut.config(1, 0.3)}) .timeScale(1) .eventCallback('onComplete', resolve) }) }, hideUI: (element) => { dom.game.classList.remove('stop') return new Promise(resolve => { new TimelineMax() .to(element, 1, {x: '300px', ease: Power4.easeIn}) .to(element, 0, {visibility: 'hidden'}) .timeScale(2) .eventCallback('onComplete', resolve) }) }, } let answerCount, digits, round, score, timer, canPress window.onload = init function init() { //Slightly, the same as before adding animation } async function newGame() { round = 0 score = 0 timer = new Timer(render.updateTime) canPress = false await animation.showUI(dom.selectLevel) } async function startGame() { render.updateRound(1) render.updateScore(0) render.updateTime('00:00') await animation.hideUI(dom.selectLevel) answerCount = ANSWER_COUNT[dom.level().value.toUpperCase()] newRound() timer.start() canPress = true } async function newRound() { await animation.digitsFrameOut() digits = _.shuffle(ALL_DIGITS).map((x, i) => { return { text: x, isAnwser: (i < answerCount), isPressed: false } }) render.initDigits(_.filter(digits, x => !x.isAnwser).map(x => x.text)) await animation.digitsFrameIn() round++ render.updateRound(round) } async function gameOver() { canPress = false timer.stop() render.updateFinal() await animation.showUI(dom.gameOver) } async function playAgain() { await animation.hideUI(dom.gameOver) newGame() } function pressKey(e) { //Slightly, the same as before adding animation } function tickTock() { //Slightly, the same as before adding animation }
Be accomplished!
Finally, an interactive flow chart is attached for your understanding.