even more clean up but now it's very pretty

main
Francesco Baldino 2 years ago
parent a7ba4ac043
commit 48a904e932

@ -2,13 +2,12 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue</title> <title>PHC Math Qwordle</title>
<link href="https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Material+Icons|Material+Icons+Outlined" rel="stylesheet">
</head> </head>
<body> <body style="background-color: #0f172a">
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>

@ -10,7 +10,8 @@
}, },
"dependencies": { "dependencies": {
"sass": "^1.55.0", "sass": "^1.55.0",
"vue": "^3.2.37" "vue": "^3.2.37",
"vue-router": "^4.1.5"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^3.1.0", "@vitejs/plugin-vue": "^3.1.0",

@ -5,10 +5,12 @@ specifiers:
sass: ^1.55.0 sass: ^1.55.0
vite: ^3.1.0 vite: ^3.1.0
vue: ^3.2.37 vue: ^3.2.37
vue-router: ^4.1.5
dependencies: dependencies:
sass: 1.55.0 sass: 1.55.0
vue: 3.2.40 vue: 3.2.40
vue-router: 4.1.5_vue@3.2.40
devDependencies: devDependencies:
'@vitejs/plugin-vue': 3.1.0_vite@3.1.4+vue@3.2.40 '@vitejs/plugin-vue': 3.1.0_vite@3.1.4+vue@3.2.40
@ -102,6 +104,10 @@ packages:
'@vue/compiler-dom': 3.2.40 '@vue/compiler-dom': 3.2.40
'@vue/shared': 3.2.40 '@vue/shared': 3.2.40
/@vue/devtools-api/6.4.4:
resolution: {integrity: sha512-Ku31WzpOV/8cruFaXaEZKF81WkNnvCSlBY4eOGtz5WMSdJvX1v1WWlSMGZeqUwPtQ27ZZz7B62erEMq8JDjcXw==}
dev: false
/@vue/reactivity-transform/3.2.40: /@vue/reactivity-transform/3.2.40:
resolution: {integrity: sha512-HQUCVwEaacq6fGEsg2NUuGKIhUveMCjOk8jGHqLXPI2w6zFoPrlQhwWEaINTv5kkZDXKEnCijAp+4gNEHG03yw==} resolution: {integrity: sha512-HQUCVwEaacq6fGEsg2NUuGKIhUveMCjOk8jGHqLXPI2w6zFoPrlQhwWEaINTv5kkZDXKEnCijAp+4gNEHG03yw==}
dependencies: dependencies:
@ -566,6 +572,15 @@ packages:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true dev: true
/vue-router/4.1.5_vue@3.2.40:
resolution: {integrity: sha512-IsvoF5D2GQ/EGTs/Th4NQms9gd2NSqV+yylxIyp/OYp8xOwxmU8Kj/74E9DTSYAyH5LX7idVUngN3JSj1X4xcQ==}
peerDependencies:
vue: ^3.2.0
dependencies:
'@vue/devtools-api': 6.4.4
vue: 3.2.40
dev: false
/vue/3.2.40: /vue/3.2.40:
resolution: {integrity: sha512-1mGHulzUbl2Nk3pfvI5aXYYyJUs1nm4kyvuz38u4xlQkLUn1i2R7nDbI4TufECmY8v1qNBHYy62bCaM+3cHP2A==} resolution: {integrity: sha512-1mGHulzUbl2Nk3pfvI5aXYYyJUs1nm4kyvuz38u4xlQkLUn1i2R7nDbI4TufECmY8v1qNBHYy62bCaM+3cHP2A==}
dependencies: dependencies:

@ -1 +1,146 @@
["MINUS","ZEROS","ROUND","THIRD","PROOF","EVENT","MATHS","CUBIC","MONAD","GAUSS","AXIOM","PRISM","CHORD","ANGLE","LITRE","MILES","VALUE","TABLE","HALVE","EXACT","LOCUS","QUART","ARRAY","SOLID","KITES","TRIAL","EMPTY","FIFTH","NINTH","DEPTH","BOOLE","FACES","TENTH","SLOPE","SIXTH","DELTA","SIGMA","LINES","EULER","FIFTY","SCALE","RULER","INDEX","OUNCE","FIRST","SPACE","MONIC","POINT","RIGHT","DIGIT","VALID","TIMES","KLEIN","TWICE","SIXES","METRE","STONE","BAYES","SEVEN","PLANE","CLOCK","THREE","CHAOS","ACUTE","UNITS","HARDY","COUNT","FORTY","TORUS","WIDTH","LOGIC","WILES","POWER","EIGHT","RANGE","PRIME","MODAL","TREND","THETA","CURVE","SIXTY","ERROR","TALLY","PLATO","UNION","KILOS","GRAPH","RATIO","CUBES","DOZEN","EQUAL","ALPHA","NODES","GROUP","ERDOS","KAPPA","LIMIT","ADDED","HERTZ","LATEX","CONIC","RINGS","BOUND","ROOTS","BOREL","ALEPH","SURDS","HELIX","APPEL","EDGES","GAMMA","POLAR","GODEL","FIELD","TUPLE","POLYA","BASIS","UPPER","LEMMA","PROVE","ORDER","SIDES","HASSE","SMALL","BRACE","KNUTH","AREAS","IDEAL","SPACE","DENSE","MODEL","LOWER","OVALS","NOISE","SOLVE","BAIRE","CHAIN","JULIA","FOCUS","KNOTS","DIRAC","HOLES","NORMS","REALS"] [
"MINUS",
"ZEROS",
"ROUND",
"THIRD",
"PROOF",
"EVENT",
"MATHS",
"CUBIC",
"MONAD",
"GAUSS",
"AXIOM",
"PRISM",
"CHORD",
"ANGLE",
"LITRE",
"MILES",
"VALUE",
"TABLE",
"HALVE",
"EXACT",
"LOCUS",
"QUART",
"ARRAY",
"SOLID",
"KITES",
"TRIAL",
"EMPTY",
"FIFTH",
"NINTH",
"DEPTH",
"BOOLE",
"FACES",
"TENTH",
"SLOPE",
"SIXTH",
"DELTA",
"SIGMA",
"LINES",
"EULER",
"FIFTY",
"SCALE",
"RULER",
"INDEX",
"OUNCE",
"FIRST",
"SPACE",
"MONIC",
"POINT",
"RIGHT",
"DIGIT",
"VALID",
"TIMES",
"KLEIN",
"TWICE",
"SIXES",
"METRE",
"STONE",
"BAYES",
"SEVEN",
"PLANE",
"CLOCK",
"THREE",
"CHAOS",
"ACUTE",
"UNITS",
"HARDY",
"COUNT",
"FORTY",
"TORUS",
"WIDTH",
"LOGIC",
"WILES",
"POWER",
"EIGHT",
"RANGE",
"PRIME",
"MODAL",
"TREND",
"THETA",
"CURVE",
"SIXTY",
"ERROR",
"TALLY",
"PLATO",
"UNION",
"KILOS",
"GRAPH",
"RATIO",
"CUBES",
"DOZEN",
"EQUAL",
"ALPHA",
"NODES",
"GROUP",
"ERDOS",
"KAPPA",
"LIMIT",
"ADDED",
"HERTZ",
"LATEX",
"CONIC",
"RINGS",
"BOUND",
"ROOTS",
"BOREL",
"ALEPH",
"SURDS",
"HELIX",
"APPEL",
"EDGES",
"GAMMA",
"POLAR",
"GODEL",
"FIELD",
"TUPLE",
"POLYA",
"BASIS",
"UPPER",
"LEMMA",
"PROVE",
"ORDER",
"SIDES",
"HASSE",
"SMALL",
"BRACE",
"KNUTH",
"AREAS",
"IDEAL",
"SPACE",
"DENSE",
"MODEL",
"LOWER",
"OVALS",
"NOISE",
"SOLVE",
"BAIRE",
"CHAIN",
"JULIA",
"FOCUS",
"KNOTS",
"DIRAC",
"HOLES",
"NORMS",
"REALS"
]

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

@ -1,280 +1,8 @@
<script> <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> </script>
<template> <template>
<div class="page" <router-view />
: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> </template>
<style lang="scss"> <style lang="scss">
@ -325,88 +53,4 @@
background-color: var(--background-color); background-color: var(--background-color);
text-shadow: 1px 1px #000000; 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> </style>

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

Before

Width:  |  Height:  |  Size: 496 B

@ -16,14 +16,16 @@
} }
}, },
share() { share() {
let output = "PHC/Qwordle"; let output = "PHC/Math qwordle";
output += " " + (this.won ? this.lastAttempt : "X") + "/6\n"; output += " " + (this.won ? this.lastAttempt : "X") + "/6\n\n";
for(let i = 0; i < this.lastAttempt; i++) { for(let i = 0; i < this.lastAttempt; i++) {
output += " "
for(let j = 0; j < 5; j++) { for(let j = 0; j < 5; j++) {
output += this.stateToSymbol(this.state[i][j].state); output += this.stateToSymbol(this.state[i][j].state);
} }
output += '\n'; output += '\n';
} }
output += `\nhttps://lab.phc.dm.unipi.it/math-qwordle/${this.$route.params.id}`
navigator.clipboard.writeText(output).then(() => { navigator.clipboard.writeText(output).then(() => {
this.$emit('share', null); this.$emit('share', null);
}); });
@ -36,20 +38,36 @@
<div class="panel"> <div class="panel">
<div class="overlay"> <div class="overlay">
<h1>{{won ? 'Successo!' : 'Oh no...'}}</h1> <h1>{{won ? 'Successo!' : 'Oh no...'}}</h1>
Le parole da indovinare erano {{secretWords[0]}} e {{secretWords[1]}} Le parole da indovinare erano:
<div class="footer"> <div class="secret-words">
<div class="button" @click="$emit('reset', $event)"> <div class="secret-word">
Riprova <div v-for="l in secretWords[0]" class="cell">
<div class="material-icons"> <div class="letter">
refresh {{l}}
</div>
</div>
</div>
<div class="secret-word">
<div v-for="l in secretWords[1]" class="cell">
<div class="letter">
{{l}}
</div>
</div>
</div> </div>
</div> </div>
<div class="footer">
<div class="button" @click="share()"> <div class="button" @click="share()">
Condividi Condividi
<div class="material-icons"> <div class="material-icons">
share share
</div> </div>
</div> </div>
<div class="button" @click="$emit('reset', $event)">
Riprova
<div class="material-icons">
refresh
</div>
</div>
</div> </div>
<div class="close"> <div class="close">
<div class="material-icons" @click="$emit('close', $event)"> <div class="material-icons" @click="$emit('close', $event)">
@ -86,10 +104,46 @@
font-weight: 600; font-weight: 600;
font-size: 1.15rem; font-size: 1.15rem;
.secret-words {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.secret-word {
display: flex;
gap: 0.2rem;
.cell {
width: 3.5rem;
height: 3.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: .25rem;
border-width: 2px;
border-style: solid;
font-weight: 800;
font-size: 2.2rem;
border-color: var(--set-cell-border);
background-color: var(--background-color);
.letter {
display: flex;
align-items: center;
justify-content: center;
transform: translateY(0.1em);
}
}
}
.footer { .footer {
width: 100%; width: 100%;
display: flex; display: flex;
flex-direction: row-reverse; align-items: center;
justify-content: center;
gap: 1rem; gap: 1rem;
} }

@ -1,46 +0,0 @@
<script>
export default {
data() {
return {
}
},
props: ["message"],
methods: {
stateToSymbol(state) {
if(state === 'correct-full') {
return '🟩';
} else if(state === 'correct-half') {
return '🟢';
} else if (state === 'misplaced-full') {
return '🟨';
} else if (state === 'misplaced-half') {
return '🟡';
} else {
return '⬜';
}
},
share() {
let output = "PHC/Qwordle";
output += " " + (this.won ? this.lastAttempt : "X") + "/6\n";
for(let i = 0; i < this.lastAttempt; i++) {
for(let j = 0; j < 5; j++) {
output += this.stateToSymbol(this.state[i][j].state);
}
output += '\n';
}
navigator.clipboard.writeText(output).then(() => {
this.$emit('copied', null);
});
}
}
};
</script>
<template>
<div class="popup">
{{message}}
</div>
</template>
<style scoped lang="scss">
</style>

@ -1,5 +1,5 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import './style.css'
import App from './App.vue' import App from './App.vue'
import router from './router'
createApp(App).mount('#app') createApp(App).use(router).mount('#app')

@ -0,0 +1,31 @@
import { createRouter, createWebHistory } from 'vue-router'
import Game from '../views/Game.vue'
const routes = [
{
path: '/:id',
name: 'Game',
component: Game
},
{
path: '/',
redirect: to => {
let id = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let charactersLength = characters.length;
for ( let i = 0; i < 8; i++ ) {
id += characters.charAt(Math.floor(Math.random() *
charactersLength));
}
return { path: `/${id}` }
},
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
export default router

@ -0,0 +1,376 @@
<script>
import GuessGrid from '../components/GuessGrid.vue'
import Keyboard from '../components/Keyboard.vue';
import Overlay from '../components/Overlay.vue';
export default {
data() {
return {
// Constants
LETTER_ANIMATION_DURATION: 0.3,
SHAKE_ANIMATION_DURATION: 0.3,
// Variables
words: [],
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-words.json');
this.words = await req.json();
req = await fetch('./math-pairs.json');
this.pairs = await req.json();
this.setupGame();
},
components: {
GuessGrid,
Keyboard,
Overlay,
},
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;
let gameId = 0;
for(let i = 0; i < this.$route.params.id.length; i++) {
let mod = this.$route.params.id.charCodeAt(i);
gameId += mod * mod * mod * mod;
}
this.secretWords = this.pairs[gameId % 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;
},
async newGame() {
let newId = this.randomId(8);
await this.$router.replace({params: {id: newId}});
this.setupGame();
},
randomId(length) {
let result = '';
let characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let charactersLength = characters.length;
for ( let i = 0; i < length; i++ ) {
result += characters.charAt(Math.floor(Math.random() *
charactersLength));
}
return result;
},
addPopUp(message) {
let id = this.randomId(16);
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" />
/
Math qwordle
</div>
</div>
<div v-if="this.gridState !== null" class="interface">
<div class="header">
<div>Math 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="newGame()"
@share="addPopUp('copied to clipboard!')"
/>
<div v-for="popup in popups" class="popup">
{{popup}}
</div>
</div>
</template>
<style lang="scss">
.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: 4rem;
.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>
Loading…
Cancel
Save