commit 22199e728b35f3c619a94ab4f7211f0ccccaa0bc Author: Oleg Date: Fri Oct 31 11:47:55 2025 +0000 Initial commit for my new js project diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..ea7ed09 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/TetrisAI.iml b/.idea/TetrisAI.iml new file mode 100644 index 0000000..0b872d8 --- /dev/null +++ b/.idea/TetrisAI.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..98fa318 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..6a7e13f --- /dev/null +++ b/css/style.css @@ -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; +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..77d5aa9 --- /dev/null +++ b/index.html @@ -0,0 +1,64 @@ + + + + + + Тетрис | ООП на JS + + + + + + + + + +
+ + + +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/animationManager.js b/js/animationManager.js new file mode 100644 index 0000000..400a927 --- /dev/null +++ b/js/animationManager.js @@ -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 + }); + } +} + + diff --git a/js/constants.js b/js/constants.js new file mode 100644 index 0000000..5f7c8f7 --- /dev/null +++ b/js/constants.js @@ -0,0 +1,5 @@ +const GRID_WIDTH = 10; +const GRID_HEIGHT = 20; +const SIZE = 30; +const COLORS = ["#CCCCCC", "red"]; +const eventBus = new EventBus(); \ No newline at end of file diff --git a/js/eventBus.js b/js/eventBus.js new file mode 100644 index 0000000..57e51b3 --- /dev/null +++ b/js/eventBus.js @@ -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); + } +} \ No newline at end of file diff --git a/js/game.js b/js/game.js new file mode 100644 index 0000000..880c585 --- /dev/null +++ b/js/game.js @@ -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) + } +} + diff --git a/js/grid.js b/js/grid.js new file mode 100644 index 0000000..0c49e0e --- /dev/null +++ b/js/grid.js @@ -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; + } +} \ No newline at end of file diff --git a/js/inputHandler.js b/js/inputHandler.js new file mode 100644 index 0000000..4edd282 --- /dev/null +++ b/js/inputHandler.js @@ -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]() + } + } +} \ No newline at end of file diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..28ec512 --- /dev/null +++ b/js/main.js @@ -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(); +}) + diff --git a/js/piece.js b/js/piece.js new file mode 100644 index 0000000..6d63eb8 --- /dev/null +++ b/js/piece.js @@ -0,0 +1,9 @@ +const piece = { + x:1, + y:1, + body:[ + [0,1,0], + [1,1,1], + [0,0,0], + ] +} \ No newline at end of file diff --git a/js/pieceFactory.js b/js/pieceFactory.js new file mode 100644 index 0000000..22301ca --- /dev/null +++ b/js/pieceFactory.js @@ -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; + } + } + } +} \ No newline at end of file diff --git a/js/renderer.js b/js/renderer.js new file mode 100644 index 0000000..2d81ea0 --- /dev/null +++ b/js/renderer.js @@ -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; + } +} \ No newline at end of file diff --git a/js/soundManager.js b/js/soundManager.js new file mode 100644 index 0000000..d59477a --- /dev/null +++ b/js/soundManager.js @@ -0,0 +1,5 @@ +class SoundManager { + constructor() { + + } +} \ No newline at end of file diff --git a/js/uiManager.js b/js/uiManager.js new file mode 100644 index 0000000..febae5e --- /dev/null +++ b/js/uiManager.js @@ -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", {}) + } +} \ No newline at end of file diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000..464466c --- /dev/null +++ b/js/utils.js @@ -0,0 +1,3 @@ +function inArray(x, y, array){ + return x >= 0 && x < array[0].length && y >= 0 && y < array.length; +} \ No newline at end of file