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";
- }
-?>
-
-
-
+
+
+
+
+ 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";
+ }
+?>
+
+
+
\ 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
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ Webtris
+
+
+
+
+
+ WEBTRIS
+
+
+
+
+
+
+ SCORE
+ RECORD
+ TEMPS
+
+ NIVEAU
+ OBJECTIF
+ LIGNES
+
+
+ 0
+ 0
+ 00:00
+
+ 0
+ 0
+ 0
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file