Initial commit for my new js project

This commit is contained in:
Oleg 2025-10-31 11:47:55 +00:00
commit 22199e728b
18 changed files with 914 additions and 0 deletions

5
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

12
.idea/TetrisAI.iml generated Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/TetrisAI.iml" filepath="$PROJECT_DIR$/.idea/TetrisAI.iml" />
</modules>
</component>
</project>

157
css/style.css Normal file
View File

@ -0,0 +1,157 @@
/* Импортируем шрифт, который указали в HTML */
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
/* Базовый сброс стилей и настройка всей страницы */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
/* Фон и центрирование игрового контейнера */
background-color: #1c1c1c;
color: #f0f0f0;
font-family: 'Press Start 2P', cursive;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
/* Главный контейнер, который держит поле и боковую панель */
.tetris-wrapper {
display: flex;
gap: 25px; /* Расстояние между игровым полем и панелью */
padding: 20px;
border: 5px solid #555;
border-radius: 10px;
background-color: #333;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
}
/* Стиль для основного игрового холста */
#game-board {
border: 3px solid #777;
background-color: #000000;
/*flex-grow: 0;*/
align-self: flex-start;
}
/* Боковая панель с информацией */
.side-panel {
display: flex;
flex-direction: column;
width: 200px;
text-align: center;
}
.side-panel h1 {
font-size: 2.2em;
margin-bottom: 25px;
color: #e74c3c;
text-shadow: 3px 3px #000;
}
/* Контейнеры для счёта, линий и уровня */
.info-box {
background-color: #000;
border: 2px solid #777;
padding: 15px 10px;
margin-bottom: 20px;
border-radius: 5px;
}
.info-title {
font-size: 0.8em;
color: #999;
margin-bottom: 10px;
display: block; /* Чтобы margin-bottom сработал */
}
.info-box p {
font-size: 1.5em;
color: #f1c40f; /* Яркий желтый для цифр */
}
/* Отдельные стили для контейнера следующей фигуры */
.next-piece-container {
display: flex; /* Используем flexbox */
flex-direction: column;
justify-content: center; /* Центрируем по вертикали */
align-items: center; /* Центрируем по горизонтали */
/* Убрали flex-grow, чтобы высота зависела от контента */
}
#next-piece-board {
background-color: #000;
border: 2px solid #777;
/* Убрали margin, так как центрированием теперь управляет родитель */
max-width: 100%;
height: auto;
}
/* Общий стиль для кнопок */
.game-button {
background-color: #27ae60; /* Зеленый */
color: #fff;
border: none;
padding: 15px;
font-family: 'Press Start 2P', cursive;
text-transform: uppercase;
cursor: pointer;
border-radius: 5px;
border-bottom: 4px solid #229954; /* 3D-эффект */
transition: all 0.1s ease;
margin-top: auto
}
.game-button:hover {
background-color: #2ecc71;
transform: translateY(-2px); /* Небольшой подъём при наведении */
}
.game-button:active {
transform: translateY(1px); /* Эффект нажатия */
border-bottom-width: 2px;
}
/* Слой-оверлей для модальных окон */
#game-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
align-items: center;
}
/* Модальное окно для сообщений (Пауза, Конец игры) */
#game-modal {
background-color: #333;
padding: 30px 40px;
border-radius: 10px;
border: 5px solid #555;
text-align: center;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.8);
}
#modal-title {
font-size: 1.8em;
margin-bottom: 15px;
color: #e74c3c;
}
#modal-text {
font-size: 1.1em;
margin-bottom: 25px;
line-height: 1.5;
}
/* Вспомогательный класс, чтобы скрыть элемент */
.hidden {
display: none !important;
}

64
index.html Normal file
View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Тетрис | ООП на JS</title>
<link rel="stylesheet" href="./css/style.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
</head>
<body>
<div class="tetris-wrapper">
<canvas id="game-board" width="309" height="619"></canvas>
<aside class="side-panel">
<h1>ТЕТРИС</h1>
<div class="info-box">
<span class="info-title">СЧЁТ</span>
<p id="score">0</p>
</div>
<div class="info-box">
<span class="info-title">ЛИНИИ</span>
<p id="lines">0</p>
</div>
<div class="info-box">
<span class="info-title">УРОВЕНЬ</span>
<p id="level">1</p>
</div>
<div class="info-box next-piece-container">
<span class="info-title">СЛЕДУЮЩАЯ</span>
<canvas id="next-piece-board" width="185" height="185"></canvas>
</div>
<button id="start-button" class="game-button">СТАРТ</button>
</aside>
</div>
<div id="game-overlay" class="hidden">
<div id="game-modal">
<h2 id="modal-title"></h2>
<p id="modal-text"></p>
<button id="modal-button" class="game-button">СТАРТ</button>
</div>
</div>
<script src="./js/utils.js"></script>
<script src="./js/eventBus.js"></script>
<script src="./js/inputHandler.js"></script>
<script src="./js/uiManager.js"></script>
<script src="./js/grid.js"></script>
<script src="./js/pieceFactory.js"></script>
<script src="./js/renderer.js"></script>
<script src="./js/animationManager.js"></script>
<script src="./js/game.js"></script>
<script src="./js/main.js"></script>
</body>
</html>

51
js/animationManager.js Normal file
View File

@ -0,0 +1,51 @@
class AnimationManager {
constructor(eventBus) {
this.eventBus = eventBus;
this.eventBus.on("animate", ({count, time, indexes, matrix})=>{
return this.start(count, time, indexes, matrix);
})
}
start(count, time, indexes, matrix){
let cnt = count;
this.matrixForAnimation = matrix;
this.blinkedLineIndexes = indexes;
return new Promise((resolve) => {
//Отпишусь от событий клавиатуры.
/* this.eventBus.emit("set-is-animating",{state:true});
this.eventBus.emit("stop-gameplay", {})
this.eventBus.emit("pause-main-render",{});
this.eventBus.emit("resume-animation-render",{});*/
this.eventBus.emit("set-render-mode", {mode:"animation"})
// Здесь я отпишусь от основного рендера
//Подпишусь на рендер анимации.
const recursion = ()=>{
this.animation(cnt);
cnt--;
if(cnt === 0){
//Подпишусь обратно на события клавиатуры.
// Здесь я отпишусь от анимационного рендера
//Подпишусь на основной рендер.
/*this.eventBus.emit("set-is-animating",{state:false});
this.eventBus.emit("start-gameplay", {});
this.eventBus.emit("pause-animation-render",{});
this.eventBus.emit("resume-main-render",{});*/
this.eventBus.emit("set-render-mode", {mode:"game"})
return resolve();
}
setTimeout(recursion, time)
}
setTimeout(recursion, time);
})
}
animation(cnt){
console.log(cnt);
this.eventBus.emit("animation-render", {
matrix: this.matrixForAnimation,
indexes: this.blinkedLineIndexes,
count: cnt
});
}
}

5
js/constants.js Normal file
View File

@ -0,0 +1,5 @@
const GRID_WIDTH = 10;
const GRID_HEIGHT = 20;
const SIZE = 30;
const COLORS = ["#CCCCCC", "red"];
const eventBus = new EventBus();

37
js/eventBus.js Normal file
View File

@ -0,0 +1,37 @@
class EventBus{
constructor(){
this.listeners = {}
}
on(eventName, callback){
if(!this.listeners[eventName]){
this.listeners[eventName] = [];
}
this.listeners[eventName].push(callback);
return ()=>{
this.off(eventName, callback);
}
}
emit(eventName, data){
if(!this.listeners[eventName]){
return;
}
this.listeners[eventName].forEach((callback)=>{
callback(data);
})
}
async emitAsync(eventName, data){
if(!this.listeners[eventName]){
return Promise.resolve();
}
const callBackPromises = this.listeners[eventName].map((callback)=>{
return Promise.resolve(callback(data));
})
await Promise.all(callBackPromises);
}
off(eventName, callback){
if(!this.listeners[eventName]){
return;
}
this.listeners[eventName] = this.listeners[eventName].filter(listener => listener !== callback);
}
}

127
js/game.js Normal file
View File

@ -0,0 +1,127 @@
class Game{
constructor(eventBus, grid, previewGrid, pieceFactory){
this.eventBus = eventBus;
this.lastTime = performance.now();
this.dropCounter = 0;
this.dropInterval = 300;
this.grid = grid;
this.previewGrid = previewGrid;
this.pieceFactory = pieceFactory;
this.bagGenerator = this.pieceFactory.bagGenerator();
this.loopRequestID = null;
this.currentPiece = null;
this.nextPiece = null;
this.level = 1;
this.lines = 0;
this.score = 0;
this.moveHandler = this.moveHandler.bind(this);
this.lockPiece = this.lockPiece.bind(this);
this._setupInputListeners();
this.loop = this.loop.bind(this);
this.eventBus.emit("update", this.uiState());
this.subscriptions = []; // Подписки.
// Смотреть здесь
this.unsubscribleStartGameplay = this.eventBus.on("start-gameplay", ({})=>{
this._setupInputListeners();
});
this.unsubscribleStopGameplay = this.eventBus.on("stop-gameplay", ({})=>{
this.unSetupInputListeners();
})
}
getRenderState(){
return {
matrix:this.grid.matrix,
piece:this.currentPiece,
nextMatrix:this.previewGrid.matrix,
nextPiece:this.nextPiece,
};
}
moveHandler({dx, dy}){ //Деструктуризация moveState
this.move(dx, dy);
}
_setupInputListeners() {
this.move_off = this.eventBus.on("move", this.moveHandler);
this.rotate_off = this.eventBus.on("rotate", ()=>{
this.rotate();
});
this.lockPiece_off = this.eventBus.on("lockPiece", this.lockPiece);
}
unSetupInputListeners() {
// Вызываем сохраненные функции-отписки, которые мы получили от eventBus.on()
if(this.move_off) this.move_off();
if(this.rotate_off) this.rotate_off();
if(this.lockPiece_off) this.lockPiece_off();
}
loop(time){
const deltaTime = time - this.lastTime;
this.lastTime = time;
this.dropCounter += deltaTime;
if (this.dropCounter > this.dropInterval) {
this.eventBus.emit("move", {dx:0, dy:1});
this.dropCounter -= this.dropInterval;
}
this.eventBus.emit("render", this.getRenderState());
this.loopRequestID = window.requestAnimationFrame(this.loop);
}
spawnNewPiece() {
if(this.nextPiece == null) {
const currentType = this.bagGenerator.next().value;
const nextType = this.bagGenerator.next().value;
this.currentPiece = this.pieceFactory.generatePieceByType(currentType, this.grid.width, this.grid.height);
this.nextPiece = this.pieceFactory.generatePieceByType(nextType, this.previewGrid.width, this.previewGrid.height, true);
}
else{
const nextType = this.bagGenerator.next().value;
this.currentPiece = this.pieceFactory.generatePieceByType(this.nextPiece.type, this.grid.width, this.grid.height);
this.nextPiece = this.pieceFactory.generatePieceByType(nextType, this.previewGrid.width, this.previewGrid.height, true);
}
}
move(dx, dy){
if(this.currentPiece === null) return;
const moved = this.currentPiece.move(dx, dy);
if(!this.grid.isCollision(moved)){
this.currentPiece = moved;
/*if (dy === 1) {
this.dropCounter = 0;
}*/
}else if(dy == 1){
//this.eventBus.emit("render", this.getRenderState());
this.eventBus.emit("lockPiece",{piece:this.currentPiece});
}
}
rotate(){
if(this.currentPiece === null) return;
const rotated = this.currentPiece.rotate();
if(!this.grid.isCollision(rotated)){
this.currentPiece = rotated;
}
}
async lockPiece({piece}){
this.grid.matrix = this.grid.getMergedMatrix(piece);
const blinkedIndexes = this.grid.getBlinkedIndexes();
if(blinkedIndexes.length > 0){
this.eventBus.emit("force-render", this.getRenderState()); //
await this.eventBus.emitAsync("animate",{count:4, time:100, indexes:blinkedIndexes, matrix:this.grid.matrix});
this.lines += this.grid.deleteLines();
this.eventBus.emit("update", this.uiState());
}
this.spawnNewPiece();
if(this.grid.isCollision(this.currentPiece)){
this.eventBus.emit("gameEnd",{});
}
}
uiState(){
return {
level:this.level,
lines:this.lines,
score:this.score
}
}
start(){
if(this.loopRequestID) return;
this.spawnNewPiece();
this.lastTime = performance.now();
this.loopRequestID = window.requestAnimationFrame(this.loop)
}
}

85
js/grid.js Normal file
View File

@ -0,0 +1,85 @@
class Grid{
constructor(gw, gh){
this.matrix = new Array(gh).fill(0).map(()=>new Array(gw).fill(0));
this.width = gw;
this.height = gh;
}
/*static inArray(x, y, array){
return x >= 0 && x < array[0].length && y >= 0 && y < array.length;
}*/
getMergedMatrix(piece){
const result = this.matrix.map(row => [...row]);
for(let y=0; y < piece.body.length; y++){
for(let x=0; x < piece.body[y].length; x++){
const color = piece.body[y][x];
const gridY = piece.y + y;
const gridX = piece.x + x;
if(inArray(gridX, gridY, this.matrix) && color !== 0){
result[gridY][gridX] = color;
}
}
}
return result;
}
isCollision(piece){
for(let y=0; y < piece.body.length; y++){
for(let x=0; x < piece.body[y].length; x++){
let gridX = piece.x + x;
let gridY = piece.y + y;
if(piece.body[y][x] > 0) {
if(!inArray(gridX, gridY, this.matrix)) {
return true;
}
else if(this.matrix[gridY][gridX] > 0){
return true;
}
}
}
}
return false;
}
mergeMatrix(piece){
for(let y=0; y < piece.body.length; y++){
for(let x=0; x < piece.body[y].length; x++){
const color = piece.body[y][x];
const gridY = piece.y + y;
const gridX = piece.x + x;
if(Grid.inArray(gridX, gridY, this.matrix) && color !== 0){
this.matrix[gridY][gridX] = color;
}
}
}
}
deleteLines(){
let deletedLines = 0;
let W = this.matrix.length -1;
for(let R = this.matrix.length -1; R >= 0; R--){
let fillLine = this.matrix[R].every((cell)=>cell > 0);
if(fillLine){
deletedLines++;
}else{
if(R != W){
this.matrix[W] = [...this.matrix[R]];
}
W--;
}
}
for(let i=0; i < deletedLines;i++){
this.matrix[i] = new Array(this.matrix[0].length).fill(0);
}
return deletedLines;
}
getBlinkedIndexes(){
const blinkedIndexes = [];
for(let y = this.matrix.length - 1; y >= 0; y--){
if(this.matrix[y].every(cell=>cell == 0)){
return blinkedIndexes;
}
if(this.matrix[y].every(cell=>cell > 0)){
blinkedIndexes.push(y);
}
}
return blinkedIndexes;
}
}

29
js/inputHandler.js Normal file
View File

@ -0,0 +1,29 @@
class InputHandler {
constructor(eventBus) {
this.eventBus = eventBus;
this.handleKeyDown = this.handleKeyDown.bind(this);
this.keys = {
"ArrowRight":()=>{
this.eventBus.emit("move", {dx:1,dy:0});
},
"ArrowLeft":()=>{
this.eventBus.emit("move", {dx:-1, dy:0});
},
"ArrowDown":()=>{
this.eventBus.emit("move", {dx:0, dy:1});
},
"ArrowUp":()=>{
this.eventBus.emit("rotate", {});
},
"Space":()=>{
this.eventBus.emit("gameStart", {});
}
}
document.addEventListener("keydown", this.handleKeyDown);
}
handleKeyDown(event) {
if(event.code in this.keys){
this.keys[event.code]()
}
}
}

63
js/main.js Normal file
View File

@ -0,0 +1,63 @@
window.addEventListener("load", async function (){
const GAME_CONFIG ={
mainGrid:{width:10,height:20},
previewGrid:{width:6,height:6},
cellSize:30,
colors:["#CCCCCC", "red", "blue", "green", "yellow", "orange", "purple", "cyan", "#FAEBD7"],
}
const canvas = document.querySelector("#game-board");
const nextCanvas = document.querySelector("#next-piece-board");
const uiLvl = document.querySelector("#level");
const uiLines = document.querySelector("#lines");
const uiScore = document.querySelector("#score");
const startButton = document.querySelector("#start-button");
const overlay = document.querySelector("#game-overlay");
const eventBus = new EventBus();
const render = new Renderer(canvas, nextCanvas, eventBus, GAME_CONFIG.cellSize, GAME_CONFIG.colors);
new UIManager(eventBus, overlay, startButton, uiLvl, uiLines, uiScore);
new AnimationManager(eventBus);
new InputHandler(eventBus);
////////////////////////////////////////////
let game = null;
let isAnimating = false;
function gameInit(){
const grid = new Grid(GAME_CONFIG.mainGrid.width, GAME_CONFIG.mainGrid.height);
const previewGrid = new Grid(GAME_CONFIG.previewGrid.width, GAME_CONFIG.previewGrid.height);
const pieceFactory = new PieceFactory();
const game = new Game(eventBus, grid, previewGrid, pieceFactory);
eventBus.emit("render", game.getRenderState());
return game;
}
function gameStart(){
if(isAnimating) return;
//Я подпишусь на эту функцию в EventBus. Событие будет вызываться при клавише пробел
if (game) {
window.cancelAnimationFrame(game.loopRequestID);
game.unSetupInputListeners();
game.unsubscribleStartGameplay();
game.unsubscribleStopGameplay();
//game.unSetupInputListeners() // <-- ГЛАВНОЕ: УДАЛЕНИЕ СЛУШАТЕЛЕЙ СТАРОЙ ИГРЫ
}
game = gameInit()
game.start();
}
function gameEnd(){
if (game) {
game.unSetupInputListeners();
game.unsubscribleStartGameplay();
game.unsubscribleStopGameplay();
window.cancelAnimationFrame(game.loopRequestID);
eventBus.emit("showGameOver", game.uiState())
}
}
eventBus.on("set-is-animating", ({state})=>{
isAnimating = state;
});
eventBus.on("gameEnd", gameEnd);
eventBus.on("gameStart", gameStart);
game = gameInit();
})

9
js/piece.js Normal file
View File

@ -0,0 +1,9 @@
const piece = {
x:1,
y:1,
body:[
[0,1,0],
[1,1,1],
[0,0,0],
]
}

129
js/pieceFactory.js Normal file
View File

@ -0,0 +1,129 @@
const TETROMINOS = {
T:{
body:[
[0,1,0],
[1,1,1],
[0,0,0]
],
color:1
},
L:{
body:[
[0,2,0],
[0,2,0],
[0,2,2]
],
color:2
},
J:{
body:[
[0,3,0],
[0,3,0],
[3,3,0]
],
color:3
},
I:{
body:[
[0, 0, 0, 0],
[4, 4, 4, 4],
[0, 0, 0, 0],
[0, 0, 0, 0]
],
color:4
},
Z:{
body:[
[5,5,0],
[0,5,5],
[0,0,0],
],
color:5
},
S:{
body:[
[0,6,6],
[6,6,0],
[0,0,0],
],
color:6
},
O:{
body:[
[7,7],
[7,7],
],
color:7
}
}
class Piece {
constructor(type, body, color, x, y){
this.type = type;
this.color = color;
this.body = body;
this.x = x;
this.y = y;
}
clone(){
return new Piece(
this.type,
this.body.map(row=>[...row]),
this.color,
this.x,
this.y,
);
}
move(dx, dy){
const clone= this.clone();
clone.x += dx;
clone.y += dy;
return clone;
}
rotate(){
const clone=this.clone();
const len = this.body.length;
const body = this.body;
clone.body = this.body.map(
(row,y)=>
row.map((cell, x)=>body[len - 1 - x][y])
);
return clone;
}
}
class PieceFactory{
constructor(){
this.types = Object.keys(TETROMINOS);
}
_createPiece(type, gridWidth, gridHeight, preview = false){
const data = TETROMINOS[type];
const x = Math.floor(gridWidth / 2 - data.body[0].length / 2);
const y = preview ? x : 0;
return new Piece(type, data.body, data.color, x, y);
}
generateRandomPiece(gridWidth, gridHeight, preview = false){
const typeIndex = Math.floor(Math.random() * this.types.length);
const type = this.types[typeIndex];
return this._createPiece(type, gridWidth, gridHeight, preview);
}
generatePieceByType(type, gridWidth, gridHeight, preview = false){
return this._createPiece(type, gridWidth, gridHeight, preview);
}
*bagGenerator(etalon = ["I","T","L","J","O","Z", "S"]){
function suffle(a){
for(let i = a.length-1; i>=0; i--){
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
while(true){
const types = suffle([...etalon]);
for(let type of types){
yield type;
}
}
}
}

93
js/renderer.js Normal file
View File

@ -0,0 +1,93 @@
class Renderer{
constructor(canvas, nextCanvas, eventBus, size, colors){
this.canvas = canvas;
this.nextCanvas = nextCanvas;
this.ctx = this.canvas.getContext('2d');
this.nextCtx = this.nextCanvas.getContext('2d');
this.eventBus = eventBus;
this.size = size;
this.colors = colors;
this.unsubscribers = [];
this.renderCallBack = ({matrix, piece, nextMatrix, nextPiece})=>{
this.draw(matrix, piece, nextMatrix, nextPiece);
}
this.animationRenderCallback = ({matrix, indexes, count})=>{
let matrixToDraw;
if(count % 2 == 0){
matrixToDraw = matrix;
}
else{
matrixToDraw = this.prepareBlinkMatrix(matrix, indexes);
}
this.drawGrid(this.ctx, matrixToDraw);
}
this.modeCallback = ({mode})=>{
if(mode === "animation"){
if(this.off_main_render) this.off_main_render();
this.off_main_render = null;
if(this.off_animation_render === null){
this.off_animation_render = this.eventBus.on("animation-render", this.animationRenderCallback);
}
}else if(mode === "game"){
if(this.off_animation_render) this.off_animation_render();
this.off_animation_render = null;
if(this.off_main_render === null){
this.off_main_render = this.eventBus.on("render", this.renderCallBack);
}
}
}
//////////
this.off_main_render = this.eventBus.on("render", this.renderCallBack);
this.off_animation_render = null;
this.pauseForceRender = this.eventBus.on("force-render", this.renderCallBack)
this.eventBus.on("set-render-mode", this.modeCallback);
////////
}
eventOn(eventName, callback){
const unsubscriber = this.eventBus.on(eventName, callback);
this.unsubscribers.push(unsubscriber);
}
dispose(){
this.unsubscribers.forEach((unsubscriber) => unsubscriber());
this.unsubscribers = [];
}
drawCell(ctx, x, y, colorIndex){
const space = 1;
ctx.fillStyle = this.colors[colorIndex];
ctx.fillRect(x * (this.size+space), y * (this.size+space), this.size, this.size);
}
drawGrid(ctx, matrix){
for(let y=0; y < matrix.length; y++){
for(let x=0; x < matrix[y].length; x++){
this.drawCell(ctx, x, y, matrix[y][x]);
}
}
}
drawPiece(ctx, matrix, piece){
if(piece === null) return;
for(let y =0; y < piece.body.length; y++){
for(let x =0; x < piece.body[y].length; x++){
let gridX = piece.x + x;
let gridY = piece.y + y;
if(inArray(gridX, gridY, matrix) && piece.body[y][x] !== 0){
this.drawCell(ctx, gridX, gridY, piece.body[y][x]);
}
}
}
}
draw(matrix, piece, nextMatrix, nextPiece){
this.drawGrid(this.ctx, matrix);
this.drawPiece(this.ctx, matrix, piece);
this.drawGrid(this.nextCtx, nextMatrix);
this.drawPiece(this.nextCtx, nextMatrix, nextPiece);
}
prepareBlinkMatrix(matrix, blinkedIndexes, blinkColorIndex = 8){
let blinkedMatrix = matrix.map((row)=>[...row]);
const blinkedRow =new Array(blinkedMatrix[0].length).fill(8);
for(let i = 0; i < blinkedIndexes.length; i++){
const blinkedIndex = blinkedIndexes[i];
blinkedMatrix[blinkedIndex] = blinkedRow;
}
return blinkedMatrix;
}
}

5
js/soundManager.js Normal file
View File

@ -0,0 +1,5 @@
class SoundManager {
constructor() {
}
}

32
js/uiManager.js Normal file
View File

@ -0,0 +1,32 @@
class UIManager {
constructor(eventBus, overlay, startButton, uiLvl, uiLines, uiScore) {
this.eventBus = eventBus;
this.overlay = overlay;
this.startButton = startButton;
this.modalButton = this.overlay.querySelector("#modal-button");
this.modalText = this.overlay.querySelector("#modal-text");
this.uiLvl = uiLvl; //HTML элемент.
this.uiLines = uiLines; //HTML элемент.
this.uiScore = uiScore; //HTML элемент
this.startHandler = this.startHandler.bind(this);
this.update = this.update.bind(this);
this.showGameOver = this.showGameOver.bind(this);
this.startButton.addEventListener("click", this.startHandler);
this.modalButton.addEventListener("click", this.startHandler);
this.eventBus.on("update", this.update);
this.eventBus.on("showGameOver",this.showGameOver)
}
showGameOver({lines, score, level}) {
this.overlay.classList.remove("hidden");
this.modalText.textContent = `Уровень:${level}, Линии: ${lines}, Очки:${score}.`;
}
update({level, lines, score}){
this.uiLvl.textContent = level;
this.uiLines.textContent = lines;
this.uiScore.textContent = score;
}
startHandler(){
this.overlay.classList.add("hidden");
this.eventBus.emit("gameStart", {})
}
}

3
js/utils.js Normal file
View File

@ -0,0 +1,3 @@
function inArray(x, y, array){
return x >= 0 && x < array[0].length && y >= 0 && y < array.length;
}