You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

413 lines
9.8 KiB
Vue

<script>
import GuessGrid from './components/GuessGrid.vue'
import Keyboard from './components/Keyboard.vue';
import Overlay from './components/Overlay.vue';
import PopUp from './components/PopUp.vue';
export default {
data() {
return {
// Constants
LETTER_ANIMATION_DURATION: 0.3,
SHAKE_ANIMATION_DURATION: 0.3,
// Variables
pairs: [],
listen: true,
gridState: null,
rowsAnimations: null,
keyboardMask: null,
currentAttempt: 0,
currentIndex: 0,
secretWords: null,
gameOver: false,
won: false,
overlayVisible: false,
popups: {}
}
},
async created() {
window.addEventListener('keyup', (e) => {
if (e.key === 'Backspace') {
this.keyPress('Delete');
} else if (e.key === 'Delete' || e.key === 'Enter') {
this.keyPress(e.key);
} else if (e.key.match(/^[a-zA-Z]$/)) {
this.keyPress(e.key.toUpperCase())
}
});
let req = await fetch('./math-pairs.json');
this.pairs = await req.json();
this.setupGame();
},
components: {
GuessGrid,
Keyboard,
Overlay,
PopUp,
},
methods: {
setupGame() {
this.gridState = Array.from({length: 6}, () => {
return Array.from({length: 5}, () => {
return {
letter: '',
state: 'unset'
};
});
});
this.rowsAnimations = new Array(6).fill(false);
this.keyboardMask = Object.fromEntries(
Array.from({length: 26}, (v, i) => [
String.fromCharCode(i + 65), 'unused'
])
);
this.keyboardMask['Enter'] = 'unused';
this.keyboardMask['Delete'] = 'unused';
console.log(this.keyboardMask);
this.currentAttempt = 0;
this.currentIndex = 0;
this.secretWords = this.pairs[Math.floor(Math.random() * this.pairs.length)];
console.log(this.secretWords);
console.log(this.secretWords[0]);
console.log(this.secretWords[1]);
this.gameOver = false;
this.won = false;
this.overlayVisible = false;
},
randomId() {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let charactersLength = characters.length;
for ( let i = 0; i < 16; i++ ) {
result += characters.charAt(Math.floor(Math.random() *
charactersLength));
}
return result;
},
addPopUp(message) {
let id = this.randomId();
this.popups[id] = message;
setTimeout(() => delete this.popups[id], 10*1000);
},
checkWin(matches, oneWordMatch) {
if (!oneWordMatch) {
return false;
}
for(let i = 0; i < 5; i++) {
if (matches[i][0] === -1 || !matches[i][1]) {
return false;
}
}
return true;
},
resolveGame(won) {
this.won = won;
this.gameOver = true;
this.overlayVisible = true;
},
keyPress(event) {
if (!this.listen) {
return;
}
if (event === "Delete") {
if(this.currentIndex > 0) {
this.currentIndex -= 1;
this.gridState[this.currentAttempt][this.currentIndex].letter = '';
this.gridState[this.currentAttempt][this.currentIndex].state = 'unset';
}
} else if (event == "Enter") {
if(this.currentIndex === 5) {
// Validate Word
let currentWord = "";
for(let i = 0; i < 5; i++) {
currentWord += this.gridState[this.currentAttempt][i].letter;
}
if(!this.words.includes(currentWord)) {
// Invalid attempt, signal error
this.listen = false;
this.rowsAnimations[this.currentAttempt] = true;
setTimeout(() => {
this.listen = true;
this.rowsAnimations[this.currentAttempt] = false;
}, this.SHAKE_ANIMATION_DURATION * 1000);
return;
}
// Calculate result
let wordsFlags = [ this.secretWords[0].split(''), this.secretWords[1].split('') ];
let matches = [ [-1,false], [-1,false], [-1,false], [-1,false], [-1,false] ];
for(let i = 0; i < 5; i++) {
let letter = this.gridState[this.currentAttempt][i].letter;
for(let j = 0; j < 2; j++) {
if (wordsFlags[j][i] === letter) {
wordsFlags[j][i] = null;
matches[i] = [j, true];
}
}
}
for(let i = 0; i < 5; i++) {
let letter = this.gridState[this.currentAttempt][i].letter;
for(let j = 0; j < 2; j++) {
for(let k = 0; k < 5; k++) {
if(wordsFlags[j][k] === letter) {
wordsFlags[j][k] = null;
matches[i] = [j, false];
}
}
}
}
let oneWordMatch = true;
for(let i = 0; i < 5; i++) {
for(let j = 0; j < 5; j++) {
if (matches[i][0] == 0 && matches[j][0] == 1) {
oneWordMatch = false;
}
}
}
// Update grid to tease result
for(let i = 0; i < 5; i++) {
if (matches[i][0] !== -1 && matches[i][1]) {
this.gridState[this.currentAttempt][i].state = `correct-${oneWordMatch ? 'full' : 'half'}`;
} else if (matches[i][0] !== -1 && !matches[i][1]) {
this.gridState[this.currentAttempt][i].state = `misplaced-${oneWordMatch ? 'full' : 'half'}`;
} else {
this.gridState[this.currentAttempt][i].state = 'wrong';
}
}
this.listen = false;
// Resolve
setTimeout(() => {
for(let i = 0; i < 5; i++) {
if (matches[i][0] !== -1 && matches[i][1]) {
if(this.keyboardMask[this.gridState[this.currentAttempt][i].letter] === 'unused' ||
this.keyboardMask[this.gridState[this.currentAttempt][i].letter] === 'misplaced') {
this.keyboardMask[this.gridState[this.currentAttempt][i].letter] = 'correct';
}
} else if (matches[i][0] !== -1 && !matches[i][1]) {
if(this.keyboardMask[this.gridState[this.currentAttempt][i].letter] === 'unused') {
this.keyboardMask[this.gridState[this.currentAttempt][i].letter] = 'misplaced';
}
} else {
if(this.keyboardMask[this.gridState[this.currentAttempt][i].letter] === 'unused') {
this.keyboardMask[this.gridState[this.currentAttempt][i].letter] = 'wrong';
}
}
}
this.currentAttempt += 1;
this.currentIndex = 0;
if (this.checkWin(matches, oneWordMatch)) {
this.resolveGame(true);
} else if (this.currentAttempt === 6) {
this.resolveGame(false);
}
this.listen = true;
}, this.LETTER_ANIMATION_DURATION * 1000 * 5);
}
} else {
if (this.currentIndex < 5) {
this.gridState[this.currentAttempt][this.currentIndex].letter = event;
this.gridState[this.currentAttempt][this.currentIndex].state = 'set';
this.currentIndex += 1;
}
}
}
}
};
</script>
<template>
<div class="page"
:style="`--letter-animation-duration: ${LETTER_ANIMATION_DURATION}s; --shake-animation-duration: ${SHAKE_ANIMATION_DURATION}s`"
>
<div class="toolbar">
<div class="logo">
<img src="logo-circuit-board.svg" alt="logo" />
/
Qwordle
</div>
</div>
<div v-if="this.gridState !== null" class="interface">
<div class="header">
<div>Qwordle</div>
<div class="end">
<div class="material-icons-outlined">info</div>
<div v-if="gameOver" class="material-icons-outlined" @click="overlayVisible = true">military_tech</div>
</div>
</div>
<GuessGrid :state="gridState" :rows-animations="rowsAnimations"/>
<Keyboard :mask="keyboardMask" @add="keyPress($event)" :listen="true"/>
</div>
<Overlay v-if="overlayVisible"
:secret-words="secretWords"
:state="gridState"
:won="won"
:last-attempt="currentAttempt"
@close="overlayVisible = false"
@reset="setupGame()"
@share="addPopUp('copied to clipboard!')"
/>
<div v-for="popup in popups" class="popup">
{{popup}}
</div>
</div>
</template>
<style lang="scss">
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
--text-color: #ffffff;
--background-color: #0f172a;
--border-color: #475569;
--button-color: #475569;
--button-highlight: #596679;
--popup-color: #323c4b;
--keyboard-unused: #475569;
--keyboard-wrong: #1e293b;
--keyboard-correct: #22c55e;
--keyboard-misplaced: #eab308;
--unset-cell-border: #475569;
--set-cell-border: #ffffff;
--correct-cell-dark: #22c55e;
--correct-cell-light: #4ade80;
--misplaced-cell-dark: #eab308;
--misplaced-cell-light: #facc15;
--wrong-cell: #334155;
}
* {
box-sizing: border-box;
}
html, body, #app, .page {
min-height: 100vh;
}
body {
margin: 0;
color: var(--text-color);
background-color: var(--background-color);
text-shadow: 1px 1px #000000;
}
.page {
position: relative;
.toolbar {
padding: 1rem .75rem 1rem 1rem;
display: flex;
align-items: center;
border-bottom: 2px solid var(--border-color);
.logo {
display: flex;
align-items: center;
img {
max-height: 2.5rem;
}
}
}
.interface {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
padding: 6rem;
.header {
width: 18rem;
display: flex;
justify-content: space-between;
font-size: 1.5rem;
font-weight: 600;
.end {
display: flex;
gap: 0.5rem;
}
}
}
}
.material-icons, .material-icons-outlined, .button {
cursor: pointer;
}
@keyframes appear {
0% {
transform: translate(-50%, 0);
opacity: 0;
}
20% {
transform: translate(-50%, -3rem);
opacity: 1;
}
80% {
transform: translate(-50%, -3rem);
opacity: 1;
}
100% {
transform: translate(-50%, 0);
opacity: 0;
}
}
.popup {
position: absolute;
bottom: 0%;
left: 50%;
transform: translateX(-50%);
opacity: 0;
animation: ease-in-out appear 5s;
background-color: var(--popup-color);
border-radius: 20px;
padding: 1rem 3rem;
font-weight: 600;
}
</style>