aboutsummaryrefslogtreecommitdiffstats
path: root/frontend/src/components/TetrisBoard.vue
diff options
context:
space:
mode:
authorLeonardo Bishop <me@leonardobishop.com>2023-11-04 21:28:11 +0000
committerLeonardo Bishop <me@leonardobishop.com>2023-11-04 21:28:11 +0000
commit7d8cca54e548d2a85287fd2325db88f2697be55a (patch)
tree3daa82c6a709363f2f08d9f875d849e4babe1f8f /frontend/src/components/TetrisBoard.vue
parente1633f3348ff7fc5e9131eeae2f2feba09f04838 (diff)
Add more shit
Diffstat (limited to 'frontend/src/components/TetrisBoard.vue')
-rw-r--r--frontend/src/components/TetrisBoard.vue340
1 files changed, 340 insertions, 0 deletions
diff --git a/frontend/src/components/TetrisBoard.vue b/frontend/src/components/TetrisBoard.vue
new file mode 100644
index 0000000..4e5911b
--- /dev/null
+++ b/frontend/src/components/TetrisBoard.vue
@@ -0,0 +1,340 @@
+<script setup lang="ts">
+import { onMounted, ref, type Ref } from 'vue';
+import { mergeTetrominoWithBoard, randomiseNextTetrominoes, tetrominoCollidesWithBoard } from '@/util/tetris';
+import { type Tetromino, allTetrominoes } from '@/model/tetrominoes';
+import { type Board } from '@/model/board';
+
+const props = defineProps({
+ networked: Boolean,
+ inputQueue: Array<string>
+});
+
+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 nextTetrominoes: Ref<Array<Tetromino>> = ref([]);
+let gameInterval: ReturnType<typeof setInterval>;
+let gameSpeed = 1000;
+let elapsedBlocks = 0;
+let gameInProgress = false;
+let freezeTick = false;
+let points = ref(0);
+let notificationMessage = ref('Press start to begin!');
+let notificationBg = ref('\#dfdfdf');
+let notificationFg = ref('black');
+
+function updateRender() {
+ if (currentTetromino) {
+ renderBoard.value = mergeTetrominoWithBoard(gameBoard, currentTetromino);
+ } else {
+ renderBoard.value = gameBoard;
+ }
+}
+
+
+function rescheduleTickTimer() {
+ clearInterval(gameInterval);
+ gameInterval = setInterval(tick, gameSpeed);
+}
+
+const tick = () => {
+ if (!currentTetromino) {
+ let next = nextTetrominoes.value.shift();
+ currentTetromino = { ...next! };
+ currentTetromino.col = 3;
+ if (tetrominoCollidesWithBoard(gameBoard, currentTetromino)) {
+ clearInterval(gameInterval);
+ notificationBg.value = '\#FF4136';
+ notificationFg.value = 'white';
+ notificationMessage.value = 'Game over!';
+ return;
+ }
+
+
+ if (nextTetrominoes.value.length === 0) {
+ nextTetrominoes.value = randomiseNextTetrominoes();
+ }
+ }
+
+ updateRender();
+
+ if (tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, row: currentTetromino.row + 1 })) {
+ if (!freezeTick) {
+ freezeTick = true;
+ return;
+ }
+
+ gameBoard = mergeTetrominoWithBoard(gameBoard, currentTetromino);
+ currentTetromino = null;
+ ++elapsedBlocks;
+
+ let clears = 0;
+ for (let row = 0; row < gameBoard.length; ++row) {
+ if (gameBoard[row].every(cell => cell)) {
+ clears++;
+ gameBoard.splice(row, 1);
+ gameBoard.unshift([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
+ }
+ }
+
+ switch (clears) {
+ case 1:
+ points.value += 40;
+ break;
+ case 2:
+ points.value += 100;
+ break;
+ case 3:
+ points.value += 300;
+ break;
+ case 4:
+ points.value += 1200;
+ break;
+ }
+
+ if (gameSpeed > 150 && elapsedBlocks % 3 === 0) {
+ gameSpeed = Math.max(150, gameSpeed - (gameSpeed > 300 ? 75 : 10));
+ rescheduleTickTimer();
+ }
+ freezeTick = false;
+ return;
+ } else {
+ freezeTick = false;
+ }
+
+ ++currentTetromino.row;
+
+};
+
+function startGame() {
+ elapsedBlocks = 0;
+ gameSpeed = 1000;
+ nextTetrominoes.value = randomiseNextTetrominoes();
+ gameInProgress = true;
+ notificationMessage.value = '';
+
+ updateRender();
+ rescheduleTickTimer();
+}
+
+function rotateTetronimo() {
+ if (currentTetromino && !tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, rotation: (currentTetromino.rotation + 1) % 4 })) {
+ currentTetromino!.rotation = (currentTetromino!.rotation + 1) % 4;
+ }
+
+ updateRender();
+}
+
+function moveTetrominoLeft() {
+ if (currentTetromino && !tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, col: currentTetromino.col - 1 })) {
+ --currentTetromino.col;
+ }
+
+ updateRender();
+}
+
+function moveTetrominoRight() {
+ if (currentTetromino && !tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, col: currentTetromino.col + 1 })) {
+ ++currentTetromino.col;
+ }
+
+ updateRender();
+}
+
+function dropTetromino() {
+ let rowsDropped = 0;
+ while (currentTetromino && !tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, row: currentTetromino.row + 1 })) {
+ ++currentTetromino.row;
+ ++rowsDropped;
+ }
+
+ if (rowsDropped === 0) {
+ return;
+ }
+
+ points.value += rowsDropped + 1;
+
+ tick();
+}
+
+function softDropTetromino() {
+ if (currentTetromino && !tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, row: currentTetromino.row + 1 })) {
+ ++currentTetromino.row;
+ }
+
+ updateRender();
+}
+
+function processInputQueue() {
+ if (!props.networked) {
+ return;
+ }
+
+ if (props.inputQueue!.length === 0) {
+ return;
+ }
+
+ let move = props.inputQueue!.shift();
+ if (move === 'left') {
+ moveTetrominoLeft();
+ } else if (move === 'right') {
+ moveTetrominoRight();
+ } else if (move === 'rotate') {
+ rotateTetronimo();
+ }
+}
+
+if (!props.networked) {
+ document.addEventListener('keydown', event => {
+ event.preventDefault();
+ if (currentTetromino && event.key === 'r') {
+ rotateTetronimo();
+ }
+ if (currentTetromino && ['ArrowLeft', 'h'].includes(event.key)) {
+ moveTetrominoLeft();
+ }
+ if (currentTetromino && ['ArrowRight', 'l'].includes(event.key)) {
+ moveTetrominoRight();
+ }
+ if (currentTetromino && ['ArrowDown', 'j', 'z'].includes(event.key)) {
+ softDropTetromino();
+ }
+ if (currentTetromino && ['ArrowUp', 'k', 'c'].includes(event.key)) {
+ dropTetromino();
+ }
+ });
+}
+
+if (props.networked) {
+ onMounted(() => {
+ setInterval(processInputQueue, 100);
+ });
+}
+
+onMounted(() => {
+ if (!gameInProgress) {
+ startGame();
+ }
+});
+</script>
+
+<template>
+ <main>
+ <h1>Tetris</h1>
+ <div class="cols">
+ <div>
+ <div v-if="notificationMessage" class="notification-banner"
+ :style="{ 'background-color': notificationBg, 'color': notificationFg }">
+ {{ notificationMessage }}
+ </div>
+ <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(${allTetrominoes[col].color})` : 'white' }">
+
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="game-info">
+ <button v-if="!gameInProgress" @click="startGame">Start game</button>
+ <h2>Next up</h2>
+ <div class="next-tetromino">
+ <div v-for="(row, rowIndex) of nextTetrominoes[0]?.shapes[0]" class="tetris-row">
+ <div v-for="(col, colIndex) of row" class="tetris-cell"
+ :style="{ 'background-color': col ? `rgb(${nextTetrominoes[0]?.color})` : 'white' }">
+
+ </div>
+ </div>
+ </div>
+
+ <h2>Score</h2>
+ <h3>
+ {{ points }}
+ </h3>
+
+ <div v-if="!networked">
+ <h2>Controls</h2>
+ <p>
+ To move left and right, use the arrow keys or <kbd>H</kbd> and <kbd>L</kbd>.
+ </p>
+
+ <p>
+ To rotate, use <kbd>R</kbd>.
+ </p>
+
+ <p>
+ To do a soft drop, use the arrow down key or <kbd>J</kbd>.
+ </p>
+
+ <p>
+ To do a hard drop, use the arrow up key or <kbd>K</kbd>.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ </main>
+</template>
+
+<style scoped>
+.cols {
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+}
+
+.game-info {
+ max-width: 250px;
+}
+
+.tetris-board {
+ width: calc(32px * 10);
+ height: calc(32px * 20);
+ border: 1px solid #dfdfdf;
+}
+
+.notification-banner {
+ width: calc(32px * 10 + 2px);
+ height: 22px;
+ text-align: center;
+ position: absolute;
+}
+
+.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