by Maurici Abad
at Fontys ICT
I'm Maurici Abad Gutierrez,
an exchange student
from Barcelona, Spain
Portfolio:
LinkedIn:
I'm organizer of the largest hackathon in Barcelona.
We promote technology among students and create a great community.
Hackathon:
Student organization:
Web App
Server:
Client:
Tools:
💬: Follow guide
👨💻: At your own
Find a partner
Install this:
https://github.com/{username}/{repo-name}
git clone https://github.com/{username}/{repo-name}.git
cd {repo-name}
code .
npm init
npm install express
{
"name": "wmole",
"version": "1.0.0",
"description": "",
"main": "src/app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node src/app.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mauriciabad/wmole.git"
},
"author": "Maurici Abad Gutierrez",
"license": "ISC",
"bugs": {
"url": "https://github.com/mauriciabad/wmole/issues"
},
"homepage": "https://github.com/mauriciabad/wmole#readme",
"dependencies": {
"express": "^4.17.1"
}
}
Change this values
📁 dist
📁 css
📘 main.css
📁 img
🖼️ mole.svg
🖼️ bunny.svg
📁 js
📒 index.js
📙 index.html
📁 src
📒 app.js
📄 .gitignore
📄 package.json
mole.svg
bunny.svg
/* - - - - Initialize variables - - - - */
const express = require('express');
const app = express();
const http = require('http').createServer(app);
// Your code goes here
/* - - - - Server logic - - - - */
app.use(express.static('dist'));
const port = process.env.PORT || 3000;
http.listen(port, () => {
console.log(`listening on port ${port}`);
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Wack A Mole Multiplayer</title>
<link rel="stylesheet" href="css/main.css">
<link href="https://fonts.googleapis.com/css?family=Luckiest+Guy&display=swap" rel="stylesheet">
</head>
<body class="body--play">
<div class="score"><span id="score">0</span><span class="score__small"> pts.</span></div>
<div class="grid">
<!-- Repeat this 9 times changing the data-holenumber: { -->
<div class="hole" data-holenumber="0">
<img class="hole__img" data-content="mole" src="img/mole.svg" alt="🦔">
<img class="hole__img" data-content="bunny" src="img/bunny.svg" alt="🐇">
</div>
<!-- } -->
</div>
<script src="js/index.js"></script>
</body>
</html>
/* - - - - - - Shared - - - - - - */
:root {
--green: #73B46E;
--brown: #957863;
--brown-dark: #403532;
}
body {
color: var(--brown-dark);
line-height: 1;
margin: 0 auto;
font-family: 'Luckiest Guy', cursive;
background-color: var(--green);
box-sizing: border-box;
}
/* - - - - - - Play - - - - - - */
.body--play{
height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: space-evenly;
flex-direction: column;
user-select: none;
}
/* --- Grid & Holes --- */
.grid {
--wide-size: calc(100vh - 8rem);
--narrow-size: 100vw;
width: var(--wide-size);
height: var(--wide-size);
max-width: var(--narrow-size);
max-height: var(--narrow-size);
display: grid;
grid-template: 1fr 1fr 1fr / 1fr 1fr 1fr;
gap: 1rem;
padding: 1rem;
}
.hole {
position: relative;
background-color: #251F1D;
border: solid 0.5rem var(--brown);
border-radius: 100%;
transition: filter 300ms ease-out;
box-shadow:
inset 0 0vh 0 0 rgba(149, 120, 99, 0.1),
inset 0 2vh 0 0 rgba(149, 120, 99, 0.1),
inset 0 4vh 0 0 rgba(149, 120, 99, 0.1),
inset 0 6vh 0 0 rgba(149, 120, 99, 0.1),
inset 0 8vh 0 0 rgba(149, 120, 99, 0.1),
inset 0 10vh 0 0 rgba(149, 120, 99, 0.1);
}
.hole__img {
opacity: 0;
transform: scale(0.5) translateY(15%);
transition: transform 100ms ease-out, opacity 100ms ease-out;
will-change: transform, opacity;
z-index: 1;
position: absolute;
top: 0; right: 0; bottom: 0; left: 0;
padding: 0.75rem;
object-fit: contain;
}
.hole__img--active {
opacity: 1;
transform: scale(1) translateY(0);
}
.hole__img--smashed {
transform: scale(0.5) translateY(15%) rotate(720deg) !important;
}
/* --- Scores --- */
.score {
font-size: 6rem;
color: var(--brown-dark);
line-height: 1;
}
.score__small { font-size: 0.6667em; }
/* - - - - - - Scoreboard - - - - - - */
.body--scoreboard { padding: 2rem; }
table { font-size: 7vw; margin: auto; border-collapse: collapse; }
thead { border-bottom: solid 0.125em var(--brown-dark); }
td,th { padding: 0.5rem; }
td:nth-child(1), th:nth-child(1) { text-align: left; padding-right: 2rem; }
td:nth-child(2), th:nth-child(2) { text-align: right; padding-left: 2rem; }
.fade{
position: fixed;
bottom: 0; left: 0; right: 0;
height: 10rem;
background: linear-gradient(to bottom, transparent, var(--green));
z-index: 1;
}
/* - - - - - - Index - - - - - - */
.body--index {
height: 100vh;
max-width: 40rem;
padding: 1rem;
overflow: hidden;
display: grid;
grid-template: 1fr 2fr / 1fr;
gap: 1rem;
user-select: none;
}
a {
background: var(--brown);
box-shadow: 0 1rem 0 var(--brown-dark);
margin-bottom: 1rem;
border-radius: 1rem;
font-size: 3rem;
color: #fff;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
}
npm run start
npm install socket.io
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Wack A Mole Multiplayer</title>
<link rel="stylesheet" href="css/index.css">
<link href="https://fonts.googleapis.com/css?family=Luckiest+Guy&display=swap" rel="stylesheet">
</head>
<body>
<div class="score"><span id="score">0</span><span class="score__small"> pts.</span></div>
<div class="grid">
<!-- Repeat this 9 times changing the data-holenumber: { -->
<div class="hole" data-holenumber="0">
<img class="hole__img" data-content="mole" src="img/mole.svg" alt="🦔">
<img class="hole__img" data-content="bunny" src="img/bunny.svg" alt="🐇">
</div>
<!-- } -->
</div>
<script src="/socket.io/socket.io.js"></script>
<script src="js/index.js"></script>
</body>
</html>
/* - - - - Initialize variables - - - - */
const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
/* - - - - Socket.io logic - - - - */
io.on('connection', (socket) => {
console.log(`${socket.id} is connected`);
socket.on('name', (name) => {
console.log(`${socket.id} name is ${name}`);
socket.emit('log', `Welcome ${name}! :D`);
});
socket.on('disconnect', () => {
console.log(`${socket.id} disconected`);
});
});
/* - - - - Server logic - - - - */
app.use(express.static('dist'));
const port = process.env.PORT || 3000;
http.listen(port, () => {
console.log(`listening on port ${port}`);
});
const socket = io();
socket.emit('name', 'Lucas');
socket.on('log', (data) => {
console.log(data);
});
src/app.js
dist/js/index.js
log
name
/* - - - Display UI changes - - - */
function displaySmash(holeNumber) {
let holeActiveContentElement = document.querySelector(`[data-holeNumber='${holeNumber}'] > .hole__img--active`);
if (holeActiveContentElement) {
holeActiveContentElement.classList.remove('hole__img--active');
holeActiveContentElement.classList.add('hole__img--smashed');
setTimeout(() => {
holeActiveContentElement.classList.remove('hole__img--smashed');
}, 100);
}
}
function displaySpawn({holeNumber, content, duration}) {
let holeContentElement = document.querySelector(`[data-holeNumber='${holeNumber}'] > [data-content='${content}']`);
holeContentElement.classList.add('hole__img--active');
setTimeout(() => {
holeContentElement.classList.remove('hole__img--active');
}, duration - 100);
}
function displayScore(score) {
document.querySelector('#score').textContent = score;
}
Help code to change UI
https://dashboard.heroku.com/apps/{app}/deploy/github
{app} = Your app name
https://dashboard.heroku.com/apps/{app}/deploy/github
{app} = Your app name
https://dashboard.heroku.com/apps/{app}/deploy/github
{app} = Your app name
https://dashboard.heroku.com/apps/{app}/deploy/github
{app} = Your app name
https://dashboard.heroku.com/apps/{app}/deploy/github
{app} = Your app name
https://dashboard.heroku.com/apps/{app}/logs
{app} = Your app name
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Wack A Mole Multiplayer</title>
<link rel="stylesheet" href="css/main.css">
<link href="https://fonts.googleapis.com/css?family=Luckiest+Guy&display=swap" rel="stylesheet">
</head>
<body class="body--index">
<a href="scoreboard.html">Scoreboard</a>
<a href="play.html">Play</a>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Wack A Mole Multiplayer</title>
<link rel="stylesheet" href="css/main.css">
<link href="https://fonts.googleapis.com/css?family=Luckiest+Guy&display=swap" rel="stylesheet">
</head>
<body class="body--scoreboard">
<table>
<thead>
<tr><th>Player</th><th>Points</th></tr>
</thead>
<tbody id="scoreboard">
</tbody>
</table>
<div class="fade"></div>
<script src="/socket.io/socket.io.js"></script>
<script src="js/scoreboard.js" defer></script>
</body>
</html>
dist/index.html
dist/scoreboard.html
[
{ "username": "Anna", "score": 13 },
{ "username": "Emma", "score": 10 },
{ "username": "Finn", "score": 6 },
{ "username": "Tess", "score": 2 }
]
Example scoreboard JSON
[
{ "username": "Anna", "score": 13 },
{ "username": "Emma", "score": 10 },
{ "username": "Finn", "score": 6 },
{ "username": "Tess", "score": 2 }
]
Example scoreboard JSON
const socket = io('/play');
const username = prompt('Enter a username', '');
if(username) socket.emit('username', username);
document.querySelectorAll('.hole').forEach(hole => {
hole.addEventListener('mousedown', smash);
hole.addEventListener('touchstart', smash);
});
socket.on('spawn', displaySpawn);
socket.on('score', displayScore);
function smash(event) {
event.preventDefault();
let holeNumber = event.currentTarget.dataset.holenumber;
socket.emit('smash', holeNumber);
displaySmash(holeNumber);
}
/* - - - Display UI changes - - - */
function displaySmash(holeNumber) {
let holeActiveContentElement = document.querySelector(`[data-holeNumber='${holeNumber}'] > .hole__img--active`);
if (holeActiveContentElement) {
holeActiveContentElement.classList.remove('hole__img--active');
holeActiveContentElement.classList.add('hole__img--smashed');
setTimeout(() => {
holeActiveContentElement.classList.remove('hole__img--smashed');
}, 100);
}
}
function displaySpawn({holeNumber, content, duration}) {
let holeContentElement = document.querySelector(`[data-holeNumber='${holeNumber}'] > [data-content='${content}']`);
holeContentElement.classList.add('hole__img--active');
setTimeout(() => {
holeContentElement.classList.remove('hole__img--active');
}, duration - 100);
}
function displayScore(score) {
document.querySelector('#score').textContent = score;
}
/* - - - - Initialize variables - - - - */
const express = require('express');
const app = express();
const http = require('http').createServer(app);
const io = require('socket.io')(http);
const ioPlay = io.of('/play');
const ioScoreboard = io.of('/scoreboard');
const game = {
holes: [
{ content: 'none', smashedBy: [] },
{ content: 'none', smashedBy: [] },
{ content: 'none', smashedBy: [] },
{ content: 'none', smashedBy: [] },
{ content: 'none', smashedBy: [] },
{ content: 'none', smashedBy: [] },
{ content: 'none', smashedBy: [] },
{ content: 'none', smashedBy: [] },
{ content: 'none', smashedBy: [] },
],
players: {
// 'exampleUserId': { score: 0, username: 'Player' },
},
points: { mole: +1, bunny: -3, none: 0 }
};
/* - - - - Game spawning logic - - - - */
setSpawner();
setSpawner();
setSpawner();
function setSpawner() {
let holeNumber = Math.floor(Math.random() * 9);
let content = (Math.random() > 0.3) ? 'mole' : 'bunny';
let duration = 300 + Math.random() * 900;
if(game.holes[holeNumber].content === 'none'){
game.holes[holeNumber] = { content, smashedBy: [] };
ioPlay.emit('spawn', {holeNumber, content, duration});
setTimeout(() => {
game.holes[holeNumber] = { content: 'none', smashedBy: [] };
}, duration);
}
setTimeout(setSpawner, 100 + Math.random() * 3100);
}
/* - - - - Player logic - - - - */
ioPlay.on('connection', (socket) => {
console.log(`${socket.id} joined the game`);
game.players[socket.id] = {
score: 0,
username: 'Player',
};
updateScoreboard();
socket.on('username', (username) => {
game.players[socket.id].username = username;
updateScoreboard();
});
socket.on('smash', (holeNumber) => {
let hole = game.holes[holeNumber];
if(!hole.smashedBy.includes(socket.id)){
hole.smashedBy.push(socket.id);
let oldScore = game.players[socket.id].score;
let newScore = Math.max(0, oldScore + game.points[hole.content]);
if(newScore !== oldScore) {
game.players[socket.id].score = newScore;
socket.emit('score', newScore);
updateScoreboard();
}
}
});
socket.on('disconnect', () => {
console.log(`${socket.id} left the game (${game.players[socket.id].username})`);
delete game.players[socket.id];
updateScoreboard();
});
});
/* - - - - Scoreboard logic - - - - */
ioScoreboard.on('connection', (socket) => {
console.log(`${socket.id} joined the scoreboard`);
socket.emit('score', toSortedArray(game.players));
socket.on('disconnect', () => {
console.log(`${socket.id} left the scoreboard`);
});
});
function updateScoreboard() {
ioScoreboard.emit('score', toSortedArray(game.players));
}
function toSortedArray(players) {
return Object.values(players).sort((a, b) => b.score - a.score);
}
/* - - - - Server logic - - - - */
app.use(express.static('dist'));
const port = process.env.PORT || 3000;
http.listen(port, () => {
console.log(`listening on port ${port}`);
});
const scoreboardElement = document.getElementById('scoreboard');
const socket = io('/scoreboard');
socket.on('score', (players) => {
scoreboardElement.innerHTML = players.reduce((html, player) => {
return `${html}<tr><td>${player.username}</td><td>${player.score}</td></tr>`
}, '');
});