aboutsummaryrefslogtreecommitdiffstats
path: root/public/projects/js-small-apps/rock-paper-scissors
diff options
context:
space:
mode:
Diffstat (limited to 'public/projects/js-small-apps/rock-paper-scissors')
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/README.md11
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/app.js115
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/index.html142
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/lib/class-game.js296
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/lib/class-player.js60
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/lib/class-rps-game.js230
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/lib/rps-instance.js70
-rw-r--r--public/projects/js-small-apps/rock-paper-scissors/style.css433
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);
+}