aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src/components/Board
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/src/components/Board')
-rw-r--r--frontend/src/components/Board/TetrisBoard.vue162
1 files changed, 162 insertions, 0 deletions
diff --git a/frontend/src/components/Board/TetrisBoard.vue b/frontend/src/components/Board/TetrisBoard.vue
new file mode 100644
index 0000000..d1f6557
--- /dev/null
+++ b/frontend/src/components/Board/TetrisBoard.vue
@@ -0,0 +1,162 @@
+<script setup lang="ts">
+import { onMounted, ref, type Ref } from 'vue';
+import { mergeTetrominoWithBoard, tetrominoCollidesWithBoard } from '@/util/tetris';
+import { type Tetromino, Tetrominoes } from '@/model/tetrominoes';
+import { type Board } from '@/model/board';
+
+const renderBoard: Ref<Board> = ref([[]]);
+let gameBoard: Board = [
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
+]
+let currentTetromino: Tetromino | null = null;
+let nextTetromino: Ref<Tetromino | undefined> = ref();
+let gameInterval: ReturnType<typeof setInterval>;
+let gameInProgress = false;
+
+function updateRender() {
+ if (currentTetromino) {
+ renderBoard.value = mergeTetrominoWithBoard(gameBoard, currentTetromino);
+ } else {
+ renderBoard.value = gameBoard;
+ }
+}
+
+const tick = () => {
+ if (!currentTetromino) {
+ currentTetromino = { ...nextTetromino.value! };
+ if (tetrominoCollidesWithBoard(gameBoard, currentTetromino)) {
+ clearInterval(gameInterval);
+ alert('Game over!');
+ return;
+ }
+
+ nextTetromino.value = Object.values(Tetrominoes)[Math.floor(Math.random() * 7)];
+ }
+
+ updateRender();
+
+ if (tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, row: currentTetromino.row + 1})) {
+ gameBoard = mergeTetrominoWithBoard(gameBoard, currentTetromino);
+ currentTetromino = null;
+ return;
+ }
+
+ ++currentTetromino.row;
+};
+
+function startGame() {
+ gameInterval = setInterval(tick, 250);
+ nextTetromino.value = Object.values(Tetrominoes)[Math.floor(Math.random() * 7)];
+ gameInProgress = true;
+}
+
+document.addEventListener('keydown', event => {
+ event.preventDefault();
+ if (currentTetromino && event.key === 'r') {
+ if (!tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, rotation: (currentTetromino.rotation + 1) % 4})) {
+ currentTetromino!.rotation = (currentTetromino!.rotation + 1) % 4;
+ }
+
+ updateRender();
+ }
+ if (currentTetromino && ['ArrowLeft', 'h'].includes(event.key)) {
+ if (!tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, col: currentTetromino.col - 1})) {
+ --currentTetromino.col;
+ }
+
+ updateRender();
+ }
+ if (currentTetromino && ['ArrowRight', 'l'].includes(event.key)) {
+ if (!tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, col: currentTetromino.col + 1})) {
+ ++currentTetromino.col;
+ }
+
+ updateRender();
+ }
+ if (currentTetromino && ['ArrowDown', 'j'].includes(event.key)) {
+ if (!tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, row: currentTetromino.row + 1})) {
+ ++currentTetromino.row;
+ }
+
+ updateRender();
+ }
+});
+</script>
+
+<template>
+ <main>
+ <h1>Tetris</h1>
+ <button v-if="!gameInProgress" @click="startGame">Start game</button>
+ <div class="cols">
+ <div class="tetris-board">
+ <div v-for="(row, rowIndex) of renderBoard" class="tetris-row">
+ <div v-for="(col, colIndex) of row" class="tetris-cell"
+ :style="{ 'background-color': col ? `rgb(${Tetrominoes[col].color})` : 'white' }">
+
+ </div>
+ </div>
+ </div>
+ <div>
+ <p>Next up</p>
+ <div class="next-tetromino">
+ <div v-for="(row, rowIndex) of nextTetromino?.shapes[0]" class="tetris-row">
+ <div v-for="(col, colIndex) of row" class="tetris-cell"
+ :style="{ 'background-color': col ? `rgb(${nextTetromino?.color})` : 'white' }">
+
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ </main>
+</template>
+
+<style scoped>
+.cols {
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+}
+
+.tetris-board {
+ width: calc(32px * 10);
+ height: calc(32px * 20);
+ border: 1px solid #dfdfdf;
+}
+
+.next-tetromino {
+ width: calc(32px * 4);
+ height: calc(32px * 4);
+}
+
+.tetris-row {
+ display: grid;
+ grid-template-columns: repeat(10, 1fr);
+}
+
+.tetris-cell {
+ width: 30px;
+ height: 30px;
+ border: 1px solid #dfdfdf;
+}
+</style> \ No newline at end of file