From 5697684ac0e34407390c2899c4bbbfdbf644447a Mon Sep 17 00:00:00 2001 From: Adrien Date: Mon, 28 Oct 2019 12:41:09 +0100 Subject: [PATCH] format --- css/index.css | 101 +-- css/webtris.css | 177 +++--- index.php | 70 +-- js/webtris.js | 1565 ++++++++++++++++++++++++----------------------- webtris.html | 82 +-- 5 files changed, 1003 insertions(+), 992 deletions(-) diff --git a/css/index.css b/css/index.css index 6f6de3b..e4eb47f 100644 --- a/css/index.css +++ b/css/index.css @@ -1,50 +1,51 @@ -@font-face { - font-family: 'Share Tech'; - font-style: normal; - font-weight: 400; - src: local('Share Tech Regular'), local('ShareTech-Regular'), url(../fonts/ShareTech.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - -* { - color: white; - font-family: 'Share Tech'; - font-size: 1.05em; -} - -body { - background-image: url("../images/bg.jpg"); - background-size: cover; -} - -h1 { - font-size: 3em; - margin: 20px; - text-shadow: 3px 2px rgb(153, 145, 175); - text-align: center; -} - -button { - color: black; - width: 100%; -} - -a { - color: lightcyan; - text-decoration: none; -} - -#actions { - display: grid; - grid-template-columns: repeat(4, 1fr); - grid-gap: 20px; - margin: 80px auto; - width: 700px; - justify-items: left; -} - -.play { - text-align: center; - text-shadow: 2px 1px rgb(153, 145, 175); - font-size: 1.5em; -} +@font-face { + font-family: 'Share Tech'; + font-style: normal; + font-weight: 400; + src: local('Share Tech Regular'), local('ShareTech-Regular'), url(../fonts/ShareTech.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +* { + color: white; + font-family: 'Share Tech'; + font-size: 1.05em; +} + +body { + background-image: url("../images/bg.jpg"); + background-size: cover; +} + +h1 { + font-size: 50px; + margin: 40px; + text-shadow: 3px 2px rgb(153, 145, 175); + text-align: center; +} + +button { + color: black; + width: 100%; +} + +a { + color: lightcyan; + text-decoration: none; +} + +#actions { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-gap: 20px; + margin: 40px auto; + width: 700px; + justify-items: left; +} + +#play { + text-align: center; + text-shadow: 2px 1px rgb(153, 145, 175); + font-size: 1.5em; + margin 40px; +} diff --git a/css/webtris.css b/css/webtris.css index 8e292e8..c5b3d98 100644 --- a/css/webtris.css +++ b/css/webtris.css @@ -1,89 +1,90 @@ -@font-face { - font-family: 'Share Tech'; - font-style: normal; - font-weight: 400; - src: local('Share Tech Regular'), local('ShareTech-Regular'), url(../fonts/ShareTech.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} -@font-face { - font-family: 'Share Tech Mono'; - font-style: normal; - font-weight: 400; - src: local('Share Tech Mono Regular'), local('ShareTechMono-Regular'), url(../fonts/ShareTechMono.woff2) format('woff2'); - unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; -} - -* { - padding: 0; - margin: 0; - color: white; - font-family: 'Share Tech'; -} - -body { - background-image: url("../images/bg.jpg"); - background-size: cover; -} - -h1 { - font-size: 3em; - margin: 40px 20px 20px 20px; - text-shadow: 3px 2px rgb(153, 145, 175); - text-align: center; -} - -canvas { - display: block; - flex-shrink: 0; -} - -.flex-columns { - display: flex; - flex-direction: row; - justify-content: center; - margin: auto; -} - -.flex-space { - flex-grow: 2; -} - -.flex-rows { - display: flex; - flex-direction: column; - margin: 5% 2%; - height: 400px; - width: 150px; -} - -#hold { - width: 120px; -} - -#stats { - display: flex; - flex-direction: row; - margin: 10% 0; - font-size: 1.2em; -} - -#stats-names { - font-family: 'Share Tech'; - text-align: left; -} - -#stats-values { - text-align: right; - font-family: 'Share Tech Mono'; - min-width: 90px; -} - -#matrix { - margin: 5% 2%; - border: 0.5px solid grey; -} - -#next { - width: 120px; - margin: 5% 2%; +@font-face { + font-family: 'Share Tech'; + font-style: normal; + font-weight: 400; + src: local('Share Tech Regular'), local('ShareTech-Regular'), url(../fonts/ShareTech.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +@font-face { + font-family: 'Share Tech Mono'; + font-style: normal; + font-weight: 400; + src: local('Share Tech Mono Regular'), local('ShareTechMono-Regular'), url(../fonts/ShareTechMono.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +* { + padding: 0; + margin: 0; + color: white; + font-family: 'Share Tech'; +} + +body { + background-image: url("../images/bg.jpg"); + background-size: cover; +} + +h1 { + font-size: 50px; + margin: 40px; + text-shadow: 3px 2px rgb(153, 145, 175); + text-align: center; +} + +canvas { + display: block; + flex-shrink: 0; +} + +.flex-columns { + display: flex; + flex-direction: row; + justify-content: center; + margin: 0 auto; +} + +.flex-space { + flex-grow: 2; +} + +.flex-rows { + display: flex; + flex-direction: column; + margin: 5% 2%; + height: 400px; + width: 150px; +} + +#hold { + width: 120px; + justify-content: right; +} + +#stats { + display: flex; + flex-direction: row; + margin: 10% 0; + font-size: 1.2em; +} + +#stats-names { + font-family: 'Share Tech'; + text-align: left; +} + +#stats-values { + text-align: right; + font-family: 'Share Tech Mono'; + min-width: 90px; +} + +#matrix { + margin: 5% 2%; + border: 0.5px solid grey; +} + +#next { + width: 120px; + margin: 5% 2%; } \ No newline at end of file diff --git a/index.php b/index.php index 31e16e2..24f40b8 100644 --- a/index.php +++ b/index.php @@ -1,36 +1,36 @@ - - - - - Webtris - - - - -

WEBTRIS

-
- "GAUCHE", - "moveRight" => "DROITE", - "softDrop" => "CHUTE LENTE", - "hardDrop" => "CHUTE RAPIDE", - "rotateCW" => "ROTATION HORAIRE", - "rotateCCW" => "ROTATE INVERSE", - "hold" => "GARDE", - "pause" => "PAUSE", - ); - foreach($actionLabel as $action => $label) - { - echo "
$label
\n"; - echo " \n"; - } -?> -
-
- JOUER -
- + + + + + Webtris + + + + +

WEBTRIS

+
+ "GAUCHE", + "moveRight" => "DROITE", + "softDrop" => "CHUTE LENTE", + "hardDrop" => "CHUTE RAPIDE", + "rotateCW" => "ROTATION HORAIRE", + "rotateCCW" => "ROTATE INVERSE", + "hold" => "GARDE", + "pause" => "PAUSE", + ); + foreach($actionLabel as $action => $label) + { + echo "
$label
\n"; + echo " \n"; + } +?> +
+
+ JOUER +
+ \ No newline at end of file diff --git a/js/webtris.js b/js/webtris.js index 42f36fc..17aa703 100644 --- a/js/webtris.js +++ b/js/webtris.js @@ -1,783 +1,784 @@ -Array.prototype.add = function(other) { - return this.map((x, i) => x + other[i]) -} - -Array.prototype.mul = function(k) { - return this.map(x => k * x) -} - -Array.prototype.translate = function(vector) { - return this.map(pos => pos.add(vector)) -} - -Array.prototype.rotate = function(spin) { - return [-spin*this[1], spin*this[0]] -} - -Array.prototype.pick = function() { - return this.splice(Math.floor(Math.random()*this.length), 1)[0] -} - - -const MINO_SIZE = 20 -const NEXT_PIECES = 5 -const HOLD_ROWS = 6 -const HOLD_COLUMNS = 6 -const MATRIX_ROWS = 20 -const MATRIX_COLUMNS = 10 -const NEXT_ROWS = 20 -const NEXT_COLUMNS = 6 -const HELD_PIECE_POSITION = [2, 2] -const FALLING_PIECE_POSITION = [4, 0] -const NEXT_PIECES_POSITIONS = Array.from({length: NEXT_PIECES}, (v, k) => [2, k*4+2]) -const LOCK_DELAY = 500 -const FALL_DELAY = 1000 -const AUTOREPEAT_DELAY = 250 -const AUTOREPEAT_PERIOD = 10 -const ANIMATION_DELAY = 100 -const TEMP_TEXTS_DELAY = 700 -const MOVEMENT = { - LEFT: [-1, 0], - RIGHT: [ 1, 0], - DOWN: [ 0, 1] -} -const SPIN = { - CW: 1, - CCW: -1 -} -const T_SPIN = { - NONE: "", - MINI: "MINI\nT-SPIN", - T_SPIN: "T-SPIN" -} -const T_SLOT = { - A: 0, - B: 1, - C: 3, - D: 2 -} -const T_SLOT_POS = [[-1, -1], [1, -1], [1, 1], [-1, 1]] -const SCORES = [ - {linesClearedName: "", "": 0, "MINI\nT-SPIN": 1, "T-SPIN": 4}, - {linesClearedName: "SINGLE", "": 1, "MINI\nT-SPIN": 2, "T-SPIN": 8}, - {linesClearedName: "DOUBLE", "": 3, "T-SPIN": 12}, - {linesClearedName: "TRIPLE", "": 5, "T-SPIN": 16}, - {linesClearedName: "TETRIS", "": 8}, -] -const REPEATABLE_ACTIONS = [moveLeft, moveRight, softDrop] -const STATE = { - PLAYING: "PLAYING", - PAUSED: "PAUSE", - GAME_OVER: "GAME OVER" -} -const actionsDefaultKeys = { - moveLeft: "ArrowLeft", - moveRight: "ArrowRight", - softDrop: "ArrowDown", - hardDrop: " ", - rotateCW: "ArrowUp", - rotateCCW: "z", - hold: "c", - pause: "Escape", -} -var actions = {} - - -class Scheduler { - constructor() { - this.intervalTasks = new Map() - this.timeoutTasks = new Map() - } - - setInterval(func, delay, ...args) { - this.intervalTasks.set(func, window.setInterval(func, delay, ...args)) - } - - setTimeout(func, delay, ...args) { - this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args)) - } - - clearInterval(func) { - if (this.intervalTasks.has(func)) { - window.clearInterval(this.intervalTasks.get(func)) - this.intervalTasks.delete(func) - } - } - - clearTimeout(func) { - if (this.timeoutTasks.has(func)) { - window.clearTimeout(this.timeoutTasks.get(func)) - this.timeoutTasks.delete(func) - } - } -} - - -shapes = [] -class Tetromino { - constructor(position=null, shape=null) { - this.pos = position - this.orientation = 0 - this.rotatedLast = false - this.rotationPoint5Used = false - this.holdEnabled = true - this.locked = false - this.srs = {} - this.srs[SPIN.CW] = [ - [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], - [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]], - [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]], - [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], - ] - this.srs[SPIN.CCW] = [ - [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]], - [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]], - [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], - [[0, 0], [-1, 0], [-1, 1], [0, 2], [-1, -2]], - ] - if (shape) - this.shape = shape - else { - if (!shapes.length) - shapes = ['I', 'J', 'L', 'O', 'S', 'T', 'Z'] - this.shape = shapes.pick() - } - switch(this.shape) { - case 'I': - this.color = "cyan" - this.lightColor = "rgb(234, 250, 250)" - this.ghostColor = "rgba(234, 250, 250, 0.5)" - this.minoesPos = [[-1, 0], [0, 0], [1, 0], [2, 0]] - this.srs[SPIN.CW] = [ - [[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]], - [[ 0, 1], [-1, 1], [ 2, 1], [-1, -1], [ 2, 2]], - [[-1, 0], [ 1, 0], [-2, 0], [ 1, -1], [-2, 2]], - [[ 0, 1], [ 1, -1], [-2, -1], [ 1, 1], [-2, -2]], - ] - this.srs[SPIN.CCW] = [ - [[ 0, 1], [-1, 1], [ 2, 1], [-1, -1], [ 2, 2]], - [[-1, 0], [ 1, 0], [-2, 0], [ 1, -1], [-2, 2]], - [[ 0, -1], [ 1, -1], [-2, -1], [ 1, 1], [-2, -2]], - [[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]], - ] - break - case 'J': - this.color = "blue" - this.lightColor = "rgb(230, 240, 255)" - this.ghostColor = "rgba(230, 240, 255, 0.5)" - this.minoesPos = [[-1, -1], [-1, 0], [0, 0], [1, 0]] - break - case 'L': - this.color = "orange" - this.lightColor = "rgb(255, 224, 204)" - this.ghostColor = "rgba(255, 224, 204, 0.5)" - this.minoesPos = [[-1, 0], [0, 0], [1, 0], [1, -1]] - break - case 'O': - this.color = "yellow" - this.lightColor = "rgb(255, 255, 230)" - this.ghostColor = "rgba(255, 255, 230, 0.5)" - this.minoesPos = [[0, 0], [1, 0], [0, -1], [1, -1]] - this.srs[SPIN.CW] = [[]] - this.srs[SPIN.CCW] = [[]] - break - case 'S': - this.color = "green" - this.lightColor = "rgb(236, 255, 230)" - this.ghostColor = "rgba(236, 255, 230, 0.5)" - this.minoesPos = [[-1, 0], [0, 0], [0, -1], [1, -1]] - break - case 'T': - this.color = "magenta" - this.lightColor= "rgb(242, 230, 255)" - this.ghostColor = "rgba(242, 230, 255, 0.5)" - this.minoesPos = [[-1, 0], [0, 0], [1, 0], [0, -1]] - break - case 'Z': - this.color = "red" - this.lightColor = "rgb(255, 230, 230)" - this.ghostColor = "rgba(255, 230, 230, 0.5)" - this.minoesPos = [[-1, -1], [0, -1], [0, 0], [1, 0]] - break - } - } - - get minoesAbsPos() { - return this.minoesPos.translate(this.pos) - } - - draw(context, ghost_pos=[0, 0]) { - const color = this.locked ? this.lightColor : this.color - if (ghost_pos[1]) { - context.save() - context.shadowColor = this.ghostColor - context.shadowOffsetX = 0 - context.shadowOffsetY = (ghost_pos[1]-this.pos[1]) * MINO_SIZE - context.shadowBlur = 3 - this.minoesAbsPos.forEach(pos => drawMino(context, pos, color)) - context.restore() - } - this.minoesAbsPos.forEach(pos => drawMino(context, pos, this.lightColor, color, ghost_pos)) - } -} - - -function drawMino(context, pos, color1, color2=null, spotlight=[0, 0]) { - if (color2) { - var center = pos.add([0.5, 0.5]) - spotlight = spotlight.add([0.5, 0.5]) - var glint = spotlight.mul(0.1).add(center.mul(0.9)).mul(MINO_SIZE) - const gradient = context.createRadialGradient( - ...glint, 2, ...glint.add([6, 4]), 2*MINO_SIZE - ) - gradient.addColorStop(0, color1) - gradient.addColorStop(1, color2) - context.fillStyle = gradient - } else - context.fillStyle = color1 - var topLeft = pos.mul(MINO_SIZE) - context.fillRect(...topLeft, MINO_SIZE, MINO_SIZE) - context.lineWidth = 0.5 - context.strokeStyle = "white" - context.strokeRect(...topLeft, MINO_SIZE, MINO_SIZE) -} - - -class HoldQueue { - constructor() { - this.context = document.getElementById("hold").getContext("2d") - this.piece = null - this.width = HOLD_COLUMNS*MINO_SIZE - this.height = HOLD_ROWS*MINO_SIZE - } - - draw() { - this.context.clearRect(0, 0, this.width, this.height) - if (state != STATE.PAUSED) { - if (this.piece) - this.piece.draw(this.context) - } - } -} - - -timeFormat = new Intl.DateTimeFormat("fr-FR", { - minute: "2-digit", second: "2-digit", hourCycle: "h24", timeZone: "UTC" -}).format - - -class Stats { - constructor () { - this.div = document.getElementById("stats-values") - this._score = 0 - this.highScore = localStorage.getItem('highScore') || 0 - this.goal = 0 - this.linesCleared = 0 - this.startTime = Date.now() - this.pauseTime = 0 - this.combo = -1 - this.lockDelay = LOCK_DELAY - this.fallDelay = FALL_DELAY - } - - get score() { - return this._score - } - - set score(score) { - this._score = score - if (score > this.highScore) - this.highScore = score - } - - newLevel(level=null) { - if (level) - this.level = level - else - this.level++ - printTempTexts(["LEVEL", this.level]) - this.goal += 5 * this.level - if (this.level <= 20) - this.fallDelay = 1000 * Math.pow(0.8 - ((this.level - 1) * 0.007), this.level - 1) - if (this.level > 15) - this.lockDelay = 500 * Math.pow(0.9, this.level - 15) - } - - locksDown(tSpin, linesCleared) { - var patternName = [] - var patternScore = 0 - var combo_score = 0 - - if (tSpin) - patternName.push(tSpin) - if (linesCleared) { - patternName.push(SCORES[linesCleared].linesClearedName) - this.combo++ - } else - this.combo = -1 - - if (linesCleared || tSpin) { - this.linesCleared += linesCleared - patternScore = SCORES[linesCleared][tSpin] - this.goal -= patternScore - patternScore *= 100 * this.level - patternName = patternName.join("\n") - } - if (this.combo >= 1) - combo_score = (linesCleared == 1 ? 20 : 50) * this.combo * this.level - - this.score += patternScore + combo_score - - if (patternScore) - printTempTexts([patternName, patternScore]) - if (combo_score) - printTempTexts([`COMBO x${this.combo}`, combo_score]) - } - - print() { - this.div.innerHTML = `${this.score}
- ${this.highScore}
- ${timeFormat(Date.now() - this.startTime)}
- ${this.level}
- ${this.goal}
- ${this.linesCleared}` - } -} - - -class Matrix { - constructor() { - this.context = document.getElementById("matrix").getContext("2d") - this.context.textAlign = "center" - this.context.textBaseline = "center" - this.context.font = "3vw 'Share Tech', sans-serif" - this.cells = Array.from(Array(MATRIX_ROWS+3), row => Array(MATRIX_COLUMNS)) - this.width = MATRIX_COLUMNS*MINO_SIZE - this.height = MATRIX_ROWS*MINO_SIZE - this.centerX = this.width / 2 - this.centerY = this.height / 2 - this.piece = null - this.trail = { - minoesPos: [], - height: 0, - gradient: null - } - this.linesCleared = [] - } - - cellIsOccupied(x, y) { - return 0 <= x && x < MATRIX_COLUMNS && y < MATRIX_ROWS ? this.cells[y+3][x] : true - } - - spaceToMove(minoesAbsPos) { - return !minoesAbsPos.some(pos => this.cellIsOccupied(...pos)) - } - - draw() { - this.context.clearRect(0, 0, this.width, this.height) - - // grid - this.context.strokeStyle = "rgba(128, 128, 128, 128)" - this.context.lineWidth = 0.5 - this.context.beginPath() - for (var x = 0; x <= this.width; x += MINO_SIZE) { - this.context.moveTo(x, 0); - this.context.lineTo(x, this.height); - } - for (var y = 0; y <= this.height; y += MINO_SIZE) { - this.context.moveTo(0, y); - this.context.lineTo(this.width, y); - } - this.context.stroke() - - if (state != STATE.PAUSED) { - // ghost position - for (var ghost_pos = Array.from(this.piece.pos); this.spaceToMove(this.piece.minoesPos.translate(ghost_pos)); ghost_pos[1]++) {} - ghost_pos[1]-- - - // locked minoes - this.cells.slice(3).forEach((row, y) => row.forEach((colors, x) => { - if (colors) drawMino(this.context, [x, y], ...colors, ghost_pos) - })) - - // trail - if (this.trail.height) { - this.context.fillStyle = this.trail.gradient - this.trail.minoesPos.forEach(topLeft => { - this.context.fillRect(...topLeft, MINO_SIZE, this.trail.height) - }) - } - - // falling piece - if (this.piece) - this.piece.draw(this.context, ghost_pos) - - // Lines cleared - if (this.linesCleared.length) { - this.context.save() - this.context.shadowColor = "white" - this.context.shadowOffsetX = 0 - this.context.shadowOffsetY = 0 - this.context.shadowBlur = 5 - this.context.fillStyle = "white" - this.linesCleared.forEach(y => this.context.fillRect(0, y, this.width, MINO_SIZE)) - this.context.restore() - } - } - - // text - var texts = [] - switch(state) { - case STATE.PLAYING: - if (tempTexts.length) - texts = tempTexts[0] - break - case STATE.PAUSED: - texts = ["PAUSED"] - break - case STATE.GAME_OVER: - texts = ["GAME", "OVER"] - } - if (texts.length) { - this.context.save() - this.context.shadowColor = "black" - this.context.shadowOffsetX = 1 - this.context.shadowOffsetY = 1 - this.context.shadowBlur = 2 - this.context.fillStyle = "white" - if (texts.length == 1) - this.context.fillText(texts[0], this.centerX, this.centerY) - else { - this.context.fillText(texts[0], this.centerX, this.centerY - 20) - this.context.fillText(texts[1], this.centerX, this.centerY + 20) - } - this.context.restore() - } - } -} - - -class NextQueue { - constructor() { - this.context = document.getElementById("next").getContext("2d") - this.pieces = Array.from({length: NEXT_PIECES}, (v, k) => new Tetromino(NEXT_PIECES_POSITIONS[k])) - this.width = NEXT_COLUMNS*MINO_SIZE - this.height = NEXT_ROWS*MINO_SIZE - } - - draw() { - this.context.clearRect(0, 0, this.width, this.height) - if (state != STATE.PAUSED) { - this.pieces.forEach(piece => piece.draw(this.context)) - } - } -} - - -function newLevel(startLevel) { - stats.newLevel(startLevel) - generationPhase() -} - -function generationPhase(held_piece=null) { - if (!held_piece) { - matrix.piece = nextQueue.pieces.shift() - nextQueue.pieces.push(new Tetromino()) - nextQueue.pieces.forEach((piece, i) => piece.pos = NEXT_PIECES_POSITIONS[i]) - } - matrix.piece.pos = FALLING_PIECE_POSITION - if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos))) - fallingPhase() - else - gameOver() -} - -function fallingPhase() { - scheduler.clearTimeout(lockPhase) - scheduler.clearTimeout(locksDown) - matrix.piece.locked = false - scheduler.setTimeout(lockPhase, stats.fallDelay) -} - -function lockPhase() { - if (!move(MOVEMENT.DOWN)) { - matrix.piece.locked = true - if (!scheduler.timeoutTasks.has(locksDown)) - scheduler.setTimeout(locksDown, stats.lockDelay) - } - requestAnimationFrame(draw) -} - -function move(movement, testMinoesPos=matrix.piece.minoesPos) { - const testPos = matrix.piece.pos.add(movement) - if (matrix.spaceToMove(testMinoesPos.translate(testPos))) { - matrix.piece.pos = testPos - matrix.piece.minoesPos = testMinoesPos - if (movement != MOVEMENT.DOWN) - matrix.piece.rotatedLast = false - if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos.add(MOVEMENT.DOWN)))) - fallingPhase() - else { - matrix.piece.locked = true - scheduler.clearTimeout(locksDown) - scheduler.setTimeout(locksDown, stats.lockDelay) - } - return true - } else { - return false - } -} - -function rotate(spin) { - const test_minoes_pos = matrix.piece.minoesPos.map(pos => pos.rotate(spin)) - rotationPoint = 1 - for (const movement of matrix.piece.srs[spin][matrix.piece.orientation]) { - if (move(movement, test_minoes_pos)) { - matrix.piece.orientation = (matrix.piece.orientation + spin + 4) % 4 - matrix.piece.rotatedLast = true - if (rotationPoint == 5) - matrix.piece.rotationPoint5Used = true - return true - } - rotationPoint++ - } - return false -} - -function locksDown(){ - scheduler.clearInterval(move) - if (matrix.piece.minoesAbsPos.every(pos => pos.y < 0)) - game_over() - else { - matrix.piece.minoesAbsPos.forEach(pos => matrix.cells[pos[1]+3][pos[0]] = [matrix.piece.lightColor, matrix.piece.color]) - - // T-Spin detection - var tSpin = T_SPIN.NONE - if (matrix.piece.rotatedLast && matrix.piece.shape == "T") { - const tSlots = T_SLOT_POS.translate(matrix.piece.pos).map(pos => matrix.cellIsOccupied(...pos)), - a = tSlots[(matrix.piece.orientation+T_SLOT.A)%4], - b = tSlots[(matrix.piece.orientation+T_SLOT.B)%4], - c = tSlots[(matrix.piece.orientation+T_SLOT.C)%4], - d = tSlots[(matrix.piece.orientation+T_SLOT.D)%4] - if (a && b && (c || d)) - tSpin = T_SPIN.T_SPIN - else if (c && d && (a || b)) - tSpin = matrix.piece.rotationPoint5Used ? T_SPIN.T_SPIN : T_SPIN.MINI - } - - // Complete lines - matrix.linesCleared = [] - matrix.cells.forEach((row, y) => { - if (row.filter(mino => mino.length).length == MATRIX_COLUMNS) { - matrix.cells.splice(y, 1) - matrix.cells.unshift(Array(MATRIX_COLUMNS)) - matrix.linesCleared.push((y-3) * MINO_SIZE) - } - }) - - stats.locksDown(tSpin, matrix.linesCleared.length) - requestAnimationFrame(draw) - scheduler.setTimeout(clearLinesCleared, ANIMATION_DELAY) - - if (stats.goal <= 0) - newLevel() - else - generationPhase() - } -} - -function clearLinesCleared() { - matrix.linesCleared = [] - requestAnimationFrame(draw) -} - -function gameOver() { - state = STATE.GAME_OVER - scheduler.clearTimeout(lockPhase) - scheduler.clearTimeout(locksDown) - scheduler.clearInterval(clock) - requestAnimationFrame(draw) - - if (stats.score == stats.highScore) { - alert("Bravo ! Vous avez battu votre précédent record.") - localStorage.setItem('highScore', stats.highScore) - } -} - -function autorepeat() { - if (actionsToRepeat.length) { - actionsToRepeat[0]() - requestAnimationFrame(draw) - if (scheduler.timeoutTasks.has(autorepeat)) { - scheduler.clearTimeout(autorepeat) - scheduler.setInterval(autorepeat, AUTOREPEAT_PERIOD) - } - } else { - scheduler.clearTimeout(autorepeat) - scheduler.clearInterval(autorepeat) - } -} - -function keyDownHandler(e) { - if (!pressedKeys.has(e.key)) { - pressedKeys.add(e.key) - if (e.key in actions[state]) { - action = actions[state][e.key] - action() - requestAnimationFrame(draw) - if (REPEATABLE_ACTIONS.includes(action)) { - actionsToRepeat.unshift(action) - scheduler.clearTimeout(autorepeat) - scheduler.clearInterval(autorepeat) - if (action == softDrop) - scheduler.setInterval(autorepeat, stats.fallDelay / 20) - else - scheduler.setTimeout(autorepeat, AUTOREPEAT_DELAY) - } - } - } -} - -function keyUpHandler(e) { - pressedKeys.delete(e.key) - if (e.key in actions[state]) { - action = actions[state][e.key] - if (actionsToRepeat.includes(action)) { - actionsToRepeat.splice(actionsToRepeat.indexOf(action), 1) - if (!actionsToRepeat.length) { - scheduler.clearTimeout(autorepeat) - scheduler.clearInterval(autorepeat) - } - } - } -} - -function moveLeft() { - move(MOVEMENT.LEFT); -} - -function moveRight() { - move(MOVEMENT.RIGHT) -} - -function softDrop() { - if (move(MOVEMENT.DOWN)) - stats.score++ -} - -function hardDrop() { - scheduler.clearTimeout(lockPhase) - scheduler.clearTimeout(locksDown) - matrix.trail.minoesPos = Array.from(matrix.piece.minoesAbsPos).map(pos => pos.mul(MINO_SIZE)) - for (matrix.trail.height=0; move(MOVEMENT.DOWN); matrix.trail.height += MINO_SIZE) { - stats.score += 2 - } - locksDown() - matrix.trail.gradient = matrix.context.createLinearGradient(0, 0, 0, matrix.trail.height) - matrix.trail.gradient.addColorStop(0,"rgba(255, 255, 255, 0)") - matrix.trail.gradient.addColorStop(1, matrix.piece.ghostColor) - scheduler.setTimeout(clearTrail, ANIMATION_DELAY) -} - -function clearTrail() { - matrix.trail.height = 0 - requestAnimationFrame(draw) -} - -function rotateCW() { - rotate(SPIN.CW) -} - -function rotateCCW() { - rotate(SPIN.CCW) -} - -function hold() { - if (this.matrix.piece.holdEnabled) { - scheduler.clearInterval(move) - scheduler.clearInterval(locksDown) - var shape = this.matrix.piece.shape - this.matrix.piece = this.holdQueue.piece - this.holdQueue.piece = new Tetromino(HELD_PIECE_POSITION, shape) - this.holdQueue.piece.holdEnabled = false - this.generationPhase(this.matrix.piece) - } -} - -function pause() { - state = STATE.PAUSED - stats.pauseTime = Date.now() - stats.startTime - scheduler.clearTimeout(lockPhase) - scheduler.clearTimeout(locksDown) - scheduler.clearTimeout(autorepeat) - scheduler.clearInterval(clock) -} - -function resume() { - state = STATE.PLAYING - stats.startTime = Date.now() - stats.pauseTime - scheduler.setTimeout(lockPhase, stats.fallDelay) - if (matrix.piece.locked) - scheduler.setTimeout(locksDown, stats.lockDelay) - requestAnimationFrame(draw) - scheduler.setInterval(clock, 1000) -} - -function printTempTexts(texts) { - tempTexts.push(texts) - if (!scheduler.intervalTasks.has(delTempTexts)) - scheduler.setInterval(delTempTexts, TEMP_TEXTS_DELAY) -} - -function delTempTexts(self) { - if (tempTexts.length) - tempTexts.shift() - else - scheduler.clearInterval(delTempTexts) -} - -function clock() { - stats.print() -} - -function draw() { - holdQueue.draw() - stats.print() - matrix.draw() - nextQueue.draw() -} - -function getKey(action) { - return localStorage.getItem(action) || actionsDefaultKeys[action] -} - -window.onload = function() { - tempTexts = [] - - holdQueue = new HoldQueue() - stats = new Stats() - matrix = new Matrix() - nextQueue = new NextQueue() - - actions[STATE.PLAYING] = {} - actions[STATE.PLAYING][getKey("moveLeft")] = moveLeft - actions[STATE.PLAYING][getKey("moveRight")] = moveRight - actions[STATE.PLAYING][getKey("softDrop")] = softDrop - actions[STATE.PLAYING][getKey("hardDrop")] = hardDrop - actions[STATE.PLAYING][getKey("rotateCW")] = rotateCW - actions[STATE.PLAYING][getKey("rotateCCW")] = rotateCCW - actions[STATE.PLAYING][getKey("hold")] = hold - actions[STATE.PLAYING][getKey("pause")] = pause - actions[STATE.PAUSED] = {} - actions[STATE.PAUSED][getKey("pause")] = resume - actions[STATE.GAME_OVER] = {} - pressedKeys = new Set() - actionsToRepeat = [] - addEventListener("keydown", keyDownHandler, false) - addEventListener("keyup", keyUpHandler, false) - - state = STATE.PLAYING - scheduler = new Scheduler() - scheduler.setInterval(clock, 1000) - this.newLevel(1) +Array.prototype.add = function(other) { + return this.map((x, i) => x + other[i]) +} + +Array.prototype.mul = function(k) { + return this.map(x => k * x) +} + +Array.prototype.translate = function(vector) { + return this.map(pos => pos.add(vector)) +} + +Array.prototype.rotate = function(spin) { + return [-spin*this[1], spin*this[0]] +} + +Array.prototype.pick = function() { + return this.splice(Math.floor(Math.random()*this.length), 1)[0] +} + + +const MINO_SIZE = 20 +const NEXT_PIECES = 5 +const HOLD_ROWS = 6 +const HOLD_COLUMNS = 6 +const MATRIX_ROWS = 20 +const MATRIX_COLUMNS = 10 +const NEXT_ROWS = 20 +const NEXT_COLUMNS = 6 +const HELD_PIECE_POSITION = [2, 2] +const FALLING_PIECE_POSITION = [4, 0] +const NEXT_PIECES_POSITIONS = Array.from({length: NEXT_PIECES}, (v, k) => [2, k*4+2]) +const LOCK_DELAY = 500 +const FALL_DELAY = 1000 +const AUTOREPEAT_DELAY = 250 +const AUTOREPEAT_PERIOD = 10 +const ANIMATION_DELAY = 100 +const TEMP_TEXTS_DELAY = 700 +const MOVEMENT = { + LEFT: [-1, 0], + RIGHT: [ 1, 0], + DOWN: [ 0, 1] +} +const SPIN = { + CW: 1, + CCW: -1 +} +const T_SPIN = { + NONE: "", + MINI: "MINI\nT-SPIN", + T_SPIN: "T-SPIN" +} +const T_SLOT = { + A: 0, + B: 1, + C: 3, + D: 2 +} +const T_SLOT_POS = [[-1, -1], [1, -1], [1, 1], [-1, 1]] +const SCORES = [ + {linesClearedName: "", "": 0, "MINI\nT-SPIN": 1, "T-SPIN": 4}, + {linesClearedName: "SINGLE", "": 1, "MINI\nT-SPIN": 2, "T-SPIN": 8}, + {linesClearedName: "DOUBLE", "": 3, "T-SPIN": 12}, + {linesClearedName: "TRIPLE", "": 5, "T-SPIN": 16}, + {linesClearedName: "TETRIS", "": 8}, +] +const REPEATABLE_ACTIONS = [moveLeft, moveRight, softDrop] +const STATE = { + PLAYING: "PLAYING", + PAUSED: "PAUSE", + GAME_OVER: "GAME OVER" +} +const actionsDefaultKeys = { + moveLeft: "ArrowLeft", + moveRight: "ArrowRight", + softDrop: "ArrowDown", + hardDrop: " ", + rotateCW: "ArrowUp", + rotateCCW: "z", + hold: "c", + pause: "Escape", +} +var actions = {} + + +class Scheduler { + constructor() { + this.intervalTasks = new Map() + this.timeoutTasks = new Map() + } + + setInterval(func, delay, ...args) { + this.intervalTasks.set(func, window.setInterval(func, delay, ...args)) + } + + setTimeout(func, delay, ...args) { + this.timeoutTasks.set(func, window.setTimeout(func, delay, ...args)) + } + + clearInterval(func) { + if (this.intervalTasks.has(func)) { + window.clearInterval(this.intervalTasks.get(func)) + this.intervalTasks.delete(func) + } + } + + clearTimeout(func) { + if (this.timeoutTasks.has(func)) { + window.clearTimeout(this.timeoutTasks.get(func)) + this.timeoutTasks.delete(func) + } + } +} + + +shapes = [] +class Tetromino { + constructor(position=null, shape=null) { + this.pos = position + this.orientation = 0 + this.rotatedLast = false + this.rotationPoint5Used = false + this.holdEnabled = true + this.locked = false + this.srs = {} + this.srs[SPIN.CW] = [ + [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], + [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]], + [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]], + [[0, 0], [-1, 0], [-1, 1], [0, -2], [-1, -2]], + ] + this.srs[SPIN.CCW] = [ + [[0, 0], [ 1, 0], [ 1, -1], [0, 2], [ 1, 2]], + [[0, 0], [ 1, 0], [ 1, 1], [0, -2], [ 1, -2]], + [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], + [[0, 0], [-1, 0], [-1, 1], [0, 2], [-1, -2]], + ] + if (shape) + this.shape = shape + else { + if (!shapes.length) + shapes = ['I', 'J', 'L', 'O', 'S', 'T', 'Z'] + this.shape = shapes.pick() + } + switch(this.shape) { + case 'I': + this.color = "cyan" + this.lightColor = "rgb(234, 250, 250)" + this.ghostColor = "rgba(234, 250, 250, 0.5)" + this.minoesPos = [[-1, 0], [0, 0], [1, 0], [2, 0]] + this.srs[SPIN.CW] = [ + [[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]], + [[ 0, 1], [-1, 1], [ 2, 1], [-1, -1], [ 2, 2]], + [[-1, 0], [ 1, 0], [-2, 0], [ 1, -1], [-2, 2]], + [[ 0, 1], [ 1, -1], [-2, -1], [ 1, 1], [-2, -2]], + ] + this.srs[SPIN.CCW] = [ + [[ 0, 1], [-1, 1], [ 2, 1], [-1, -1], [ 2, 2]], + [[-1, 0], [ 1, 0], [-2, 0], [ 1, -1], [-2, 2]], + [[ 0, -1], [ 1, -1], [-2, -1], [ 1, 1], [-2, -2]], + [[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]], + ] + break + case 'J': + this.color = "blue" + this.lightColor = "rgb(230, 240, 255)" + this.ghostColor = "rgba(230, 240, 255, 0.5)" + this.minoesPos = [[-1, -1], [-1, 0], [0, 0], [1, 0]] + break + case 'L': + this.color = "orange" + this.lightColor = "rgb(255, 224, 204)" + this.ghostColor = "rgba(255, 224, 204, 0.5)" + this.minoesPos = [[-1, 0], [0, 0], [1, 0], [1, -1]] + break + case 'O': + this.color = "yellow" + this.lightColor = "rgb(255, 255, 230)" + this.ghostColor = "rgba(255, 255, 230, 0.5)" + this.minoesPos = [[0, 0], [1, 0], [0, -1], [1, -1]] + this.srs[SPIN.CW] = [[]] + this.srs[SPIN.CCW] = [[]] + break + case 'S': + this.color = "green" + this.lightColor = "rgb(236, 255, 230)" + this.ghostColor = "rgba(236, 255, 230, 0.5)" + this.minoesPos = [[-1, 0], [0, 0], [0, -1], [1, -1]] + break + case 'T': + this.color = "magenta" + this.lightColor= "rgb(242, 230, 255)" + this.ghostColor = "rgba(242, 230, 255, 0.5)" + this.minoesPos = [[-1, 0], [0, 0], [1, 0], [0, -1]] + break + case 'Z': + this.color = "red" + this.lightColor = "rgb(255, 230, 230)" + this.ghostColor = "rgba(255, 230, 230, 0.5)" + this.minoesPos = [[-1, -1], [0, -1], [0, 0], [1, 0]] + break + } + } + + get minoesAbsPos() { + return this.minoesPos.translate(this.pos) + } + + draw(context, ghost_pos=[0, 0]) { + const color = this.locked ? this.lightColor : this.color + if (ghost_pos[1]) { + context.save() + context.shadowColor = this.ghostColor + context.shadowOffsetX = 0 + context.shadowOffsetY = (ghost_pos[1]-this.pos[1]) * MINO_SIZE + context.shadowBlur = 3 + this.minoesAbsPos.forEach(pos => drawMino(context, pos, color)) + context.restore() + } + this.minoesAbsPos.forEach(pos => drawMino(context, pos, this.lightColor, color, ghost_pos)) + } +} + + +function drawMino(context, pos, color1, color2=null, spotlight=[0, 0]) { + if (color2) { + var center = pos.add([0.5, 0.5]) + spotlight = spotlight.add([0.5, 0.5]) + var glint = spotlight.mul(0.1).add(center.mul(0.9)).mul(MINO_SIZE) + const gradient = context.createRadialGradient( + ...glint, 2, ...glint.add([6, 4]), 2*MINO_SIZE + ) + gradient.addColorStop(0, color1) + gradient.addColorStop(1, color2) + context.fillStyle = gradient + } else + context.fillStyle = color1 + var topLeft = pos.mul(MINO_SIZE) + context.fillRect(...topLeft, MINO_SIZE, MINO_SIZE) + context.lineWidth = 0.5 + context.strokeStyle = "white" + context.strokeRect(...topLeft, MINO_SIZE, MINO_SIZE) +} + + +class HoldQueue { + constructor() { + this.context = document.getElementById("hold").getContext("2d") + this.piece = null + this.width = HOLD_COLUMNS*MINO_SIZE + this.height = HOLD_ROWS*MINO_SIZE + } + + draw() { + this.context.clearRect(0, 0, this.width, this.height) + if (state != STATE.PAUSED) { + if (this.piece) + this.piece.draw(this.context) + } + } +} + + +timeFormat = new Intl.DateTimeFormat("fr-FR", { + minute: "2-digit", second: "2-digit", hourCycle: "h24", timeZone: "UTC" +}).format + + +class Stats { + constructor () { + this.div = document.getElementById("stats-values") + this._score = 0 + this.highScore = localStorage.getItem('highScore') || 0 + this.goal = 0 + this.linesCleared = 0 + this.startTime = Date.now() + this.pauseTime = 0 + this.combo = -1 + this.lockDelay = LOCK_DELAY + this.fallDelay = FALL_DELAY + } + + get score() { + return this._score + } + + set score(score) { + this._score = score + if (score > this.highScore) + this.highScore = score + } + + newLevel(level=null) { + if (level) + this.level = level + else + this.level++ + printTempTexts(["LEVEL", this.level]) + this.goal += 5 * this.level + if (this.level <= 20) + this.fallDelay = 1000 * Math.pow(0.8 - ((this.level - 1) * 0.007), this.level - 1) + if (this.level > 15) + this.lockDelay = 500 * Math.pow(0.9, this.level - 15) + } + + locksDown(tSpin, linesCleared) { + var patternName = [] + var patternScore = 0 + var combo_score = 0 + + if (tSpin) + patternName.push(tSpin) + if (linesCleared) { + patternName.push(SCORES[linesCleared].linesClearedName) + this.combo++ + } else + this.combo = -1 + + if (linesCleared || tSpin) { + this.linesCleared += linesCleared + patternScore = SCORES[linesCleared][tSpin] + this.goal -= patternScore + patternScore *= 100 * this.level + patternName = patternName.join("\n") + } + if (this.combo >= 1) + combo_score = (linesCleared == 1 ? 20 : 50) * this.combo * this.level + + this.score += patternScore + combo_score + + if (patternScore) + printTempTexts([patternName, patternScore]) + if (combo_score) + printTempTexts([`COMBO x${this.combo}`, combo_score]) + } + + print() { + this.div.innerHTML = `${this.score}
+ ${this.highScore}
+ ${timeFormat(Date.now() - this.startTime)}
+
+ ${this.level}
+ ${this.goal}
+ ${this.linesCleared}` + } +} + + +class Matrix { + constructor() { + this.context = document.getElementById("matrix").getContext("2d") + this.context.textAlign = "center" + this.context.textBaseline = "center" + this.context.font = "27px 'Share Tech', sans-serif" + this.cells = Array.from(Array(MATRIX_ROWS+3), row => Array(MATRIX_COLUMNS)) + this.width = MATRIX_COLUMNS*MINO_SIZE + this.height = MATRIX_ROWS*MINO_SIZE + this.centerX = this.width / 2 + this.centerY = this.height / 2 + this.piece = null + this.trail = { + minoesPos: [], + height: 0, + gradient: null + } + this.linesCleared = [] + } + + cellIsOccupied(x, y) { + return 0 <= x && x < MATRIX_COLUMNS && y < MATRIX_ROWS ? this.cells[y+3][x] : true + } + + spaceToMove(minoesAbsPos) { + return !minoesAbsPos.some(pos => this.cellIsOccupied(...pos)) + } + + draw() { + this.context.clearRect(0, 0, this.width, this.height) + + // grid + this.context.strokeStyle = "rgba(128, 128, 128, 128)" + this.context.lineWidth = 0.5 + this.context.beginPath() + for (var x = 0; x <= this.width; x += MINO_SIZE) { + this.context.moveTo(x, 0); + this.context.lineTo(x, this.height); + } + for (var y = 0; y <= this.height; y += MINO_SIZE) { + this.context.moveTo(0, y); + this.context.lineTo(this.width, y); + } + this.context.stroke() + + if (state != STATE.PAUSED) { + // ghost position + for (var ghost_pos = Array.from(this.piece.pos); this.spaceToMove(this.piece.minoesPos.translate(ghost_pos)); ghost_pos[1]++) {} + ghost_pos[1]-- + + // locked minoes + this.cells.slice(3).forEach((row, y) => row.forEach((colors, x) => { + if (colors) drawMino(this.context, [x, y], ...colors, ghost_pos) + })) + + // trail + if (this.trail.height) { + this.context.fillStyle = this.trail.gradient + this.trail.minoesPos.forEach(topLeft => { + this.context.fillRect(...topLeft, MINO_SIZE, this.trail.height) + }) + } + + // falling piece + if (this.piece) + this.piece.draw(this.context, ghost_pos) + + // Lines cleared + if (this.linesCleared.length) { + this.context.save() + this.context.shadowColor = "white" + this.context.shadowOffsetX = 0 + this.context.shadowOffsetY = 0 + this.context.shadowBlur = 5 + this.context.fillStyle = "white" + this.linesCleared.forEach(y => this.context.fillRect(0, y, this.width, MINO_SIZE)) + this.context.restore() + } + } + + // text + var texts = [] + switch(state) { + case STATE.PLAYING: + if (tempTexts.length) + texts = tempTexts[0] + break + case STATE.PAUSED: + texts = ["PAUSED"] + break + case STATE.GAME_OVER: + texts = ["GAME", "OVER"] + } + if (texts.length) { + this.context.save() + this.context.shadowColor = "black" + this.context.shadowOffsetX = 1 + this.context.shadowOffsetY = 1 + this.context.shadowBlur = 2 + this.context.fillStyle = "white" + if (texts.length == 1) + this.context.fillText(texts[0], this.centerX, this.centerY) + else { + this.context.fillText(texts[0], this.centerX, this.centerY - 20) + this.context.fillText(texts[1], this.centerX, this.centerY + 20) + } + this.context.restore() + } + } +} + + +class NextQueue { + constructor() { + this.context = document.getElementById("next").getContext("2d") + this.pieces = Array.from({length: NEXT_PIECES}, (v, k) => new Tetromino(NEXT_PIECES_POSITIONS[k])) + this.width = NEXT_COLUMNS*MINO_SIZE + this.height = NEXT_ROWS*MINO_SIZE + } + + draw() { + this.context.clearRect(0, 0, this.width, this.height) + if (state != STATE.PAUSED) { + this.pieces.forEach(piece => piece.draw(this.context)) + } + } +} + + +function newLevel(startLevel) { + stats.newLevel(startLevel) + generationPhase() +} + +function generationPhase(held_piece=null) { + if (!held_piece) { + matrix.piece = nextQueue.pieces.shift() + nextQueue.pieces.push(new Tetromino()) + nextQueue.pieces.forEach((piece, i) => piece.pos = NEXT_PIECES_POSITIONS[i]) + } + matrix.piece.pos = FALLING_PIECE_POSITION + if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos))) + fallingPhase() + else + gameOver() +} + +function fallingPhase() { + scheduler.clearTimeout(lockPhase) + scheduler.clearTimeout(locksDown) + matrix.piece.locked = false + scheduler.setTimeout(lockPhase, stats.fallDelay) +} + +function lockPhase() { + if (!move(MOVEMENT.DOWN)) { + matrix.piece.locked = true + if (!scheduler.timeoutTasks.has(locksDown)) + scheduler.setTimeout(locksDown, stats.lockDelay) + } + requestAnimationFrame(draw) +} + +function move(movement, testMinoesPos=matrix.piece.minoesPos) { + const testPos = matrix.piece.pos.add(movement) + if (matrix.spaceToMove(testMinoesPos.translate(testPos))) { + matrix.piece.pos = testPos + matrix.piece.minoesPos = testMinoesPos + if (movement != MOVEMENT.DOWN) + matrix.piece.rotatedLast = false + if (matrix.spaceToMove(matrix.piece.minoesPos.translate(matrix.piece.pos.add(MOVEMENT.DOWN)))) + fallingPhase() + else { + matrix.piece.locked = true + scheduler.clearTimeout(locksDown) + scheduler.setTimeout(locksDown, stats.lockDelay) + } + return true + } else { + return false + } +} + +function rotate(spin) { + const test_minoes_pos = matrix.piece.minoesPos.map(pos => pos.rotate(spin)) + rotationPoint = 1 + for (const movement of matrix.piece.srs[spin][matrix.piece.orientation]) { + if (move(movement, test_minoes_pos)) { + matrix.piece.orientation = (matrix.piece.orientation + spin + 4) % 4 + matrix.piece.rotatedLast = true + if (rotationPoint == 5) + matrix.piece.rotationPoint5Used = true + return true + } + rotationPoint++ + } + return false +} + +function locksDown(){ + scheduler.clearInterval(move) + if (matrix.piece.minoesAbsPos.every(pos => pos.y < 0)) + game_over() + else { + matrix.piece.minoesAbsPos.forEach(pos => matrix.cells[pos[1]+3][pos[0]] = [matrix.piece.lightColor, matrix.piece.color]) + + // T-Spin detection + var tSpin = T_SPIN.NONE + if (matrix.piece.rotatedLast && matrix.piece.shape == "T") { + const tSlots = T_SLOT_POS.translate(matrix.piece.pos).map(pos => matrix.cellIsOccupied(...pos)), + a = tSlots[(matrix.piece.orientation+T_SLOT.A)%4], + b = tSlots[(matrix.piece.orientation+T_SLOT.B)%4], + c = tSlots[(matrix.piece.orientation+T_SLOT.C)%4], + d = tSlots[(matrix.piece.orientation+T_SLOT.D)%4] + if (a && b && (c || d)) + tSpin = T_SPIN.T_SPIN + else if (c && d && (a || b)) + tSpin = matrix.piece.rotationPoint5Used ? T_SPIN.T_SPIN : T_SPIN.MINI + } + + // Complete lines + matrix.linesCleared = [] + matrix.cells.forEach((row, y) => { + if (row.filter(mino => mino.length).length == MATRIX_COLUMNS) { + matrix.cells.splice(y, 1) + matrix.cells.unshift(Array(MATRIX_COLUMNS)) + matrix.linesCleared.push((y-3) * MINO_SIZE) + } + }) + + stats.locksDown(tSpin, matrix.linesCleared.length) + requestAnimationFrame(draw) + scheduler.setTimeout(clearLinesCleared, ANIMATION_DELAY) + + if (stats.goal <= 0) + newLevel() + else + generationPhase() + } +} + +function clearLinesCleared() { + matrix.linesCleared = [] + requestAnimationFrame(draw) +} + +function gameOver() { + state = STATE.GAME_OVER + scheduler.clearTimeout(lockPhase) + scheduler.clearTimeout(locksDown) + scheduler.clearInterval(clock) + requestAnimationFrame(draw) + + if (stats.score == stats.highScore) { + alert("Bravo ! Vous avez battu votre précédent record.") + localStorage.setItem('highScore', stats.highScore) + } +} + +function autorepeat() { + if (actionsToRepeat.length) { + actionsToRepeat[0]() + requestAnimationFrame(draw) + if (scheduler.timeoutTasks.has(autorepeat)) { + scheduler.clearTimeout(autorepeat) + scheduler.setInterval(autorepeat, AUTOREPEAT_PERIOD) + } + } else { + scheduler.clearTimeout(autorepeat) + scheduler.clearInterval(autorepeat) + } +} + +function keyDownHandler(e) { + if (!pressedKeys.has(e.key)) { + pressedKeys.add(e.key) + if (e.key in actions[state]) { + action = actions[state][e.key] + action() + requestAnimationFrame(draw) + if (REPEATABLE_ACTIONS.includes(action)) { + actionsToRepeat.unshift(action) + scheduler.clearTimeout(autorepeat) + scheduler.clearInterval(autorepeat) + if (action == softDrop) + scheduler.setInterval(autorepeat, stats.fallDelay / 20) + else + scheduler.setTimeout(autorepeat, AUTOREPEAT_DELAY) + } + } + } +} + +function keyUpHandler(e) { + pressedKeys.delete(e.key) + if (e.key in actions[state]) { + action = actions[state][e.key] + if (actionsToRepeat.includes(action)) { + actionsToRepeat.splice(actionsToRepeat.indexOf(action), 1) + if (!actionsToRepeat.length) { + scheduler.clearTimeout(autorepeat) + scheduler.clearInterval(autorepeat) + } + } + } +} + +function moveLeft() { + move(MOVEMENT.LEFT); +} + +function moveRight() { + move(MOVEMENT.RIGHT) +} + +function softDrop() { + if (move(MOVEMENT.DOWN)) + stats.score++ +} + +function hardDrop() { + scheduler.clearTimeout(lockPhase) + scheduler.clearTimeout(locksDown) + matrix.trail.minoesPos = Array.from(matrix.piece.minoesAbsPos).map(pos => pos.mul(MINO_SIZE)) + for (matrix.trail.height=0; move(MOVEMENT.DOWN); matrix.trail.height += MINO_SIZE) { + stats.score += 2 + } + locksDown() + matrix.trail.gradient = matrix.context.createLinearGradient(0, 0, 0, matrix.trail.height) + matrix.trail.gradient.addColorStop(0,"rgba(255, 255, 255, 0)") + matrix.trail.gradient.addColorStop(1, matrix.piece.ghostColor) + scheduler.setTimeout(clearTrail, ANIMATION_DELAY) +} + +function clearTrail() { + matrix.trail.height = 0 + requestAnimationFrame(draw) +} + +function rotateCW() { + rotate(SPIN.CW) +} + +function rotateCCW() { + rotate(SPIN.CCW) +} + +function hold() { + if (this.matrix.piece.holdEnabled) { + scheduler.clearInterval(move) + scheduler.clearInterval(locksDown) + var shape = this.matrix.piece.shape + this.matrix.piece = this.holdQueue.piece + this.holdQueue.piece = new Tetromino(HELD_PIECE_POSITION, shape) + this.holdQueue.piece.holdEnabled = false + this.generationPhase(this.matrix.piece) + } +} + +function pause() { + state = STATE.PAUSED + stats.pauseTime = Date.now() - stats.startTime + scheduler.clearTimeout(lockPhase) + scheduler.clearTimeout(locksDown) + scheduler.clearTimeout(autorepeat) + scheduler.clearInterval(clock) +} + +function resume() { + state = STATE.PLAYING + stats.startTime = Date.now() - stats.pauseTime + scheduler.setTimeout(lockPhase, stats.fallDelay) + if (matrix.piece.locked) + scheduler.setTimeout(locksDown, stats.lockDelay) + requestAnimationFrame(draw) + scheduler.setInterval(clock, 1000) +} + +function printTempTexts(texts) { + tempTexts.push(texts) + if (!scheduler.intervalTasks.has(delTempTexts)) + scheduler.setInterval(delTempTexts, TEMP_TEXTS_DELAY) +} + +function delTempTexts(self) { + if (tempTexts.length) + tempTexts.shift() + else + scheduler.clearInterval(delTempTexts) +} + +function clock() { + stats.print() +} + +function draw() { + holdQueue.draw() + stats.print() + matrix.draw() + nextQueue.draw() +} + +function getKey(action) { + return localStorage.getItem(action) || actionsDefaultKeys[action] +} + +window.onload = function() { + tempTexts = [] + + holdQueue = new HoldQueue() + stats = new Stats() + matrix = new Matrix() + nextQueue = new NextQueue() + + actions[STATE.PLAYING] = {} + actions[STATE.PLAYING][getKey("moveLeft")] = moveLeft + actions[STATE.PLAYING][getKey("moveRight")] = moveRight + actions[STATE.PLAYING][getKey("softDrop")] = softDrop + actions[STATE.PLAYING][getKey("hardDrop")] = hardDrop + actions[STATE.PLAYING][getKey("rotateCW")] = rotateCW + actions[STATE.PLAYING][getKey("rotateCCW")] = rotateCCW + actions[STATE.PLAYING][getKey("hold")] = hold + actions[STATE.PLAYING][getKey("pause")] = pause + actions[STATE.PAUSED] = {} + actions[STATE.PAUSED][getKey("pause")] = resume + actions[STATE.GAME_OVER] = {} + pressedKeys = new Set() + actionsToRepeat = [] + addEventListener("keydown", keyDownHandler, false) + addEventListener("keyup", keyUpHandler, false) + + state = STATE.PLAYING + scheduler = new Scheduler() + scheduler.setInterval(clock, 1000) + this.newLevel(1) } \ No newline at end of file diff --git a/webtris.html b/webtris.html index dad5545..35a741e 100644 --- a/webtris.html +++ b/webtris.html @@ -1,38 +1,46 @@ - - - - - Webtris - - - - - -

WEBTRIS

-
-
-
-
- -
-
-
-
- SCORE
- RECORD
- TEMPS
- NIVEAU
- OBJECTIF
- LIGNES
-
-
-
- -
- Votre navigateur ne supporte pas HTML5, veuillez le mettre à jour pour jouer. -
- -
-
- + + + + + Webtris + + + + + +

WEBTRIS

+
+
+ +
+
+
+
+
+ SCORE
+ RECORD
+ TEMPS
+
+ NIVEAU
+ OBJECTIF
+ LIGNES +
+
+ 0
+ 0
+ 00:00
+
+ 0
+ 0
+ 0 +
+
+ +
+ Votre navigateur ne supporte pas HTML5, veuillez le mettre à jour pour jouer. +
+ +
+
+ \ No newline at end of file