diff --git a/css/style.css b/css/style.css index ac3b4a1..07e80b5 100644 --- a/css/style.css +++ b/css/style.css @@ -1,3 +1,11 @@ +@font-face { + font-family: 'Share Tech Mono'; + font-style: normal; + font-weight: 400; + src: local('Share Tech Mono'), 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; @@ -12,7 +20,7 @@ body { canvas { display: block; flex-shrink: 0; - } +} .columns { display: flex; @@ -20,7 +28,7 @@ canvas { justify-content: center; } -.side { +.space { flex-grow: 2; } @@ -28,19 +36,27 @@ canvas { display: flex; flex-direction: column; margin: 5% 2%; + height: 400px; } .hold { max-width: 120px; - height: initial; } .stats { + display: flex; + flex-direction: row; margin: 10% 0; + font-family: 'Share Tech Mono'; + font-size: 1.2em; +} + +.stats-values { + text-align: right; + min-width: 80px; } .matrix { - max-width: 200px; margin: 5% 2%; border: 0.5px solid grey; } diff --git a/fonts/ShareTechMono.woff2 b/fonts/ShareTechMono.woff2 new file mode 100644 index 0000000..cee230d Binary files /dev/null and b/fonts/ShareTechMono.woff2 differ diff --git a/index.html b/index.html index e9fd5ae..1b61414 100644 --- a/index.html +++ b/index.html @@ -9,18 +9,29 @@
-
+
- -
- SCORE: 123123123123
- HIGH SCORE:
- TIME:
+
+
+
+
+
+
+ SCORE
+ HIGH
+ LEVEL
+ GOAL
+ LINES
+ TIME
+
+
+
+
Votre navigateur ne supporte pas HTML5, veuillez le mettre à jour pour jouer. -
+
\ No newline at end of file diff --git a/js/webtris.js b/js/webtris.js index a2f3161..dcd6b79 100644 --- a/js/webtris.js +++ b/js/webtris.js @@ -1,6 +1,9 @@ Array.prototype.add = function(other) { return this.map((x, i) => x + other[i]) } +Array.prototype.translate = function(vector) { + return this.map(pos => pos.add(vector)) +} Array.prototype.rotate = function(spin) { return [-spin*this[1], spin*this[0]] } @@ -11,8 +14,12 @@ Array.prototype.sample = function() { const MINO_SIZE = 20 const NEXT_PIECES = 5 -const MATRIX_LINES = 20 +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]) @@ -21,12 +28,12 @@ const FALL_DELAY = 1000 const AUTOREPEAT_DELAY = 300 const AUTOREPEAT_PERIOD = 10 const MOVEMENT = { - LEFT: [-1, 0], - RIGHT: [1, 0], - DOWN: [0, 1] + LEFT: [-1, 0], + RIGHT: [ 1, 0], + DOWN: [ 0, 1] } const SPIN = { - CW: 1, + CW: 1, CCW: -1 } const T_SPIN = { @@ -47,16 +54,47 @@ const SCORES = [ {LINES_CLEAR_NAME: "TRIPLE", NO_T_SPIN: 5, T_SPIN: 16}, {LINES_CLEAR_NAME: "TETRIS", NO_T_SPIN: 8}, ] +const REPEATABLE_ACTIONS = [moveLeft, moveRight, softDrop] + + +class Scheduler { + constructor() { + this.intervalTasks = new Map() + this.timeoutTasks = new Map() + } + + setInterval(func, delay) { + this.intervalTasks.set(func, window.setInterval(func, delay)) + } + + setTimeout(func, delay) { + this.timeoutTasks.set(func, window.setTimeout(func, delay)) + } + + 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() { - this.pos = FALLING_PIECE_POSITION + constructor(position=null, shape=null) { + this.pos = position this.orientation = 0 - this.rotated_last = false - this.rotation_point_5_used = false - this.hold_enabled = true + this.rotatedLast = false + this.rotationPoint5Used = false + this.holdEnabled = true this.srs = { CW: [ [[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], @@ -71,13 +109,18 @@ class Tetromino { [[0, 0], [-1, 0], [-1, 1], [0, 2], [-1, -2]], ], } - if (!shapes.lenght) - shapes = ['I', 'J', 'L', 'O', 'S', 'T', 'Z'] - this.shape = shapes.sample() + if (shape) + this.shape = shape + else { + if (!shapes.length) + shapes = ['I', 'J', 'L', 'O', 'S', 'T', 'Z'] + this.shape = shapes.sample() + } switch(this.shape) { case 'I': - this.color = "cyan" - this.minoes_pos = [[-1, 0], [0, 0], [1, 0], [2, 0]] + this.color = "rgb(132, 225, 225)" + this.ghostColor = "rgba(40, 164, 164, 0.5)" + this.minoesPos = [[-1, 0], [0, 0], [1, 0], [2, 0]] this.srs = { CW: [ [[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]], @@ -94,47 +137,62 @@ class Tetromino { } break case 'J': - this.color = "blue" - this.minoes_pos = [[-1, -1], [-1, 0], [0, 0], [1, 0]] + this.color = "rgb(102, 163, 255)" + this.ghostColor = "rgba(0, 82, 204, 0.5)" + this.minoesPos = [[-1, -1], [-1, 0], [0, 0], [1, 0]] break case 'L': - this.color = "orange" - this.minoes_pos = [[-1, 0], [0, 0], [1, 0], [1, -1]] + this.color = "rgb(255, 148, 77)" + this.ghostColor = "rgba(204, 82, 0, 0.5)" + this.minoesPos = [[-1, 0], [0, 0], [1, 0], [1, -1]] break case 'O': - this.color = "yellow" - this.minoes_pos = [[0, 0], [1, 0], [0, -1], [1, -1]] + this.color = "rgb(255, 255, 102)" + this.ghostColor = "rgba(204, 204, 0, 0.5)" + this.minoesPos = [[0, 0], [1, 0], [0, -1], [1, -1]] this.srs = { CW: [[]], CCW: [[]] } break case 'S': - this.color = "green" - this.minoes_pos = [[-1, -1], [0, -1], [0, 0], [1, 0]] + this.color = "rgb(159, 255, 128)" + this.ghostColor = "rgb(38, 153, 0, 0.5)" + this.minoesPos = [[-1, -1], [0, -1], [0, 0], [1, 0]] break case 'T': - this.color = "magenta" - this.minoes_pos = [[-1, 0], [0, 0], [1, 0], [0, -1]] + this.color = "rgb(179, 102, 255)" + this.ghostColor = "rgba(102, 0, 204, 0.5)" + this.minoesPos = [[-1, 0], [0, 0], [1, 0], [0, -1]] break case 'Z': - this.color = "red" - this.minoes_pos = [[-1, -1], [0, -1], [0, 0], [1, 0]] + this.color = "rgb(255, 51, 51)" + this.ghostColor = "rgba(204, 0, 0, 0.5)" + this.minoesPos = [[-1, -1], [0, -1], [0, 0], [1, 0]] break } } - get abs_minoes_pos() { - return this.minoes_pos.map(pos => pos.add(this.pos)) + get absMinoesPos() { + return this.minoesPos.translate(this.pos) } - draw(context) { - this.abs_minoes_pos.map(pos => draw_mino(context, ...pos, this.color)) + draw(context, ghostYOffset=0) { + if (ghostYOffset) { + context.save() + context.shadowColor = this.ghostColor + context.shadowOffsetX = 0 + context.shadowOffsetY = ghostYOffset * MINO_SIZE + context.shadowBlur = 3 + } + this.absMinoesPos.map(pos => draw_mino(context, ...pos, this.color)) + if (ghostYOffset) + context.restore() } } -function draw_mino(context, x, y, color) { +function draw_mino(context, x, y, color, ghostYOffset) { context.fillStyle = color context.fillRect(x*MINO_SIZE, y*MINO_SIZE, MINO_SIZE, MINO_SIZE) context.lineWidth = 0.5 @@ -143,50 +201,132 @@ function draw_mino(context, x, y, color) { } +class HoldQueue { + constructor(context) { + this.context = context + 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 (this.piece) + this.piece.draw(this.context) + } +} + +timeFormat = new Intl.DateTimeFormat("en-US", { + hour: "numeric", minute: "2-digit", second: "2-digit", hourCycle: "h24", timeZone: "UTC" +}).format + +class Stats { + constructor (div, start_level=1) { + this.div = div + this._score = 0 + this.highScore = 0 + this.level = start_level - 1 + this.goal = 0 + this.linesCleared = 0 + this.startTime = Date.now() + 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 + } + + new_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) + } + + print() { + this.div.innerHTML = this.score + this.div.innerHTML += "
" + this.highScore + this.div.innerHTML += "
" + this.level + this.div.innerHTML += "
" + this.goal + this.div.innerHTML += "
" + this.linesCleared + this.div.innerHTML += "
" + timeFormat(Date.now() - this.startTime) + } +} + + class Matrix { constructor(context) { this.context = context - this.cells = Array.from({length: MATRIX_COLUMNS}, (v, k) => Array(MATRIX_LINES)) + this.cells = Array.from(Array(MATRIX_COLUMNS), x => Array(MATRIX_ROWS)) + this.width = MATRIX_COLUMNS*MINO_SIZE + this.height = MATRIX_ROWS*MINO_SIZE + this.piece = null } - cell_is_occupied(x, y) { - return 0 <= x && x < MATRIX_COLUMNS && y < MATRIX_LINES ? this.cells[x][y] : true + cellIsOccupied(x, y) { + return 0 <= x && x < MATRIX_COLUMNS && y < MATRIX_ROWS ? this.cells[x][y] : true } - space_to_move(piece_pos, minoes_pos) { - for (const pos of minoes_pos) { - if (this.cell_is_occupied(...pos.add(piece_pos))) + spaceToMove(absMinoesPos) { + for (const pos of absMinoesPos) { + if (this.cellIsOccupied(...pos)) return false } return true } draw() { + this.context.clearRect(0, 0, this.width, this.height) // grid - const width = MATRIX_COLUMNS*MINO_SIZE - const height = MATRIX_LINES*MINO_SIZE - this.context.clearRect(0, 0, width, height) this.context.strokeStyle = "rgba(128, 128, 128, 128)" this.context.lineWidth = 0.5 this.context.beginPath() - for (var x = 0; x <= width; x += MINO_SIZE) { + for (var x = 0; x <= this.width; x += MINO_SIZE) { this.context.moveTo(x, 0); - this.context.lineTo(x, height); + this.context.lineTo(x, this.height); } - for (var y = 0; y <= height; y += MINO_SIZE) { + for (var y = 0; y <= this.height; y += MINO_SIZE) { this.context.moveTo(0, y); - this.context.lineTo(width, y); + this.context.lineTo(this.width, y); } this.context.stroke() // falling piece - falling_piece.draw(this.context) + if (this.piece) + for (var ghostYOffset = 0; this.spaceToMove(this.piece.minoesPos.translate([this.piece.pos[0], this.piece.pos[1]+ghostYOffset])); ghostYOffset++) {} + this.piece.draw(this.context, --ghostYOffset) } } + +class NextQueue { + constructor(context) { + this.context = context + 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) + this.pieces.map(piece => piece.draw(this.context)) + } +} + + function move(movement) { - const test_pos = falling_piece.pos.add(movement) - if (matrix.space_to_move(test_pos, falling_piece.minoes_pos)) { - falling_piece.pos = test_pos + const test_pos = matrix.piece.pos.add(movement) + if (matrix.spaceToMove(matrix.piece.minoesPos.translate(test_pos))) { + matrix.piece.pos = test_pos return true } else { @@ -195,14 +335,14 @@ function move(movement) { } function rotate(spin) { - const test_minoes_pos = falling_piece.minoes_pos.map(pos => pos.rotate(spin)) + const test_minoes_pos = matrix.piece.minoesPos.map(pos => pos.rotate(spin)) rotation_point = 0 - for (const movement of falling_piece.srs[spin==SPIN.CW?"CW":"CCW"][falling_piece.orientation]) { - const test_pos = falling_piece.pos.add(movement) - if (matrix.space_to_move(test_pos, test_minoes_pos)) { - falling_piece.pos = test_pos - falling_piece.minoes_pos = test_minoes_pos - falling_piece.orientation = (falling_piece.orientation + spin + 4) % 4 + for (const movement of matrix.piece.srs[spin==SPIN.CW?"CW":"CCW"][matrix.piece.orientation]) { + const test_pos = matrix.piece.pos.add(movement) + if (matrix.spaceToMove(test_minoes_pos.translate(test_pos))) { + matrix.piece.pos = test_pos + matrix.piece.minoesPos = test_minoes_pos + matrix.piece.orientation = (matrix.piece.orientation + spin + 4) % 4 break; } rotation_point++ @@ -239,34 +379,47 @@ function rotateCCW() { rotate(SPIN.CCW) } -actions = { - "ArrowLeft": moveLeft, - "ArrowRight": moveRight, - "ArrowDown": softDrop, - " ": hardDrop, - "ArrowUp": rotateCW, - "z": rotateCCW +function hold() { + if (this.matrix.piece.holdEnabled) { + this.matrix.piece.holdEnabled = false + clearInterval(lockPhaseIntervalID) + var shape = this.matrix.piece.shape + this.matrix.piece = this.holdQueue.piece + this.holdQueue.piece = new Tetromino(HELD_PIECE_POSITION, shape) + this.generationPhase(this.matrix.piece) + } } -pressedKeys = new Set() -repeatableActions = [moveLeft, moveRight, softDrop] -actionsToRepeat = [] -autorepeatTimeoutID = null -autorepeatIntervalID = null +function new_level() { + stats.new_level() + fallIntervalID = setInterval(fall, stats.fallDelay) + generationPhase() +} + +function generationPhase(held_piece=null) { + if (!held_piece) { + this.matrix.piece = this.nextQueue.pieces.shift() + this.nextQueue.pieces.push(new Tetromino()) + this.nextQueue.pieces.map((piece, i, pieces) => piece.pos = NEXT_PIECES_POSITIONS[i]) + } + this.matrix.piece.pos = FALLING_PIECE_POSITION + /*if (this.matrix.spaceToMove(this.matrix.piece.minoesPos.translate(this.matrix.piece.pos))) + fallingPhase() + else + gameOver()*/ +} function autorepeat() { if (actionsToRepeat.length) { actionsToRepeat[0]() - if (autorepeatTimeoutID) { - autorepeatTimeoutID = clearTimeout(autorepeatTimeoutID) - autorepeatIntervalID = setInterval(autorepeat, AUTOREPEAT_PERIOD) + if (scheduler.timeoutTasks.has(autorepeat)) { + scheduler.clearTimeout(autorepeat) + scheduler.setInterval(autorepeat, AUTOREPEAT_PERIOD) } } else { - if (autorepeatTimeoutID) - autorepeatTimeoutID = clearTimeout(autorepeatTimeoutID) - if (autorepeatIntervalID) - autorepeatIntervalID = clearInterval(autorepeatIntervalID) + scheduler.clearTimeout(autorepeat) + scheduler.clearInterval(autorepeat) } } @@ -276,18 +429,14 @@ function keyDownHandler(e) { pressedKeys.add(e.key) action = actions[e.key] action() - if (repeatableActions.includes(action)) { + if (REPEATABLE_ACTIONS.includes(action)) { actionsToRepeat.unshift(action) - if (autorepeatTimeoutID) { - autorepeatTimeoutID = clearTimeout(autorepeatTimeoutID) - } - if (autorepeatIntervalID) { - autorepeatIntervalID = clearInterval(autorepeatIntervalID) - } + scheduler.clearTimeout(autorepeat) + scheduler.clearInterval(autorepeat) if (actionsToRepeat == softDrop) - autorepeatIntervalID = setInterval(autorepeat, FALL_DELAY / 20) + scheduler.setInterval(autorepeat, FALL_DELAY / 20) else - autorepeatTimeoutID = setTimeout(autorepeat, AUTOREPEAT_DELAY) + scheduler.setTimeout(autorepeat, AUTOREPEAT_DELAY) } } } @@ -300,39 +449,42 @@ function keyUpHandler(e) { if (actionsToRepeat.includes(action)) { actionsToRepeat.splice(actionsToRepeat.indexOf(action), 1) if (!actionsToRepeat.length) { - if (autorepeatTimeoutID) { - autorepeatTimeoutID = clearTimeout(autorepeatTimeoutID) - } - if (autorepeatIntervalID) { - autorepeatIntervalID = clearInterval(autorepeatIntervalID) - } + scheduler.clearTimeout(autorepeat) + scheduler.clearInterval(autorepeat) } } } } function draw() { - held_piece.draw(holdContext) + holdQueue.draw() + stats.print() matrix.draw() - next_pieces.map(piece => piece.draw(nextContext)) + nextQueue.draw() requestAnimationFrame(draw) } window.onload = function() { - holdContext = document.getElementById("hold").getContext("2d") - matrixContext = document.getElementById("matrix").getContext("2d") - nextContext = document.getElementById("next").getContext("2d") + holdQueue = new HoldQueue(document.getElementById("hold").getContext("2d")) + stats = new Stats(document.getElementById("stats-values")) + matrix = new Matrix(document.getElementById("matrix").getContext("2d")) + nextQueue = new NextQueue(document.getElementById("next").getContext("2d")) + scheduler = new Scheduler() - matrix = new Matrix(matrixContext) - held_piece = new Tetromino() - held_piece.pos = HELD_PIECE_POSITION - falling_piece = new Tetromino() - falling_piece.pos = FALLING_PIECE_POSITION - next_pieces = Array.from({length: NEXT_PIECES}, (v, k) => new Tetromino()) - next_pieces.map((piece, i, array) => piece.pos = NEXT_PIECES_POSITIONS[i]) - - document.addEventListener("keydown", keyDownHandler, false); - document.addEventListener("keyup", keyUpHandler, false); - setInterval(fall, FALL_DELAY); + actions = { + "ArrowLeft": moveLeft, + "ArrowRight": moveRight, + "ArrowDown": softDrop, + " ": hardDrop, + "ArrowUp": rotateCW, + "z": rotateCCW, + "c": hold + } + pressedKeys = new Set() + actionsToRepeat = [] + addEventListener("keydown", keyDownHandler, false) + addEventListener("keyup", keyUpHandler, false) requestAnimationFrame(draw) + + this.new_level() } \ No newline at end of file