Skip to content

Instantly share code, notes, and snippets.

@daveknights
Created January 15, 2023 23:09
Show Gist options
  • Save daveknights/ed7d035999e706a9a20ac64a5f7aca1e to your computer and use it in GitHub Desktop.
Save daveknights/ed7d035999e706a9a20ac64a5f7aca1e to your computer and use it in GitHub Desktop.
An online game with increasing levels of difficulty.
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@800&family=Spectral:wght@800&display=swap');
:root {
--brick-pattern: repeating-linear-gradient(90deg, #fad79e, #fad79e 2px, #cca065 2px, #cca065 38px, #965c00 38px, #965c00 40px);
--brick-border-bottom: solid 1px #965c00;
--brick-border-top: solid 2px #fad79e;
--engraved-colour: #aa9668;
--game-bg: linear-gradient(#2487dc, #90c6ef 600px, #dfc07c 600px, #e3bc81);
--gold-grad: linear-gradient(yellow, gold 80%, sandybrown);
--red-grad: linear-gradient(pink, red 80%, darkred);
--sand: #dfc07c;
--selected: #00cc99;
--sky-shadow: #0467bb;
}
* {
border: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
}
h1 {
color: gold;
font-family: 'Spectral', serif;
font-family: 'Orbitron', sans-serif;
font-size: 4rem;
margin: 0 0 40px 2px;
position: absolute;
text-align: center;
text-shadow: 1px 2px 1px black, -2px -1px 1px white;
top: 30px;
}
h2 {
color: black;
font-family: 'Orbitron', sans-serif;
letter-spacing: 1px;
margin-bottom: 2rem;
text-align: center;
width: 100%;
}
.game-bg {
background: var(--game-bg);
height: 800px;
margin: 50px auto 0;
padding: 30px;
position: relative;
width: 600px;
}
.game-bg,
.pyramid-container,
.bonus-container,
.block-stack {
align-items: center;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.level-container {
margin-bottom: 20px;
}
.row {
display: flex;
gap: 20px;
justify-content: center;
margin-bottom: 20px;
}
.level,
.bonus-level {
align-items: center;
background: var(--sand);
border-bottom: var(--brick-border-bottom);
border-top: var(--brick-border-top);
box-shadow: 0 2px 5px var(--sky-shadow);
display: flex;
height: 90px;
justify-content: center;
}
.level {
width: 90px;
}
.playable,
.unlocked {
cursor: pointer;
}
.playable,
.bonus-level p {
color: var(--engraved-colour);
font-size: 3rem;
font-weight: bold;
height: 90px;
line-height: 90px;
position: relative;
text-align: center;
text-shadow: 0px 1px 0px rgba(255,255,255,0.7), 0px -1px 0px rgba(0,0,0,0.7);
width: 100%;
}
.locked {
border-radius: 0 0 1px 1px;
height: 12px;
margin-top: 8px;
outline: solid 14px var(--engraved-colour);
position: relative;
width: 6px;
}
.locked::before {
border-radius: 50%;
content: '';
display: block;
height: 15px;
left: -4px;
outline: solid 4px var(--engraved-colour);
position: absolute;
top: -22px;
width: 14px;
}
.playable::before,
.playable::after,
.star::before,
.star::after {
border-left: 15px solid transparent;
border-right: 15px solid transparent;
bottom: 8px;
content: '';
display: block;
right: 5px;
position: absolute;
}
.playable::before {
border-bottom: 15px solid rgba(0,0,0,0.7);
right: 6px;
}
.playable::after {
border-bottom: 15px solid var(--engraved-colour);
}
.star::before {
border-bottom: 15px solid #a1772d;
right:7px;
}
.star::after {
border-bottom: 15px solid gold;
}
.bonus-level {
position: relative;
width: 420px;
}
.bonus-level .locked {
display: inline-block;
margin-bottom: 6px;
margin-right: 40px;
}
.button-nav,
.difficulty {
margin-bottom: auto;
margin-top: 100px;
display: flex;
justify-content: space-between;
width: 100%;
}
.difficulty button,
.button-nav button {
background: var(--gold-grad);
border: solid 5px white;
border-radius: 10px;
box-shadow: 0 0 5px black, inset 0 0 1px 1px black;
cursor: pointer;
font-weight: bold;
font-size: 1.5rem;
height: 60px;
width: 150px;
}
button span {
display: inline-block;
}
.return-span {
margin-right: 9px;
}
.next-span {
margin-left: 12px;
}
.difficulty {
flex-wrap: wrap;
margin-top: 40px;
width: 420px;
}
.difficulty button {
color: grey;
}
.difficulty button:hover {
color: black;
}
.difficulty button.selected {
border-color: var(--selected);
color: black;
cursor: auto;
}
.pyramid-container {
padding-bottom: 130px;
}
.bonus-container {
padding-bottom: 150px;
}
.block-line {
background: var(--brick-pattern);
border-bottom: var(--brick-border-bottom);
border-top: var(--brick-border-top);
cursor: pointer;
height: 30px;
width: 300px;
}
.capstone {
align-items: center;
display: flex;
flex-direction: column;
height: 60px;
width: 80px;
}
.capstone::before,
.capstone::after {
background: var(--brick-pattern);
border-bottom: var(--brick-border-bottom);
border-top: var(--brick-border-top);
content: '';
height: 30px;
}
.capstone::before {
width: 40px;
}
.capstone::after {
width: 80px;
}
.message {
color: white;
bottom: 80px;
font-family: 'Spectral', serif;
font-family: 'Orbitron', sans-serif;
font-size: 3.5rem;
position: absolute;
text-align: center;
-webkit-text-stroke: 2px;
-webkit-text-stroke-color: black;
width: 420px;
}
.message::before {
content: '';
display: block;
height: 440px;
width: 420px;
}
progress[value]::-webkit-progress-bar {
background-color: #eee;
border-radius: 2px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset;
}
progress.time-indicator {
-webkit-appearance: none;
appearance: none;
border: solid 1px #555;
color: gold;
height: 20px;
width: 100%;
}
progress.time-indicator.final-second {
color: red;
}
progress.time-indicator::-webkit-progress-value {
background: var(--gold-grad);
}
progress.time-indicator::-moz-progress-bar {
background: var(--gold-grad);
}
progress.time-indicator.final-second::-webkit-progress-value {
background: var(--red-grad);
}
progress.time-indicator.final-second::-moz-progress-bar {
background: var(--gold-grad);
}
import React, { Fragment, useEffect, useState, useCallback } from 'react';
import Levels from './Levels';
import Pyramid from './Pyramid';
import BonusLevel from './BonusLevel';
import TimeIndicator from './TimeIndicator';
import Navigation from './Navigation';
import DisplayMessage from './DisplayMessage';
import './App.css';
const initialBlockWidths = ['320', '280', '240', '200', '160', '120'];
const initialBonusParts = [...Array(11).keys()];
const levels = {
1: {widths: [...initialBlockWidths]},
2: {widths: [...initialBlockWidths, '360']},
3: {widths: [...initialBlockWidths, '360', '400']},
4: {widths: [...initialBlockWidths, '360', '400', '440']},
5: {widths: [...initialBlockWidths, '360', '400', '440', '480']},
6: {widths: [...initialBlockWidths, '360', '400', '440', '480', '520']},
};
const initialLevelData = {};
// Populate initialLevelData
for (const level in levels) {
parseInt(level) === 1 ? initialLevelData[level] = {locked: false, star: false} :
initialLevelData[level] = {locked: true, star: false};
}
initialLevelData['bonus'] = {locked: true, star: false}
let timeClass = '';
let gameTimer;
let gameTime = 0;
const App = () => {
const [difficulty, setDifficulty] = useState('normal');
const [level, setLevel] = useState(1);
const [completedLevels, setCompletedLevels] = useState([]);
const [blockWidths, setBlockWidths] = useState(initialBlockWidths);
const [bonusParts, setBonusParts] = useState(initialBonusParts);
const [timeIndicatorValue, setTimeIndicatorValue] = useState(0);
const [levelData, setLevelData] = useState(initialLevelData);
const [playing, setPlaying] = useState(false);
const [isSolved, setIsSolved] = useState(false);
const [message, setMessage] = useState('');
const stopTimer = () => {
clearInterval(gameTimer);
gameTimer = null;
setMessage(`Time's Up!`);
};
const upDateTimeIndicator = useCallback((chosenLevel, widthArray = null) => {
const levelDifficulty = difficulty === 'normal' ? 1 : 1.5;
const bonusDifficulty = difficulty === 'normal' ? 1.5 : 2;
const partsLength = chosenLevel === 'bonus' ? bonusParts.length * bonusDifficulty : widthArray.length * levelDifficulty;
gameTime += 0.1;
setTimeIndicatorValue((gameTime / partsLength) * 100);
if (!timeClass && gameTime > partsLength - 1) {
timeClass = ' final-second';
}
gameTime >= partsLength && stopTimer();
}, [bonusParts.length, difficulty]);
const startTimer = useCallback((chosenLevel, widthArray) => {
!gameTimer && (gameTimer = setInterval(() => upDateTimeIndicator(chosenLevel, widthArray), 100));
}, [upDateTimeIndicator]);
const showLevels = () => {
setPlaying(false);
setIsSolved(false);
stopTimer();
setMessage('');
};
const handleSelectLevel = e => {
if (e.target.classList.contains('playable') || e.target.classList.contains('unlocked')) {
setPlaying(true);
if (e.target.textContent === 'Bonus Level') {
startLevel('bonus')
} else if (e.target.parentNode.classList.contains('level')) {
startLevel(parseInt(e.target.textContent));
}
}
};
const handleSetDifficulty = e => setDifficulty(e.target.dataset.difficulty);
const shuffle = widths => widths
.map(value => ({ value, sort: Math.random() }))
.sort((a, b) => a.sort - b.sort)
.map(({ value }) => value).slice(0, widths.length);
const startLevel = useCallback((chosenLevel, action = null) => {
let widthArray = null;
setMessage('');
if (action === 'next level' && (chosenLevel !== 'bonus' && chosenLevel !== 6)) {
setLevel(level => level + 1);
setBlockWidths(shuffle(levels[level + 1].widths));
chosenLevel = chosenLevel + 1;
widthArray = levels[level + 1].widths;
} else if ((action === 'next level' && level === 6) || chosenLevel === 'bonus') {
chosenLevel = 'bonus';
setLevel('bonus');
setBonusParts(bP => shuffle(bP));
} else {
setLevel(chosenLevel);
setBlockWidths(shuffle(levels[chosenLevel].widths));
widthArray = levels[chosenLevel].widths
}
timeClass = '';
startTimer(chosenLevel, widthArray);
}, [startTimer, level]);
useEffect (() => {
let sorted;
const updateLevelData = () => {
gameTimer && setMessage('Well Done!');
clearInterval(gameTimer);
if (gameTimer && !completedLevels.includes(level)) {
const updatedLevelData = {...levelData};
setCompletedLevels(current => [...current, level]);
updatedLevelData[level].locked === true && (updatedLevelData[level].locked = false);
updatedLevelData[level].star === false && (updatedLevelData[level].star = true);
if (level < 6 && updatedLevelData[level + 1].locked === true) {
updatedLevelData[level + 1].locked = false;
}
if (completedLevels.length + 1 === 6) {
updatedLevelData['bonus'].locked = false;
}
setLevelData(updatedLevelData);
}
};
const checkBonusSorting = () => {
for (const part of bonusParts) {
if (bonusParts[part] > bonusParts[part + 1]) {
sorted = false;
break;
} else {
sorted = true;
}
}
sorted && updateLevelData();
gameTimer && setIsSolved(sorted);
isSolved && (gameTimer = null);
};
const checkSorting = () => {
for (const width in blockWidths) {
if (parseInt(width) > 0 && parseInt(blockWidths[width]) !== parseInt(blockWidths[parseInt(width) - 1]) + 40) {
sorted = false;
break;
} else {
sorted = true;
}
}
sorted && updateLevelData();
gameTimer && setIsSolved(sorted);
isSolved && (gameTimer = null);
}
playing && (level !== 'bonus' ? checkSorting() : checkBonusSorting());
}, [completedLevels, isSolved, levelData, bonusParts, level, blockWidths, playing]);
const handleNavBtnClick = e => {
gameTime = 0;
setIsSolved(false);
switch (e.target.dataset.action) {
case 'to levels':
showLevels();
break;
case 'try again':
startLevel(level);
break;
case 'next level':
startLevel(level, e.target.dataset.action);
break;
default:
break;
}
};
return (
<Fragment>
<h1>PYR▲MIDS</h1>
{!playing && <Levels
levelData={levelData}
handleSelectLevelFn={handleSelectLevel}
difficulty={difficulty}
handleSetDifficultyFn={handleSetDifficulty} />}
{playing && (!gameTimer || isSolved) && <Navigation
showNext={completedLevels.includes(level) && level !== 'bonus' }
handleNavBtnClickFn={handleNavBtnClick} />}
{(playing && level !== 'bonus') && <Pyramid
blockWidths={blockWidths}
setBlockWidths={setBlockWidths}
isSolved={isSolved} />}
{(playing && level === 'bonus') && <BonusLevel
parts={bonusParts}
setBonusParts={setBonusParts} />}
{message && <DisplayMessage message={message} />}
{playing && <TimeIndicator
timeClass={timeClass}
value={timeIndicatorValue} />}
</Fragment>
);
}
export default App;
import React from "react";
const blockLine = props => <div className="block-line" style={{width: props.width + 'px'}}></div>;
export default blockLine;
.strip {
cursor: pointer;
display: flex;
height: 32px;
justify-content: center;
}
.part-0 {
overflow: hidden;
position: relative;
width: 160px;
}
.part-0::before {
background: #e3bc81;
border-radius: 50%;
content: '';
display: block;
height: 50px;
margin-top: 10px;
position: absolute;
width: 114px;
}
.part-0::after {
border-top: 32px solid #cca065;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-radius: 50% 50% 0 0;
content: '';
position: absolute;
width: 32px;
}
.part-1 {
border-left: 25px solid transparent;
border-right: 25px solid transparent;
width: 168px;
}
.part-1::before {
border-top: 15px solid #cca065;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-radius: 0 0 15px 15px;
content: '';
position: absolute;
width: 22px;
z-index: 1;
}
.part-1::after,
.part-2::after,
.part-3::after {
background: #e3bc81;
content: '';
height: 32px;
position: absolute;
width: 114px;
}
.part-1,
.part-3 {
border-bottom: 32px solid #cca065;
}
.part-2,
.part-4 {
border-bottom: 32px solid #965c00;
}
.part-2 {
border-left: 19px solid transparent;
border-right: 19px solid transparent;
width: 208px;
}
.part-3 {
border-left: 20px solid transparent;
border-right: 20px solid transparent;
width: 248px;
}
.part-4 {
border-left: 15px solid transparent;
border-right: 15px solid transparent;
width: 276px;
}
.part-4::after,
.part-5::after {
border-top: 32px solid #e3bc81;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
content: '';
position: absolute;
width: 94px;
}
.part-5 {
background: #cca065;
border-radius: 2px 2px 20px 20px;
height: 32px;
width: 276px;
}
.part-5::after {
border-radius: 0 0 50% 50%;
width: 74px;
}
.part-6 {
border-top: 32px solid #965c00;
border-left: 28px solid transparent;
border-right: 28px solid transparent;
position: relative;
width: 250px;
}
.part-6::after {
border-bottom: 32px solid #e3bc81;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
content: '';
position: absolute;
top: -32px;
width: 42px;
}
.part-7 {
border-top: 32px solid #cca065;
border-left: 32px solid transparent;
border-right: 32px solid transparent;
position: relative;
width: 194px;
}
.part-7::after {
background: #e3bc81;
border-radius: 80px 80px 0 0;
content: '';
height: 32px;
position: absolute;
top: -32px;
width: 180px;
}
.part-8 {
background: #cca065;
border-radius: 50px 50px 0 0;
width: 224px;
}
.part-9 {
background: #e3bc81;
border-radius: 25px 25px 0 0;
position: relative;
width: 268px;
}
.part-9::before,
.part-9::after {
background: #e3bc81;
bottom: 0;
border-radius: 50px 50px 0 0;
border-top: solid 1px #965c00;
content: '';
height: 10px;
position: absolute;
width: 100px;
}
.part-9::before {
left: -16px;
}
.part-9::after {
right: -16px;
}
.part-10 {
background: #cca065;
display: flex;
position: relative;
width: 300px;
}
.part-10::before,
.part-10::after {
background: linear-gradient(90deg, #e3bc81, #e3bc81 22px, #965c00 22px, #965c00 26px,
#e3bc81 26px, #e3bc81 48px, #965c00 48px, #965c00 52px,
#e3bc81 52px, #e3bc81 74px, #965c00 74px, #965c00 78px,
#e3bc81 78px, #e3bc81);
border-bottom: solid 1px #965c00;
content: '';
height: 32px;
width: 100px;
}
.part-10::after {
margin-left: auto;
}
import React from "react";
import './BonusLevel.css';
import { ReactSortable } from "react-sortablejs";
const bonusLevel = props => (
<div className="bonus-container">
<ReactSortable className="block-stack" list={props.parts} setList={props.setBonusParts}>
{props.parts.map(part => <div key={part} className={`strip part-${part}`}></div>)}
</ReactSortable>
</div>
);
export default bonusLevel;
const capstone = () => <div className="capstone"></div>;
export default capstone;
import React from "react";
const displayMessage = props => <strong className="message">{props.message}</strong>;
export default displayMessage;
import React from "react";
const levels = props => {
const data = props.levelData;
return (
<div className="level-container" onClick={props.handleSelectLevelFn}>
<div className="row">
<div className="level">
{data['1'].locked ? <div className="locked"></div> : <p className={`playable${data['1'].star ? ' star' : ''}`}>{Object.keys(data)[0]}</p>}
</div>
</div>
<div className="row">
<div className="level">
{data['2'].locked ? <div className="locked"></div> : <p className={`playable${data['2'].star ? ' star' : ''}`}>{Object.keys(data)[1]}</p>}
</div>
<div className="level">
{data['3'].locked ? <div className="locked"></div> : <p className={`playable${data['3'].star ? ' star' : ''}`}>{Object.keys(data)[2]}</p>}
</div>
</div>
<div className="row">
<div className="level">
{data['4'].locked ? <div className="locked"></div> : <p className={`playable${data['4'].star ? ' star' : ''}`}>{Object.keys(data)[3]}</p>}
</div>
<div className="level">
{data['5'].locked ? <div className="locked"></div> : <p className={`playable${data['5'].star ? ' star' : ''}`}>{Object.keys(data)[4]}</p>}
</div>
<div className="level">
{data['6'].locked ? <div className="locked"></div> : <p className={`playable${data['6'].star ? ' star' : ''}`}>{Object.keys(data)[5]}</p>}
</div>
</div>
<div className={`bonus-level${!data['bonus'].locked ? ' unlocked' : ''}`}>
<p className={`${!data['bonus'].locked ? 'playable' : ''}${data['bonus'].star ? ' star' : ''}`}>{data['bonus'].locked && <span className="locked"></span>}Bonus Level</p>
</div>
<div className="difficulty" onClick={props.handleSetDifficultyFn}>
<h2>Difficulty:</h2>
<button type="button" {...(props.difficulty === 'easy' && {className: "selected"})} data-difficulty="easy">Easy</button>
<button type="button" {...(props.difficulty === 'normal' && {className: "selected"})} data-difficulty="normal">Normal</button>
</div>
</div>
);
};
export default levels;
import React from "react";
const navigation = props => (
<div className="button-nav" onClick={props.handleNavBtnClickFn}>
<button type="button" data-action="to levels"><span className="return-span">&#9664;</span>Return</button>
<button type="button" data-action="try again">Try again</button>
{props.showNext && <button type="button" data-action="next level">Next<span className="next-span">&#9654;</span></button>}
</div>
);
export default navigation;
import React from "react";
import { ReactSortable } from "react-sortablejs";
import BlockLine from "./BlockLine";
import Capstone from "./Capstone";
const pyramid = props => (
<div className="pyramid-container">
{props.isSolved && <Capstone />}
<ReactSortable className="block-stack" list={props.blockWidths} setList={props.setBlockWidths}>
{props.blockWidths.map(width => <BlockLine key={width} width={width} />)}
</ReactSortable>
</div>
);
export default pyramid;
import React from "react";
const timeIndicator = props => <progress className={`time-indicator${props.timeClass}`} max="100" value={props.value}></progress>;
export default timeIndicator;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment