diff options
Diffstat (limited to 'public/projects/js-small-apps/rock-paper-scissors')
8 files changed, 1357 insertions, 0 deletions
diff --git a/public/projects/js-small-apps/rock-paper-scissors/README.md b/public/projects/js-small-apps/rock-paper-scissors/README.md new file mode 100644 index 0000000..834c716 --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/README.md @@ -0,0 +1,11 @@ +# Rock Paper Scissors + +A Javascript implementation of the game. + +## Preview + +You can see a live preview here: https://demo.armandphilippot.com/#rps-game + +## License + +This project is open-source and available under the [MIT License](../LICENSE). diff --git a/public/projects/js-small-apps/rock-paper-scissors/app.js b/public/projects/js-small-apps/rock-paper-scissors/app.js new file mode 100644 index 0000000..581b442 --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/app.js @@ -0,0 +1,115 @@ +import RPSGame from "./lib/class-rps-game.js"; + +function createPreview() { + const preview = document.querySelector(".preview__content"); + const player1Wrapper = document.createElement("div"); + const player2Wrapper = document.createElement("div"); + const vsWrapper = document.createElement("div"); + player1Wrapper.id = "player1Wrapper"; + player2Wrapper.id = "player2Wrapper"; + vsWrapper.textContent = "vs"; + vsWrapper.style.fontWeight = 600; + preview.append(player1Wrapper, vsWrapper, player2Wrapper); +} + +function getUserPreviewId(id) { + let userId; + if (id === "player1-name" || id === "player1-ia") { + userId = "player1Wrapper"; + } else if (id === "player2-name" || id === "player2-ia") { + userId = "player2Wrapper"; + } + return userId; +} + +function fillPreview(target) { + const userId = getUserPreviewId(target.id); + if (userId) { + const dest = document.getElementById(userId); + dest.textContent = target.value; + } +} + +function toggleIABadge(target) { + const userId = getUserPreviewId(target.id); + if (userId) { + const dest = document.getElementById(userId); + target.checked + ? dest.classList.add("ia-badge") + : dest.classList.remove("ia-badge"); + } +} + +function startGame(player1, player2, maxRound) { + const register = document.querySelector(".register"); + const game = document.querySelector(".game"); + const buttons = { + rock: document.querySelector(".btn--rock"), + paper: document.querySelector(".btn--paper"), + scissors: document.querySelector(".btn--scissors"), + newGame: document.querySelector(".btn--new-game"), + }; + const p1Scoring = { + name: document.getElementById("player1username"), + value: document.getElementById("player1score"), + }; + const p2Scoring = { + name: document.getElementById("player2username"), + value: document.getElementById("player2score"), + }; + const messages = document.querySelector(".message-board"); + const players = []; + players.push(player1); + players.push(player2); + + const app = new RPSGame(players, buttons, p1Scoring, p2Scoring, messages); + app.init(); + app.maxRound = maxRound && maxRound !== "0" ? maxRound : ""; + register.style.display = "none"; + game.style.display = "flex"; +} + +function listen() { + const inputP1Name = document.getElementById("player1-name"); + const inputP2Name = document.getElementById("player2-name"); + const checkboxP1IA = document.getElementById("player1-ia"); + const checkboxP2IA = document.getElementById("player2-ia"); + const inputMaxRound = document.getElementById("round-number"); + const registerBtn = document.querySelector(".form__submit"); + + if (inputP1Name.value) fillPreview(inputP1Name); + if (inputP2Name.value) fillPreview(inputP2Name); + if (checkboxP1IA.checked) toggleIABadge(checkboxP1IA); + if (checkboxP2IA.checked) toggleIABadge(checkboxP2IA); + + inputP1Name.addEventListener("keyup", (e) => fillPreview(e.target)); + inputP2Name.addEventListener("keyup", (e) => fillPreview(e.target)); + checkboxP1IA.addEventListener("change", (event) => { + toggleIABadge(event.target); + if (checkboxP2IA.checked && event.target.checked) { + checkboxP2IA.checked = false; + toggleIABadge(checkboxP2IA); + } + }); + checkboxP2IA.addEventListener("change", (event) => { + toggleIABadge(event.target); + if (checkboxP1IA.checked && event.target.checked) { + checkboxP1IA.checked = false; + toggleIABadge(checkboxP1IA); + } + }); + registerBtn.addEventListener("click", (event) => { + event.preventDefault(); + const player1 = { username: inputP1Name.value, ia: checkboxP1IA.checked }; + const player2 = { username: inputP2Name.value, ia: checkboxP2IA.checked }; + const maxRound = inputMaxRound.value; + startGame(player1, player2, maxRound); + }); +} + +function init() { + createPreview(); + listen(); +} + +init(); diff --git a/public/projects/js-small-apps/rock-paper-scissors/index.html b/public/projects/js-small-apps/rock-paper-scissors/index.html new file mode 100644 index 0000000..19c9d7a --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/index.html @@ -0,0 +1,142 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta http-equiv="X-UA-Compatible" content="IE=edge" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>Rock Paper Scissors - Game</title> + <link rel="stylesheet" href="style.css" /> + </head> + <body> + <header class="branding"> + <h1 class="branding__title">Rock Paper Scissors</h1> + </header> + <main class="main"> + <div class="register"> + <form action="#" class="form form--register"> + <fieldset class="form__fieldset"> + <legend class="form__legend">Player 1</legend> + <div class="form__item"> + <label class="form__label" for="player1-name">Name:</label> + <input + type="text" + id="player1-name" + name="player1-name" + placeholder="Pseudo" + class="form__input" + /> + </div> + <div class="form__item"> + <input + type="checkbox" + name="player1-ia" + id="player1-ia" + class="form__checkbox" + /> + <label class="form__label form__label--checkbox" for="player1-ia" + >IA?</label + > + </div> + </fieldset> + <fieldset class="form__fieldset"> + <legend class="form__legend">Player 2</legend> + <div class="form__item"> + <label class="form__label" for="player2-name">Name:</label> + <input + type="text" + id="player2-name" + name="player2-name" + placeholder="Pseudo" + class="form__input" + /> + </div> + <div class="form__item"> + <input + type="checkbox" + name="player2-ia" + id="player2-ia" + class="form__checkbox" + /> + <label class="form__label form__label--checkbox" for="player2-ia" + >IA?</label + > + </div> + </fieldset> + <fieldset class="form__fieldset"> + <legend class="form__legend">Settings</legend> + <div class="form__item"> + <label class="form__label" for="round-number" + >Number of rounds:</label + > + <input + type="number" + name="round-number" + id="round-number" + class="form__input" + /> + <p class="form__hint"> + Note: If empty or 0, your party will never end! + </p> + </div> + </fieldset> + <button type="submit" class="form__submit btn btn--register"> + Play! + </button> + </form> + <div class="register__preview"> + <div class="preview"> + <div class="preview__content"></div> + </div> + </div> + </div> + <div class="game"> + <div class="scoring-board"> + <div class="scoring-board__item"> + <div id="player1username" class="scoring-board__username"> + Player1 + </div> + <div id="player1score" class="scoring-board__score">0</div> + </div> + <div class="scoring-board__item"> + <div id="player2username" class="scoring-board__username"> + Player2 + </div> + <div id="player2score" class="scoring-board__score">0</div> + </div> + </div> + <div class="message-board">Ready? Let's play!</div> + <div class="actions"> + <button type="button" class="actions__body btn btn--action btn--rock"> + <span class="actions__txt screen-reader-txt">Rock</span> + <span class="actions__icon">✊</span> + </button> + <button + type="button" + class="actions__body btn btn--action btn--paper" + > + <span class="actions__txt screen-reader-txt">Paper</span> + <span class="actions__icon">✋</span> + </button> + <button + type="button" + class="actions__body btn btn--action btn--scissors" + > + <span class="actions__txt screen-reader-txt">Scissors</span> + <span class="actions__icon">✌️</span> + </button> + </div> + <div class="settings"> + <button type="reset" class="settings__body btn btn--new-game"> + New Game? + </button> + </div> + </div> + </main> + <footer class="footer"> + <p class="footer__copyright"> + Rock Paper Scissors. MIT 2021. Armand Philippot. + </p> + </footer> + <script type="module" src="./app.js"></script> + </body> +</html> diff --git a/public/projects/js-small-apps/rock-paper-scissors/lib/class-game.js b/public/projects/js-small-apps/rock-paper-scissors/lib/class-game.js new file mode 100644 index 0000000..92898a1 --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/lib/class-game.js @@ -0,0 +1,296 @@ +import Player from "./class-player.js"; + +/** + * Game class. + */ +class Game { + #name = "My Game"; + #language = "en-US"; + #playerId = 0; + #players = []; + #roundWinners = []; + #roundLosers = []; + #gameWinners = []; + #gameLosers = []; + #state = "paused"; + #turn; + #currentTurn = 1; + #maxTurn = 0; + #currentRound = 1; + #maxRound = 0; + + /** + * Initialize a new Game instance. + * @param {String} name - The game name. + * @param {Object[]} players - The players. + * @param {String} players[].username - The player username. + * @param {Boolean} players[].ia - True to set the player as an IA. + */ + constructor(name, players) { + this.#name = name; + players.forEach((player) => { + this.#players.push( + new Player(++this.#playerId, player.username, player.ia) + ); + }); + } + + set name(string) { + this.#name = string; + } + + get name() { + return this.#name; + } + + set language(languageCode) { + this.#language = languageCode; + } + + get language() { + return this.#language; + } + + set players(array) { + array.forEach((player) => + this.#players.push( + new Player(++this.#playerId, player.username, player.ia) + ) + ); + } + + get players() { + return this.#players; + } + + set roundWinners(array) { + if (array.length > 0) { + array.forEach((player) => this.#roundWinners.push(player)); + } else { + this.#roundWinners = []; + } + } + + get roundWinners() { + return this.#roundWinners; + } + + set roundLosers(array) { + if (array.length > 0) { + array.forEach((player) => this.#roundLosers.push(player)); + } else { + this.#roundLosers = []; + } + } + + get roundLosers() { + return this.#roundLosers; + } + + set gameWinners(array) { + if (array.length > 0) { + array.forEach((player) => this.#gameWinners.push(player)); + } else { + this.#gameWinners = []; + } + } + + get gameWinners() { + return this.#gameWinners; + } + + set gameLosers(array) { + if (array.length > 0) { + array.forEach((player) => this.#gameLosers.push(player)); + } else { + this.#gameLosers = []; + } + } + + get gameLosers() { + return this.#gameLosers; + } + + set state(string) { + this.#state = string; + } + + get state() { + return this.#state; + } + + set turn(generator) { + this.#turn = generator; + } + + get turn() { + return this.#turn; + } + + set currentTurn(int) { + this.#currentTurn = int; + } + + get currentTurn() { + return this.#currentTurn; + } + + set maxTurn(int = this.getPlayersNumber()) { + this.#maxTurn = int; + } + + get maxTurn() { + return this.#maxTurn; + } + + set currentRound(int) { + this.#currentRound = int; + } + + get currentRound() { + return this.#currentRound; + } + + set maxRound(int) { + this.#maxRound = int; + } + + get maxRound() { + return this.#maxRound; + } + + newPlayer(username) { + this.players.push(new Player(++this.#playerId, username)); + } + + getPlayersNumber() { + return this.players.length; + } + + getPlayer(number) { + return this.players[number - 1]; + } + + getCurrentPlayer() { + return this.getPlayer(this.currentTurn); + } + + getNextPlayer() { + if (this.currentTurn < this.maxTurn) { + return this.getPlayer(this.currentTurn + 1); + } else { + return this.getPlayer(1); + } + } + + isFirstTurn() { + return this.currentTurn === 1; + } + + isNewRound() { + return this.currentRound > 1 && this.isFirstTurn(); + } + + isGameOver() { + return this.state === "ended"; + } + + resetScore() { + this.players.forEach((player) => { + player.score = 0; + }); + } + + newGame() { + this.roundWinners = []; + this.roundLosers = []; + this.gameWinners = []; + this.gameLosers = []; + this.state = "paused"; + this.turn = this.#generateTurns(); + this.currentTurn = 1; + this.currentRound = 1; + this.maxTurn = this.getPlayersNumber(); + this.resetScore(); + } + + setPlayerChoice(choice) { + if (this.state === "running") { + this.getCurrentPlayer().choice = choice; + } + } + + *#generateTurns() { + this.currentRound = 1; + while (this.maxRound ? this.currentRound <= this.maxRound : true) { + this.currentTurn = 1; + while (this.currentTurn <= this.maxTurn) { + yield this.currentTurn; + this.currentTurn++; + } + this.currentRound++; + } + this.stop(); + this.setGameWinners(); + this.setGameLosers(); + return; + } + + /** + * Get a random choice from an array of choices. + * @param {Array} array - The choices. + * @returns {*} A random choice. + */ + getRandomChoice(array) { + const randomIndex = Math.floor(Math.random() * array.length); + return array[randomIndex]; + } + + getOrderedScores() { + let scores = []; + this.players.forEach((player) => { + scores.push(player.score); + }); + scores.sort((a, b) => a - b); + + return scores; + } + + setGameWinners() { + const scores = this.getOrderedScores(); + const highestScore = scores.pop(); + const winners = this.players.filter( + (player) => player.score === highestScore + ); + this.gameWinners = winners; + } + + setGameLosers() { + const scores = this.getOrderedScores(); + const lowestScore = scores.shift(); + const losers = this.players.filter( + (player) => player.score === lowestScore + ); + this.gameLosers = losers; + } + + resume() { + this.state = "running"; + } + + pause() { + this.state = "paused"; + } + + stop() { + this.state = "ended"; + } + + launch() { + this.newGame(); + this.resume(); + this.turn.next(); + } +} + +export default Game; diff --git a/public/projects/js-small-apps/rock-paper-scissors/lib/class-player.js b/public/projects/js-small-apps/rock-paper-scissors/lib/class-player.js new file mode 100644 index 0000000..8935581 --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/lib/class-player.js @@ -0,0 +1,60 @@ +/** + * Player class. + */ +class Player { + #id; + #username = "Anonymous"; + #choice = ""; + #score = 0; + #ia = false; + + /** + * Initialize a new Player instance. + * @param {Integer} id - The player id. + * @param {String} username - The player username. + * @param {Boolean} ia - True to set player as an IA. + */ + constructor(id, username, ia) { + this.#id = id; + this.#username = username; + this.#ia = ia; + } + + get id() { + return this.#id; + } + + set username(name) { + this.#username = name; + } + + get username() { + return this.#username; + } + + set choice(choice) { + this.#choice = choice; + } + + set score(score) { + this.#score = score; + } + + get choice() { + return this.#choice; + } + + get score() { + return this.#score; + } + + set ia(boolean) { + this.#ia = boolean; + } + + get ia() { + return this.#ia; + } +} + +export default Player; diff --git a/public/projects/js-small-apps/rock-paper-scissors/lib/class-rps-game.js b/public/projects/js-small-apps/rock-paper-scissors/lib/class-rps-game.js new file mode 100644 index 0000000..fe517db --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/lib/class-rps-game.js @@ -0,0 +1,230 @@ +import Game from "./class-game.js"; + +/** + * RPSGame class. + */ +class RPSGame extends Game { + #choices = ["rock", "paper", "scissors"]; + #buttons = { rock: "", paper: "", scissors: "", newGame: "" }; + #p1Scoring = { name: "", value: "" }; + #p2Scoring = { name: "", value: "" }; + #messages = ""; + #messageIterator; + #timeoutId; + + /** + * Initialize a new RPSGame instance. + * @param {Object[]} players - An array of player object. + * @param {String} players[].username - The player username. + * @param {Boolean} players[].ia - True to set the player as an IA. + * @param {Object} buttons - The buttons HTMLElement. + * @param {HTMLElement} buttons.rock - Button Element for rock choice. + * @param {HTMLElement} buttons.paper - Button Element for paper choice. + * @param {HTMLElement} buttons.scissors - Button Element for scissors choice. + * @param {HTMLElement} buttons.newGame - Button Element to start new game. + * @param {Object} p1Scoring - The player 1 scoring display. + * @param {HTMLElement} p1Scoring.name - Element to display player 1 name. + * @param {HTMLElement} p1Scoring.value - Element to display player 1 score. + * @param {Object} p2Scoring - The player 2 scoring display. + * @param {HTMLElement} p2Scoring.name - Element to display player 2 name. + * @param {HTMLElement} p2Scoring.value - Element to display player 2 score. + * @param {HTMLElement} messages - Element to display turn/game results. + */ + constructor( + players, + buttons = { rock: "", paper: "", scissors: "", newGame: "" }, + p1Scoring = { name: "", value: "" }, + p2Scoring = { name: "", value: "" }, + messages + ) { + super("Rock Paper Scissors", players); + this.#buttons = buttons; + this.#p1Scoring = p1Scoring; + this.#p2Scoring = p2Scoring; + this.#messages = messages; + } + + get messages() { + return this.#messages; + } + + set messageIterator(generator) { + this.#messageIterator = generator; + } + + get messageIterator() { + return this.#messageIterator; + } + + #updatePlayers() { + this.#p1Scoring.name.textContent = this.getPlayer(1).username; + this.#p2Scoring.name.textContent = this.getPlayer(2).username; + } + + #updateScore() { + this.#p1Scoring.value.textContent = this.getPlayer(1).score; + this.#p2Scoring.value.textContent = this.getPlayer(2).score; + } + + async #createMessage(msg, delay = 0) { + return new Promise( + (resolve) => + (this.#timeoutId = setTimeout(() => { + resolve(msg); + }, delay)) + ); + } + + async *#generateMessages() { + let msg; + msg = yield await this.#createMessage("New game, let's play!"); + + while (this.state === "running") { + for (let index = 0; index < this.getPlayersNumber(); index++) { + if (this.getCurrentPlayer().ia) { + msg = yield this.#createMessage( + `${this.getCurrentPlayer().username} is playing...`, + this.isFirstTurn() ? 1200 : 200 + ); + } else { + msg = yield this.#createMessage( + `${this.getCurrentPlayer().username}'s turn...`, + this.isFirstTurn() ? 1200 : 900 + ); + } + } + msg = yield this.#createMessage( + msg, + this.getCurrentPlayer().ia ? 1000 : 500 + ); + if (!this.isGameOver()) { + msg = yield this.#createMessage("New round...", 1500); + } + } + const winnersList = this.gameWinners.map((winner) => winner.username); + const losersList = this.gameLosers.map((loser) => loser.username); + msg = yield this.#createMessage( + `Winner: ${winnersList.join(", ")} / Loser: ${losersList.join(", ")}` + ); + } + + async printNextMessage(msg = null) { + if (!this.messageIterator) { + this.messageIterator = this.#generateMessages(); + } + this.messages.textContent = await this.messageIterator + .next(msg) + .then((object) => object.value); + } + + async #setTurnIssue() { + const choices = `${this.getPlayer(1).choice}-${this.getPlayer(2).choice}`; + let turnWinner; + let turnLoser; + let even = false; + let msg; + + switch (choices) { + case "rock-paper": + case "paper-scissors": + case "scissors-rock": + turnWinner = this.getPlayer(2); + turnLoser = this.getPlayer(1); + break; + case "paper-rock": + case "rock-scissors": + case "scissors-paper": + turnWinner = this.getPlayer(1); + turnLoser = this.getPlayer(2); + break; + default: + even = true; + break; + } + + if (!even) { + this.turnWinners = [turnWinner]; + this.turnLosers = [turnLoser]; + turnWinner.score++; + msg = `${turnWinner.username} wins! ${turnWinner.choice} beats ${turnLoser.choice}.`; + } else { + msg = `No winner. ${this.getPlayer(1).choice} equals to ${ + this.getPlayer(2).choice + }.`; + } + await this.printNextMessage(msg); + this.#updateScore(); + await this.printNextMessage(); + } + + async #getIAAction() { + if (this.currentTurn % 2 === 0) { + await this.#setTurnIssue(); + this.turn.next(); + await this.printNextMessage(); + if (!this.isGameOver()) { + this.getCurrentPlayer().choice = this.getRandomChoice(this.#choices); + } + } else { + this.turn.next(); + await this.printNextMessage(); + this.getCurrentPlayer().choice = this.getRandomChoice(this.#choices); + await this.#setTurnIssue(); + } + this.turn.next(); + await this.printNextMessage(); + } + + async listen() { + for (const [name, element] of Object.entries(this.#buttons)) { + element.addEventListener("click", async (event) => { + event.preventDefault(); + switch (name) { + case "rock": + case "paper": + case "scissors": + if (this.state === "running") { + this.setPlayerChoice(name); + if (this.currentTurn % 2 === 0) { + await this.#setTurnIssue(); + } + if (this.getNextPlayer().ia) { + await this.#getIAAction(); + } else { + this.turn.next(); + await this.printNextMessage(); + } + } + break; + case "newGame": + this.messageIterator = null; + clearTimeout(this.#timeoutId); + await this.launch(); + default: + break; + } + }); + } + } + + async launch() { + super.launch(); + this.#updatePlayers(); + this.#updateScore(); + await this.printNextMessage(); + await this.printNextMessage(); + + if (this.getCurrentPlayer().ia) { + this.getCurrentPlayer().choice = this.getRandomChoice(this.#choices); + this.turn.next(); + await this.printNextMessage(); + } + } + + async init() { + await this.launch(); + this.listen(); + } +} + +export default RPSGame; diff --git a/public/projects/js-small-apps/rock-paper-scissors/lib/rps-instance.js b/public/projects/js-small-apps/rock-paper-scissors/lib/rps-instance.js new file mode 100644 index 0000000..73c5888 --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/lib/rps-instance.js @@ -0,0 +1,70 @@ +import RPS_Game from "./rps-game.js"; + +export default class RPS_Instance extends RPS_Game { + #buttons = {}; + #scoring = { player1: '', player2: '' }; + #resultBox = ''; + #resultMsg = ''; + + constructor(player1, player2, buttons, scoring, result) { + super(player1, player2); + this.#buttons = buttons; + this.#scoring = scoring; + this.#resultBox = result; + this.#resultMsg = result.innerHTML; + } + + updateScoring() { + for (const [name, element] of Object.entries(this.#scoring)) { + if ( 'player1' === name ) { + element.innerHTML = this.player1Score; + } + + if ( 'player2' === name ) { + element.innerHTML = this.player2Score; + } + } + } + + updateResult(result = '') { + let txt; + if ( result === 'player1' ) { + txt = this.player1Choice + ' beats ' + this.player2Choice + ".<br>You win!" + this.#resultBox.style.color = 'green'; + } else if ( result === 'player2' ) { + txt = this.player1Choice + ' loses to ' + this.player2Choice + ".<br>You lose..." + this.#resultBox.style.color = 'red'; + } else if ( result === 'even' ) { + txt = this.player1Choice + ' equals to ' + this.player2Choice + ".<br>No winner."; + this.#resultBox.style.color = 'black'; + } else { + txt = this.#resultMsg; + this.#resultBox.style.color = ''; + } + + this.#resultBox.innerHTML = txt; + } + + listen() { + for (const [name, element] of Object.entries(this.#buttons)) { + element.addEventListener('click', () => { + if ( 'reset' === name ) { + this.reset(); + this.updateResult(); + this.updateScoring(); + } else { + this.player1Choice = name; + const result = this.calculateScore(); + this.setScore(result); + this.updateResult(result); + this.updateScoring(); + } + }) + } + } + + init() { + this.reset(); + this.listen(); + } +} diff --git a/public/projects/js-small-apps/rock-paper-scissors/style.css b/public/projects/js-small-apps/rock-paper-scissors/style.css new file mode 100644 index 0000000..28d2f03 --- /dev/null +++ b/public/projects/js-small-apps/rock-paper-scissors/style.css @@ -0,0 +1,433 @@ +/* + * Base + */ + +*, +*::after, +*::before { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + display: flex; + flex-flow: column nowrap; + background: hsl(180, 29%, 95%); + font-family: "Courier New", Courier, monospace; + font-size: 1rem; + line-height: 1.618; + min-height: 100vh; + max-width: 100vw; +} + +button { + display: block; + cursor: pointer; + font-family: inherit; + font-size: 100%; + line-height: 1.15; + margin: 0; +} + +/* + * Helpers + */ + +.screen-reader-txt { + border: 0; + clip: rect(1px, 1px, 1px, 1px); + width: 1px; + height: 1px; + overflow: hidden; + padding: 0; + position: absolute !important; + word-break: normal; + word-wrap: normal !important; +} + +/* + * Layout + */ + +.branding { + margin: 2rem auto clamp(2rem, 5vh, 5rem); +} + +.branding__title { + color: hsl(207, 85%, 47%); + font-size: clamp(2.5rem, 3vw, 3rem); + text-align: center; + text-shadow: 1px 1px 1px hsl(207, 85%, 27%); +} + +.main { + display: flex; + flex-flow: column nowrap; + flex: 1; + margin: 3rem 0; +} + +.register { + flex: 1; + display: grid; + grid-template-columns: repeat( + auto-fill, + min(calc(100vw - 2rem), calc(1200px / 2 - 4rem)) + ); + gap: 2rem; + align-items: center; + justify-content: center; + justify-items: center; + margin: auto; + width: min(calc(100vw - 2rem), calc(1200px - 2rem)); +} + +.form--register { + justify-self: stretch; +} + +.form { + display: flex; + flex-flow: column wrap; + gap: clamp(1rem, 3vh, 2rem); +} + +.form__fieldset { + display: flex; + flex-flow: row wrap; + align-items: flex-end; + gap: 2rem; + border: 4px solid hsl(207, 85%, 45%); + padding: 1.7rem clamp(0.5rem, 3vw, 1.5rem); + position: relative; + margin-top: 2.5rem; +} + +.form__legend { + background: hsl(180, 29%, 95%); + border: 4px solid hsl(207, 85%, 45%); + border-bottom-width: 0; + color: hsl(207, 85%, 28%); + font-size: 1.4rem; + font-weight: 600; + letter-spacing: 2px; + text-transform: uppercase; + padding: 0.5rem 1.3rem; + position: absolute; + top: -40px; + left: -4px; + right: -4px; +} + +.form__item { + flex: 1 1 max-content; +} + +.form__item:last-child:not(:only-of-type) { + margin-left: auto; + margin-bottom: 0.8rem; +} + +.form__label { + display: block; + cursor: pointer; + position: relative; + letter-spacing: 1px; +} + +.form__label--checkbox { + display: initial; +} + +.form__label--checkbox::before { + position: absolute; + top: -1px; + left: -28px; + display: block; + content: ""; + width: 1.2rem; + height: 1.2rem; + background: hsl(180, 29%, 95%); + border: 3px solid hsl(207, 85%, 45%); + color: hsl(207, 85%, 40%); + font-size: 1.1rem; + font-weight: 600; + line-height: 0.85; + text-align: center; +} + +.form__input { + background: hsl(180, 29%, 95%); + border: 3px solid hsl(207, 85%, 45%); + font-size: inherit; + padding: 0.7rem 0.8rem; +} + +.form__checkbox { + order: 2; + width: max-content; +} + +.form__checkbox:checked ~ .form__label--checkbox::before { + content: "x"; +} + +.form__hint { + font-size: 0.85rem; + margin: 1rem 0 0; +} + +.form__submit { + margin: 1rem 0 0 auto; +} + +.preview { + background: hsl(207, 85%, 45%); + clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%); + width: clamp(18rem, 40vw, 30rem); + height: clamp(18rem, 40vw, 30rem); + position: relative; +} + +.preview__content { + display: flex; + flex-flow: column wrap; + align-items: center; + justify-content: center; + gap: 1.5rem; + background: hsl(180, 29%, 95%); + border-radius: 50%; + font-size: clamp(1.2rem, 3vw, 2rem); + width: 80%; + height: 80%; + padding: 1rem; + position: relative; + top: 10%; + left: 10%; +} + +.ia-badge { + position: relative; +} + +.ia-badge::after { + display: block; + content: "IA"; + position: absolute; + top: 0; + right: -2.5rem; + background: rgb(253, 206, 119); + border-radius: 50%; + font-size: 0.9rem; + padding: 4px 8px; + width: max-content; +} + +.game { + flex: 1; + display: none; + flex-flow: column nowrap; + justify-content: space-between; + align-items: center; + gap: 2rem; + margin: auto; + padding: clamp(1rem, 3vh, 2rem) 0; + width: min(calc(100vw - 2rem), 600px); +} + +.scoring-board { + font-family: "Courier New", Courier, monospace; + font-size: 1.5rem; + text-align: center; + width: 100%; +} + +.scoring-board__item { + background: hsl(0, 0%, 100%); + border: 5px solid hsl(207, 85%, 47%); + border-radius: 3px; + display: inline-block; + margin-bottom: clamp(1rem, 3vh, 2rem); + padding: 1rem 0; + width: calc(50% - 1rem); + position: relative; +} + +.scoring-board__item:first-child { + box-shadow: 1px 1px 0 2px hsl(207, 85%, 27%); +} + +.scoring-board__item:not(:first-child) { + box-shadow: -1px 1px 0 2px hsl(207, 85%, 27%); + margin-left: 1rem; +} + +.scoring-board__item:first-child::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + right: -50%; + background: hsl(207, 85%, 47%); + box-shadow: 0 3px 0 0 hsl(207, 85%, 27%); + height: 5px; + z-index: -1; +} + +.scoring-board__username { + font-variant: small-caps; + letter-spacing: 1px; +} + +.scoring-board__score { + font-size: 2rem; + font-weight: 600; + margin-top: 1rem; +} + +.message-board { + font-size: 1.4rem; + text-align: center; + margin: 1rem auto; +} + +.actions { + display: grid; + grid-template-columns: repeat(3, 33%); + justify-content: center; + gap: clamp(1rem, 3vw, 2rem); + font-size: 1.2rem; + margin-bottom: clamp(1rem, 3vh, 2rem); +} + +.actions__body { + width: clamp(5rem, 15vw, 8rem); + height: clamp(4.5rem, 12vw, 7rem); + position: relative; +} + +.actions__icon { + font-size: clamp(2.2rem, 7vw, 3.2rem); + filter: drop-shadow(1px 1px 1px hsl(207, 85%, 27%)); + position: relative; + top: 4px; +} + +.settings { + display: flex; + flex-flow: row wrap; + gap: 2rem; + justify-content: center; +} + +.footer { + font-size: 0.9rem; + text-align: center; + margin: clamp(2rem, 5vh, 4rem) auto 2rem; +} + +/* + * Components + */ + +.btn { + font-weight: 600; + transition: all 0.2s ease-in-out 0s; +} + +.btn:focus { + outline: none; +} + +.btn--action { + background: hsl(207, 85%, 45%); + border: 2px solid hsl(207, 85%, 28%); + border-radius: 100%; + box-shadow: inset 0 0 0 3px hsl(207, 85%, 38%), 0 5px 0 0 hsl(207, 85%, 28%), + 0 5px 0 2px hsl(207, 85%, 20%); +} + +.btn--action:hover, +.btn--action:focus { + background: hsl(207, 85%, 52%); + transform: translateY(2px); +} + +.btn--action:hover { + box-shadow: inset 0 0 0 3px hsl(207, 85%, 38%), 0 3px 0 0 hsl(207, 85%, 32%), + 0 3px 0 2px hsl(207, 85%, 22%); +} + +.btn--action:focus { + box-shadow: inset 0 0 0 3px hsl(207, 85%, 38%), 0 3px 0 0 hsl(207, 85%, 32%), + 0 3px 0 2px hsl(207, 85%, 22%), 0 3px 0 8px hsl(204, 42%, 68%); +} + +.btn--action:active { + box-shadow: inset 0 0 0 3px hsl(207, 85%, 38%), 0 0 0 0 hsl(207, 85%, 28%), + 0 0 0 1px hsl(207, 85%, 20%); + transform: translateY(8px); +} + +.btn--new-game { + border: 2px solid hsl(0, 50%, 45%); + border-radius: 0.5rem; + color: hsl(0, 50%, 45%); + box-shadow: 1px 1px 0 1px hsl(0, 50%, 25%); + padding: 0.75rem 1rem; +} + +.btn--new-game:hover, +.btn--new-game:focus { + background: hsl(0, 35%, 90%); + color: hsl(0, 50%, 25%); + transform: translateX(-3px) translateY(-3px); +} + +.btn--new-game:hover { + box-shadow: 3px 3px 0 1px hsl(0, 50%, 25%); +} + +.btn--new-game:focus { + box-shadow: 3px 3px 0 1px hsl(0, 50%, 25%), 3px 3px 0 4px hsl(0, 11%, 75%); +} + +.btn--new-game:active { + background: hsl(204, 14%, 93%); + color: hsl(0, 50%, 25%); + box-shadow: none; + transform: translateX(3px) translateY(3px); +} + +.btn--register { + border: 3px solid hsl(207, 85%, 50%); + border-radius: 8px; + background-color: hsl(207, 85%, 40%); + box-shadow: inset 0 0 0 3px #fff, 1px 1px 0 1px hsl(207, 85%, 25%), + 3px 3px 0 2px hsl(207, 85%, 30%); + color: #fff; + font-size: 1.2rem; + font-variant: small-caps; + font-weight: 600; + text-shadow: 2px 2px 0 #000; + width: max-content; + padding: 1rem 2rem; +} + +.btn--register:hover, +.btn--register:focus { + background-color: hsl(207, 85%, 47%); + border-color: hsl(207, 85%, 57%); + box-shadow: inset 0 0 0 3px #fff, 1px 1px 0 1px hsl(207, 85%, 25%), + 5px 5px 0 2px hsl(207, 85%, 30%); + transform: translateY(-3px) translateX(-3px); +} + +.btn--register:active { + background-color: hsl(207, 85%, 35%); + border-color: hsl(207, 85%, 45%); + box-shadow: inset 0 0 0 3px #fff, 1px 1px 0 1px hsl(207, 85%, 25%), + 0 0 0 1px hsl(207, 85%, 30%); + transform: translateY(3px) translateX(3px); +} |
