From 9f83ab70cfd5f2c41a23593f8ee5065b1015e242 Mon Sep 17 00:00:00 2001
From: Leonardo Bishop <me@leonardobishop.com>
Date: Sat, 4 Nov 2023 15:25:06 +0000
Subject: Add frontend

---
 frontend/src/components/Board/TetrisBoard.vue | 162 ++++++++++++++++++++++++++
 1 file changed, 162 insertions(+)
 create mode 100644 frontend/src/components/Board/TetrisBoard.vue

(limited to 'frontend/src/components/Board/TetrisBoard.vue')

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
-- 
cgit v1.2.3-70-g09d2

