Initial commit for my new js project
This commit is contained in:
commit
22199e728b
5
.idea/.gitignore
generated
vendored
Normal file
5
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
12
.idea/TetrisAI.iml
generated
Normal file
12
.idea/TetrisAI.iml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
157
css/style.css
Normal 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
64
index.html
Normal 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
51
js/animationManager.js
Normal 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
5
js/constants.js
Normal 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
37
js/eventBus.js
Normal 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
127
js/game.js
Normal 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
85
js/grid.js
Normal 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
29
js/inputHandler.js
Normal 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
63
js/main.js
Normal 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
9
js/piece.js
Normal 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
129
js/pieceFactory.js
Normal 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
93
js/renderer.js
Normal 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
5
js/soundManager.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
class SoundManager {
|
||||||
|
constructor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
32
js/uiManager.js
Normal file
32
js/uiManager.js
Normal 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
3
js/utils.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
function inArray(x, y, array){
|
||||||
|
return x >= 0 && x < array[0].length && y >= 0 && y < array.length;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user