diff options
| author | Leonardo Bishop <me@leonardobishop.com> | 2023-11-04 21:28:11 +0000 |
|---|---|---|
| committer | Leonardo Bishop <me@leonardobishop.com> | 2023-11-04 21:28:11 +0000 |
| commit | 7d8cca54e548d2a85287fd2325db88f2697be55a (patch) | |
| tree | 3daa82c6a709363f2f08d9f875d849e4babe1f8f /frontend/src | |
| parent | e1633f3348ff7fc5e9131eeae2f2feba09f04838 (diff) | |
Add more shit
Diffstat (limited to 'frontend/src')
| -rw-r--r-- | frontend/src/components/Home.vue | 15 | ||||
| -rw-r--r-- | frontend/src/components/TetrisBoard.vue (renamed from frontend/src/components/Board/TetrisBoard.vue) | 174 | ||||
| -rw-r--r-- | frontend/src/main.ts | 6 | ||||
| -rw-r--r-- | frontend/src/router/index.ts | 15 | ||||
| -rw-r--r-- | frontend/src/views/HomeView.vue | 5 | ||||
| -rw-r--r-- | frontend/src/views/HostView.vue | 75 | ||||
| -rw-r--r-- | frontend/src/views/JoinView.vue | 60 | ||||
| -rw-r--r-- | frontend/src/views/PlayView.vue | 10 |
8 files changed, 304 insertions, 56 deletions
diff --git a/frontend/src/components/Home.vue b/frontend/src/components/Home.vue new file mode 100644 index 0000000..3dc0341 --- /dev/null +++ b/frontend/src/components/Home.vue @@ -0,0 +1,15 @@ +<template> + <h1>Tetris</h1> + <p>Pick an option</p> + <ul> + <li> + <RouterLink to="/play">Play solo game</RouterLink> + </li> + <li> + <RouterLink to="/host">Host networked game</RouterLink> + </li> + <li> + <RouterLink to="/join">Join networked game</RouterLink> + </li> + </ul> +</template>
\ No newline at end of file diff --git a/frontend/src/components/Board/TetrisBoard.vue b/frontend/src/components/TetrisBoard.vue index 7528ad7..4e5911b 100644 --- a/frontend/src/components/Board/TetrisBoard.vue +++ b/frontend/src/components/TetrisBoard.vue @@ -1,9 +1,14 @@ <script setup lang="ts"> -import { ref, type Ref } from 'vue'; +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], @@ -33,6 +38,7 @@ 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'); @@ -74,6 +80,11 @@ const tick = () => { updateRender(); if (tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, row: currentTetromino.row + 1 })) { + if (!freezeTick) { + freezeTick = true; + return; + } + gameBoard = mergeTetrominoWithBoard(gameBoard, currentTetromino); currentTetromino = null; ++elapsedBlocks; @@ -86,7 +97,7 @@ const tick = () => { gameBoard.unshift([0, 0, 0, 0, 0, 0, 0, 0, 0, 0]); } } - + switch (clears) { case 1: points.value += 40; @@ -106,11 +117,14 @@ const tick = () => { gameSpeed = Math.max(150, gameSpeed - (gameSpeed > 300 ? 75 : 10)); rescheduleTickTimer(); } + freezeTick = false; return; + } else { + freezeTick = false; } ++currentTetromino.row; - + }; function startGame() { @@ -119,55 +133,108 @@ function startGame() { nextTetrominoes.value = randomiseNextTetrominoes(); gameInProgress = true; notificationMessage.value = ''; - + updateRender(); rescheduleTickTimer(); } -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; - } +function rotateTetronimo() { + if (currentTetromino && !tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, rotation: (currentTetromino.rotation + 1) % 4 })) { + currentTetromino!.rotation = (currentTetromino!.rotation + 1) % 4; + } + + updateRender(); +} - updateRender(); +function moveTetrominoLeft() { + if (currentTetromino && !tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, col: currentTetromino.col - 1 })) { + --currentTetromino.col; } - if (currentTetromino && ['ArrowLeft', 'h'].includes(event.key)) { - if (!tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, col: currentTetromino.col - 1 })) { - --currentTetromino.col; - } - updateRender(); + updateRender(); +} + +function moveTetrominoRight() { + if (currentTetromino && !tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, col: currentTetromino.col + 1 })) { + ++currentTetromino.col; } - if (currentTetromino && ['ArrowRight', 'l'].includes(event.key)) { - if (!tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, col: currentTetromino.col + 1 })) { - ++currentTetromino.col; - } - updateRender(); + updateRender(); +} + +function dropTetromino() { + let rowsDropped = 0; + while (currentTetromino && !tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, row: currentTetromino.row + 1 })) { + ++currentTetromino.row; + ++rowsDropped; } - if (currentTetromino && ['ArrowDown', 'j', 'z'].includes(event.key)) { - if (!tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, row: currentTetromino.row + 1 })) { - ++currentTetromino.row; - } - updateRender(); + if (rowsDropped === 0) { + return; } - if (currentTetromino && ['ArrowUp', 'k', 'c'].includes(event.key)) { - let rowsDropped = 0; - while (!tetrominoCollidesWithBoard(gameBoard, { ...currentTetromino, row: currentTetromino.row + 1 })) { - ++currentTetromino.row; - ++rowsDropped; + + 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 (rowsDropped === 0) { - return; + 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(); } - - points.value += rowsDropped + 1; + if (currentTetromino && ['ArrowUp', 'k', 'c'].includes(event.key)) { + dropTetromino(); + } + }); +} + +if (props.networked) { + onMounted(() => { + setInterval(processInputQueue, 100); + }); +} - tick(); +onMounted(() => { + if (!gameInProgress) { + startGame(); } }); </script> @@ -177,7 +244,8 @@ document.addEventListener('keydown', event => { <h1>Tetris</h1> <div class="cols"> <div> - <div v-if="notificationMessage" class="notification-banner" :style="{ 'background-color': notificationBg, 'color': notificationFg}"> + <div v-if="notificationMessage" class="notification-banner" + :style="{ 'background-color': notificationBg, 'color': notificationFg }"> {{ notificationMessage }} </div> <div class="tetris-board"> @@ -200,28 +268,30 @@ document.addEventListener('keydown', event => { </div> </div> </div> - + <h2>Score</h2> <h3> {{ points }} </h3> - <h2>Controls</h2> - <p> - To move left and right, use the arrow keys or <kbd>H</kbd> and <kbd>L</kbd>. - </p> + <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 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 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> + <p> + To do a hard drop, use the arrow up key or <kbd>K</kbd>. + </p> + </div> </div> </div> diff --git a/frontend/src/main.ts b/frontend/src/main.ts index a4db798..7f87494 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -3,11 +3,15 @@ import './assets/main.css' import { createApp } from 'vue' import App from './App.vue' import router from './router' -import TetrisBoard from './components/Board/TetrisBoard.vue' +import TetrisBoard from './components/TetrisBoard.vue' +import Home from './components/Home.vue' +import QrcodeVue from 'qrcode.vue' const app = createApp(App) app.use(router) app.component('TetrisBoard', TetrisBoard) +app.component('Home', Home) +app.component('QRCode', QrcodeVue) app.mount('#app') diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index e58491f..fe575db 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -8,6 +8,21 @@ const router = createRouter({ path: '/', name: 'home', component: HomeView + }, + { + path: '/host', + name: 'host', + component: () => import('../views/HostView.vue') + }, + { + path: '/play', + name: 'play', + component: () => import('../views/PlayView.vue') + }, + { + path: '/join/:id', + name: 'join', + component: () => import('../views/JoinView.vue') } ] }) diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue index 09d319e..0b9fe32 100644 --- a/frontend/src/views/HomeView.vue +++ b/frontend/src/views/HomeView.vue @@ -1,10 +1,9 @@ <script setup lang="ts"> -import type TetrisBoard from '../components/Board/TetrisBoard.vue'; - +import type Home from '../components/Home.vue'; </script> <template> <main> - <TetrisBoard></TetrisBoard> + <Home></Home> </main> </template> diff --git a/frontend/src/views/HostView.vue b/frontend/src/views/HostView.vue new file mode 100644 index 0000000..5b29289 --- /dev/null +++ b/frontend/src/views/HostView.vue @@ -0,0 +1,75 @@ +<script setup lang="ts"> +import { ref, type Ref } from 'vue'; +import type TetrisBoard from '../components/TetrisBoard.vue'; + +let started = ref(false) +let waitingForCode = ref(true) +let code = ref('') +let url = ref(''); +let numClients = ref(0); +let inputQueue: Ref<Array<string>> = ref([]); + +let socket: WebSocket; + +function startGame() { + if (!socket) { + return; + } + socket.send(JSON.stringify({ + action: 'start', + sessionId: code.value, + })); +} + +fetch(`${import.meta.env.VITE_BACKEND_BASE_URL}/session`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + } +).then(async (response) => { + const data: any = await response.json(); + code.value = data.id; + url.value = `${import.meta.env.VITE_FRONTEND_BASE_URL}/join/${data.id}`; + waitingForCode.value = false; + openSocket(data.id); +}); + +function openSocket(id: string) { + socket = new WebSocket(`${import.meta.env.VITE_BACKEND_WS_URL}`); + + socket.onopen = () => { + socket.send(JSON.stringify({ + action: 'host', + sessionId: id, + })); + }; + socket.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.action === 'join') { + numClients.value = data.clients; + } else if (data.action === 'state' && data.state === 'playing') { + started.value = true; + } else if (data.action === 'move') { + inputQueue.value.push(data.move); + } + }; +} +</script> + +<template> + <main> + <template v-if="waitingForCode"> + <h1>Requesting session...</h1> + </template> + <template v-if="!started && !waitingForCode"> + <h1>Join this game</h1> + <QRCode size=500 level="M" :value="url"></QRCode> + <h2><a :href="url">{{ url }}</a></h2> + <p>Connected clients: {{ numClients }}</p> + <button @click="startGame">Start game</button> + </template> + <TetrisBoard v-if="started" :networked="true" :input-queue="inputQueue"></TetrisBoard> + </main> +</template> diff --git a/frontend/src/views/JoinView.vue b/frontend/src/views/JoinView.vue new file mode 100644 index 0000000..abfbd1a --- /dev/null +++ b/frontend/src/views/JoinView.vue @@ -0,0 +1,60 @@ +<script setup lang="ts"> +import { ref } from 'vue'; +import { useRoute } from 'vue-router'; + +const route = useRoute(); +const sessionId = route.params.id; + +const joiningGame = ref(true); +const connected = ref(false); +const gameState = ref(''); + +const socket = new WebSocket(`${import.meta.env.VITE_BACKEND_WS_URL}`); +socket.onopen = () => { + socket.send(JSON.stringify({ + action: 'join', + sessionId: sessionId, + })); +}; +socket.onmessage = (event) => { + const data = JSON.parse(event.data); + if (data.action === 'state') { + gameState.value = data.state; + joiningGame.value = false; + connected.value = true; + } +}; + +function sendMove(move: string) { + socket.send(JSON.stringify({ + action: 'move', + sessionId: sessionId, + move: move, + })); +} + +function sendLeft() { + sendMove('left'); +} + +function sendRotate() { + sendMove('rotate'); +} + +function sendRight() { + sendMove('right'); +} +</script> + +<template> + <main> + <h1 v-if="joiningGame">Joining game "{{ sessionId }}"...</h1> + <h1 v-if="connected">Connected to "{{ sessionId }}"</h1> + <h1 v-if="gameState === 'waiting'">Waiting for host to start the game...</h1> + <div v-if="gameState === 'playing'"> + <button @click="sendLeft">Left</button> + <button @click="sendRotate">Rotate</button> + <button @click="sendRight">Right</button> + </div> + </main> +</template> diff --git a/frontend/src/views/PlayView.vue b/frontend/src/views/PlayView.vue new file mode 100644 index 0000000..8c00589 --- /dev/null +++ b/frontend/src/views/PlayView.vue @@ -0,0 +1,10 @@ +<script setup lang="ts"> +import type TetrisBoard from '../components/TetrisBoard.vue'; + +</script> + +<template> + <main> + <TetrisBoard :networked="false"></TetrisBoard> + </main> +</template> |
