lot of changes

This commit is contained in:
Adrien MALINGREY 2019-10-26 19:13:01 +02:00
parent 0f3ab4052e
commit 9d7fa05272
4 changed files with 296 additions and 117 deletions

View File

@ -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; padding: 0;
margin: 0; margin: 0;
@ -12,7 +20,7 @@ body {
canvas { canvas {
display: block; display: block;
flex-shrink: 0; flex-shrink: 0;
} }
.columns { .columns {
display: flex; display: flex;
@ -20,7 +28,7 @@ canvas {
justify-content: center; justify-content: center;
} }
.side { .space {
flex-grow: 2; flex-grow: 2;
} }
@ -28,19 +36,27 @@ canvas {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 5% 2%; margin: 5% 2%;
height: 400px;
} }
.hold { .hold {
max-width: 120px; max-width: 120px;
height: initial;
} }
.stats { .stats {
display: flex;
flex-direction: row;
margin: 10% 0; margin: 10% 0;
font-family: 'Share Tech Mono';
font-size: 1.2em;
}
.stats-values {
text-align: right;
min-width: 80px;
} }
.matrix { .matrix {
max-width: 200px;
margin: 5% 2%; margin: 5% 2%;
border: 0.5px solid grey; border: 0.5px solid grey;
} }

BIN
fonts/ShareTechMono.woff2 Normal file

Binary file not shown.

View File

@ -9,18 +9,29 @@
</head> </head>
<body> <body>
<div class="columns"> <div class="columns">
<div class="side"></div> <div class="space"></div>
<div class="rows"> <div class="rows">
<canvas id="hold" class="hold" width="120" height="120"></canvas> <div class="columns">
<div class="stats"> <div class="space"></div>
SCORE: 123123123123<br/> <canvas id="hold" class="hold" width="120" height="120"></canvas>
HIGH SCORE:<br/>
TIME:<br/>
</div> </div>
<div class="space"></div>
<div class="stats">
<div class="stats-names">
SCORE<br/>
HIGH<br/>
LEVEL<br/>
GOAL<br/>
LINES<br/>
TIME<br/>
</div>
<div id="stats-values" class="stats-values"></div>
</div>
</div> </div>
<canvas id="matrix" class="matrix" width="200" height="400">Votre navigateur ne supporte pas HTML5, veuillez le mettre à jour pour jouer.</canvas> <canvas id="matrix" class="matrix" width="200" height="400">Votre navigateur ne supporte pas HTML5, veuillez le mettre à jour pour jouer.</canvas>
<canvas id="next" class="next" width="120" height="400"></canvas> <canvas id="next" class="next" width="120" height="400"></canvas>
<div class="side"></div> <div class="space"></div>
</div> </div>
</body> </body>
</html> </html>

View File

@ -1,6 +1,9 @@
Array.prototype.add = function(other) { Array.prototype.add = function(other) {
return this.map((x, i) => x + other[i]) 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) { Array.prototype.rotate = function(spin) {
return [-spin*this[1], spin*this[0]] return [-spin*this[1], spin*this[0]]
} }
@ -11,8 +14,12 @@ Array.prototype.sample = function() {
const MINO_SIZE = 20 const MINO_SIZE = 20
const NEXT_PIECES = 5 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 MATRIX_COLUMNS = 10
const NEXT_ROWS = 20
const NEXT_COLUMNS = 6
const HELD_PIECE_POSITION = [2, 2] const HELD_PIECE_POSITION = [2, 2]
const FALLING_PIECE_POSITION = [4, 0] const FALLING_PIECE_POSITION = [4, 0]
const NEXT_PIECES_POSITIONS = Array.from({length: NEXT_PIECES}, (v, k) => [2, k*4+2]) 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_DELAY = 300
const AUTOREPEAT_PERIOD = 10 const AUTOREPEAT_PERIOD = 10
const MOVEMENT = { const MOVEMENT = {
LEFT: [-1, 0], LEFT: [-1, 0],
RIGHT: [1, 0], RIGHT: [ 1, 0],
DOWN: [0, 1] DOWN: [ 0, 1]
} }
const SPIN = { const SPIN = {
CW: 1, CW: 1,
CCW: -1 CCW: -1
} }
const T_SPIN = { const T_SPIN = {
@ -47,16 +54,47 @@ const SCORES = [
{LINES_CLEAR_NAME: "TRIPLE", NO_T_SPIN: 5, T_SPIN: 16}, {LINES_CLEAR_NAME: "TRIPLE", NO_T_SPIN: 5, T_SPIN: 16},
{LINES_CLEAR_NAME: "TETRIS", NO_T_SPIN: 8}, {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 = [] shapes = []
class Tetromino { class Tetromino {
constructor() { constructor(position=null, shape=null) {
this.pos = FALLING_PIECE_POSITION this.pos = position
this.orientation = 0 this.orientation = 0
this.rotated_last = false this.rotatedLast = false
this.rotation_point_5_used = false this.rotationPoint5Used = false
this.hold_enabled = true this.holdEnabled = true
this.srs = { this.srs = {
CW: [ CW: [
[[0, 0], [-1, 0], [-1, -1], [0, 2], [-1, 2]], [[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]], [[0, 0], [-1, 0], [-1, 1], [0, 2], [-1, -2]],
], ],
} }
if (!shapes.lenght) if (shape)
shapes = ['I', 'J', 'L', 'O', 'S', 'T', 'Z'] this.shape = shape
this.shape = shapes.sample() else {
if (!shapes.length)
shapes = ['I', 'J', 'L', 'O', 'S', 'T', 'Z']
this.shape = shapes.sample()
}
switch(this.shape) { switch(this.shape) {
case 'I': case 'I':
this.color = "cyan" this.color = "rgb(132, 225, 225)"
this.minoes_pos = [[-1, 0], [0, 0], [1, 0], [2, 0]] this.ghostColor = "rgba(40, 164, 164, 0.5)"
this.minoesPos = [[-1, 0], [0, 0], [1, 0], [2, 0]]
this.srs = { this.srs = {
CW: [ CW: [
[[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]], [[ 1, 0], [-1, 0], [ 2, 0], [-1, 1], [ 2, -2]],
@ -94,47 +137,62 @@ class Tetromino {
} }
break break
case 'J': case 'J':
this.color = "blue" this.color = "rgb(102, 163, 255)"
this.minoes_pos = [[-1, -1], [-1, 0], [0, 0], [1, 0]] this.ghostColor = "rgba(0, 82, 204, 0.5)"
this.minoesPos = [[-1, -1], [-1, 0], [0, 0], [1, 0]]
break break
case 'L': case 'L':
this.color = "orange" this.color = "rgb(255, 148, 77)"
this.minoes_pos = [[-1, 0], [0, 0], [1, 0], [1, -1]] this.ghostColor = "rgba(204, 82, 0, 0.5)"
this.minoesPos = [[-1, 0], [0, 0], [1, 0], [1, -1]]
break break
case 'O': case 'O':
this.color = "yellow" this.color = "rgb(255, 255, 102)"
this.minoes_pos = [[0, 0], [1, 0], [0, -1], [1, -1]] this.ghostColor = "rgba(204, 204, 0, 0.5)"
this.minoesPos = [[0, 0], [1, 0], [0, -1], [1, -1]]
this.srs = { this.srs = {
CW: [[]], CW: [[]],
CCW: [[]] CCW: [[]]
} }
break break
case 'S': case 'S':
this.color = "green" this.color = "rgb(159, 255, 128)"
this.minoes_pos = [[-1, -1], [0, -1], [0, 0], [1, 0]] this.ghostColor = "rgb(38, 153, 0, 0.5)"
this.minoesPos = [[-1, -1], [0, -1], [0, 0], [1, 0]]
break break
case 'T': case 'T':
this.color = "magenta" this.color = "rgb(179, 102, 255)"
this.minoes_pos = [[-1, 0], [0, 0], [1, 0], [0, -1]] this.ghostColor = "rgba(102, 0, 204, 0.5)"
this.minoesPos = [[-1, 0], [0, 0], [1, 0], [0, -1]]
break break
case 'Z': case 'Z':
this.color = "red" this.color = "rgb(255, 51, 51)"
this.minoes_pos = [[-1, -1], [0, -1], [0, 0], [1, 0]] this.ghostColor = "rgba(204, 0, 0, 0.5)"
this.minoesPos = [[-1, -1], [0, -1], [0, 0], [1, 0]]
break break
} }
} }
get abs_minoes_pos() { get absMinoesPos() {
return this.minoes_pos.map(pos => pos.add(this.pos)) return this.minoesPos.translate(this.pos)
} }
draw(context) { draw(context, ghostYOffset=0) {
this.abs_minoes_pos.map(pos => draw_mino(context, ...pos, this.color)) 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.fillStyle = color
context.fillRect(x*MINO_SIZE, y*MINO_SIZE, MINO_SIZE, MINO_SIZE) context.fillRect(x*MINO_SIZE, y*MINO_SIZE, MINO_SIZE, MINO_SIZE)
context.lineWidth = 0.5 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 += "<br/>" + this.highScore
this.div.innerHTML += "<br/>" + this.level
this.div.innerHTML += "<br/>" + this.goal
this.div.innerHTML += "<br/>" + this.linesCleared
this.div.innerHTML += "<br/>" + timeFormat(Date.now() - this.startTime)
}
}
class Matrix { class Matrix {
constructor(context) { constructor(context) {
this.context = 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) { cellIsOccupied(x, y) {
return 0 <= x && x < MATRIX_COLUMNS && y < MATRIX_LINES ? this.cells[x][y] : true return 0 <= x && x < MATRIX_COLUMNS && y < MATRIX_ROWS ? this.cells[x][y] : true
} }
space_to_move(piece_pos, minoes_pos) { spaceToMove(absMinoesPos) {
for (const pos of minoes_pos) { for (const pos of absMinoesPos) {
if (this.cell_is_occupied(...pos.add(piece_pos))) if (this.cellIsOccupied(...pos))
return false return false
} }
return true return true
} }
draw() { draw() {
this.context.clearRect(0, 0, this.width, this.height)
// grid // 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.strokeStyle = "rgba(128, 128, 128, 128)"
this.context.lineWidth = 0.5 this.context.lineWidth = 0.5
this.context.beginPath() 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.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.moveTo(0, y);
this.context.lineTo(width, y); this.context.lineTo(this.width, y);
} }
this.context.stroke() this.context.stroke()
// falling piece // 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) { function move(movement) {
const test_pos = falling_piece.pos.add(movement) const test_pos = matrix.piece.pos.add(movement)
if (matrix.space_to_move(test_pos, falling_piece.minoes_pos)) { if (matrix.spaceToMove(matrix.piece.minoesPos.translate(test_pos))) {
falling_piece.pos = test_pos matrix.piece.pos = test_pos
return true return true
} }
else { else {
@ -195,14 +335,14 @@ function move(movement) {
} }
function rotate(spin) { 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 rotation_point = 0
for (const movement of falling_piece.srs[spin==SPIN.CW?"CW":"CCW"][falling_piece.orientation]) { for (const movement of matrix.piece.srs[spin==SPIN.CW?"CW":"CCW"][matrix.piece.orientation]) {
const test_pos = falling_piece.pos.add(movement) const test_pos = matrix.piece.pos.add(movement)
if (matrix.space_to_move(test_pos, test_minoes_pos)) { if (matrix.spaceToMove(test_minoes_pos.translate(test_pos))) {
falling_piece.pos = test_pos matrix.piece.pos = test_pos
falling_piece.minoes_pos = test_minoes_pos matrix.piece.minoesPos = test_minoes_pos
falling_piece.orientation = (falling_piece.orientation + spin + 4) % 4 matrix.piece.orientation = (matrix.piece.orientation + spin + 4) % 4
break; break;
} }
rotation_point++ rotation_point++
@ -239,34 +379,47 @@ function rotateCCW() {
rotate(SPIN.CCW) rotate(SPIN.CCW)
} }
actions = { function hold() {
"ArrowLeft": moveLeft, if (this.matrix.piece.holdEnabled) {
"ArrowRight": moveRight, this.matrix.piece.holdEnabled = false
"ArrowDown": softDrop, clearInterval(lockPhaseIntervalID)
" ": hardDrop, var shape = this.matrix.piece.shape
"ArrowUp": rotateCW, this.matrix.piece = this.holdQueue.piece
"z": rotateCCW this.holdQueue.piece = new Tetromino(HELD_PIECE_POSITION, shape)
this.generationPhase(this.matrix.piece)
}
} }
pressedKeys = new Set() function new_level() {
repeatableActions = [moveLeft, moveRight, softDrop] stats.new_level()
actionsToRepeat = [] fallIntervalID = setInterval(fall, stats.fallDelay)
autorepeatTimeoutID = null generationPhase()
autorepeatIntervalID = null }
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() { function autorepeat() {
if (actionsToRepeat.length) { if (actionsToRepeat.length) {
actionsToRepeat[0]() actionsToRepeat[0]()
if (autorepeatTimeoutID) { if (scheduler.timeoutTasks.has(autorepeat)) {
autorepeatTimeoutID = clearTimeout(autorepeatTimeoutID) scheduler.clearTimeout(autorepeat)
autorepeatIntervalID = setInterval(autorepeat, AUTOREPEAT_PERIOD) scheduler.setInterval(autorepeat, AUTOREPEAT_PERIOD)
} }
} }
else { else {
if (autorepeatTimeoutID) scheduler.clearTimeout(autorepeat)
autorepeatTimeoutID = clearTimeout(autorepeatTimeoutID) scheduler.clearInterval(autorepeat)
if (autorepeatIntervalID)
autorepeatIntervalID = clearInterval(autorepeatIntervalID)
} }
} }
@ -276,18 +429,14 @@ function keyDownHandler(e) {
pressedKeys.add(e.key) pressedKeys.add(e.key)
action = actions[e.key] action = actions[e.key]
action() action()
if (repeatableActions.includes(action)) { if (REPEATABLE_ACTIONS.includes(action)) {
actionsToRepeat.unshift(action) actionsToRepeat.unshift(action)
if (autorepeatTimeoutID) { scheduler.clearTimeout(autorepeat)
autorepeatTimeoutID = clearTimeout(autorepeatTimeoutID) scheduler.clearInterval(autorepeat)
}
if (autorepeatIntervalID) {
autorepeatIntervalID = clearInterval(autorepeatIntervalID)
}
if (actionsToRepeat == softDrop) if (actionsToRepeat == softDrop)
autorepeatIntervalID = setInterval(autorepeat, FALL_DELAY / 20) scheduler.setInterval(autorepeat, FALL_DELAY / 20)
else else
autorepeatTimeoutID = setTimeout(autorepeat, AUTOREPEAT_DELAY) scheduler.setTimeout(autorepeat, AUTOREPEAT_DELAY)
} }
} }
} }
@ -300,39 +449,42 @@ function keyUpHandler(e) {
if (actionsToRepeat.includes(action)) { if (actionsToRepeat.includes(action)) {
actionsToRepeat.splice(actionsToRepeat.indexOf(action), 1) actionsToRepeat.splice(actionsToRepeat.indexOf(action), 1)
if (!actionsToRepeat.length) { if (!actionsToRepeat.length) {
if (autorepeatTimeoutID) { scheduler.clearTimeout(autorepeat)
autorepeatTimeoutID = clearTimeout(autorepeatTimeoutID) scheduler.clearInterval(autorepeat)
}
if (autorepeatIntervalID) {
autorepeatIntervalID = clearInterval(autorepeatIntervalID)
}
} }
} }
} }
} }
function draw() { function draw() {
held_piece.draw(holdContext) holdQueue.draw()
stats.print()
matrix.draw() matrix.draw()
next_pieces.map(piece => piece.draw(nextContext)) nextQueue.draw()
requestAnimationFrame(draw) requestAnimationFrame(draw)
} }
window.onload = function() { window.onload = function() {
holdContext = document.getElementById("hold").getContext("2d") holdQueue = new HoldQueue(document.getElementById("hold").getContext("2d"))
matrixContext = document.getElementById("matrix").getContext("2d") stats = new Stats(document.getElementById("stats-values"))
nextContext = document.getElementById("next").getContext("2d") matrix = new Matrix(document.getElementById("matrix").getContext("2d"))
nextQueue = new NextQueue(document.getElementById("next").getContext("2d"))
scheduler = new Scheduler()
matrix = new Matrix(matrixContext) actions = {
held_piece = new Tetromino() "ArrowLeft": moveLeft,
held_piece.pos = HELD_PIECE_POSITION "ArrowRight": moveRight,
falling_piece = new Tetromino() "ArrowDown": softDrop,
falling_piece.pos = FALLING_PIECE_POSITION " ": hardDrop,
next_pieces = Array.from({length: NEXT_PIECES}, (v, k) => new Tetromino()) "ArrowUp": rotateCW,
next_pieces.map((piece, i, array) => piece.pos = NEXT_PIECES_POSITIONS[i]) "z": rotateCCW,
"c": hold
document.addEventListener("keydown", keyDownHandler, false); }
document.addEventListener("keyup", keyUpHandler, false); pressedKeys = new Set()
setInterval(fall, FALL_DELAY); actionsToRepeat = []
addEventListener("keydown", keyDownHandler, false)
addEventListener("keyup", keyUpHandler, false)
requestAnimationFrame(draw) requestAnimationFrame(draw)
this.new_level()
} }