diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/data.js | 98 | ||||
| -rw-r--r-- | src/main.js | 184 | ||||
| -rw-r--r-- | src/manage.js | 179 | ||||
| -rw-r--r-- | src/style.css | 12 | ||||
| -rw-r--r-- | src/timer.js | 84 |
5 files changed, 557 insertions, 0 deletions
diff --git a/src/data.js b/src/data.js new file mode 100644 index 0000000..4358ee6 --- /dev/null +++ b/src/data.js @@ -0,0 +1,98 @@ +class Switch { + constructor(id, description, status) { + this.id = id; + this.status = typeof status !== "undefined" ? status : false; + this.description = description; + this.name = description + " " + (id + 1); + } + toggle() { + this.status = !this.status; + this.print(); + } + print() { + let status = this.status ? "active" : "inactive"; + console.log(this.name + " is currently " + status); + } + render() { + let color = this.status ? "is-success" : "is-danger"; + let type = `class="button is-medium ${color}"`; + let id = this.description + "_" + this.id; + return `<button id=${id} ${type}>${this.name}</button>`; + } +} + +export class Court extends Switch { + constructor(id, status) { + super(id, "Court", status); + } +} + +export class Player extends Switch { + constructor(id, status, level, games, name) { + super(id, "Regular", status); + this.level = typeof level !== "undefined" ? level : 1; + this.games = typeof games !== "undefined" ? games : 0; + if (typeof name !== "undefined") { + this.name = name; + } + } +} + +const guestCategories = { + 0: { description: "Beginner", level: 1 }, + 1: { description: "Novice", level: 3 }, + 2: { description: "Intermediate", level: 6 }, +}; + +export class Guest extends Switch { + maxCount = 4; + categoryCount = 3; + + constructor(id, status, games) { + super(id, "Guest", status); + const categoryId = Math.floor(id / this.maxCount) || 0; + this.level = guestCategories[categoryId].level; + this.games = typeof games !== "undefined" ? games : 0; + const guestNumber = (id % this.maxCount) + 1; + this.name = guestCategories[categoryId].description + " " + guestNumber; + } +} + +export class Team { + constructor(playerOne, playerTwo) { + this.playerOne = playerOne; + this.playerTwo = playerTwo; + } + print() { + console.log("Team consists of:"); + this.playerOne.print(); + this.playerTwo.print(); + } + render() { + let p1 = `<td style="width: 23%; text-align: right;">${this.playerOne}</td>`; + let p2 = `<td style="width: 23%; text-align: left;">${this.playerTwo}</td>`; + return p1 + p2; + } +} + +export class Game extends Switch { + constructor(id, status, teamOne, teamTwo) { + super(id, "Game", status); + this.teamOne = teamOne; + this.teamTwo = teamTwo; + } + print() { + super.print(); + let teamOne = this.teamOne.playerOne + " and " + this.teamOne.playerTwo; + let teamTwo = this.teamTwo.playerOne + " and " + this.teamTwo.playerTwo; + console.log(teamOne + " is playing against " + teamTwo); + } + render() { + let blockedState = this.teamOne.playerOne === "RESERVED"; + let type = blockedState ? `class="is-danger is-dark"` : ``; + let court = `<td style="width: 8%; text-align: center;">${ + this.id + 1 + }</td>`; + return `<tr ${type}>${court}${this.teamOne.render()}${this.teamTwo.render()}</tr>`; + } +} diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..cb7ec8e --- /dev/null +++ b/src/main.js @@ -0,0 +1,184 @@ +import "notyf/notyf.min.css"; +import "bulma/css/bulma.css"; +import "@fortawesome/fontawesome-free/css/all.css"; +import "./style.css"; +import { Notyf } from "notyf"; +import { Timer } from "./timer"; +import { Team, Game } from "./data"; +import { Manager, CourtManager, PlayerManager, GuestManager } from "./manage"; + +export function getNotifier(seconds) { + return new Notyf({ + duration: seconds * milliSecond, + position: { x: "center", y: "top" }, + dismissible: true, + }); +} + +export class GameManager extends Manager { + timer = new Timer(); + courts = new CourtManager(); + players = new PlayerManager(); + guests = new GuestManager(); + + constructor() { + super("Game", 12); + this.round = Number(localStorage.getItem("round")) || 0; + this.render(); + addGameBehaviour(this); + } + createItem(i, description) { + if (typeof description === "undefined") { + description = "---"; + } + let team = new Team(description, description); + return new Game(i, true, team, team); + } + loadItem(data) { + let id = Number(data.id); + let teamOne = new Team(data.teamOne.playerOne, data.teamOne.playerTwo); + let teamTwo = new Team(data.teamTwo.playerOne, data.teamTwo.playerTwo); + let status = Boolean(data.status); + return new Game(id, status, teamOne, teamTwo); + } + addBehaviour() { + let confirmedStatus = this.data[0].status; + let description = confirmedStatus ? "Round " + this.round : "Proposal"; + document.getElementById("Game_description").innerHTML = description; + } + addBlockedGames() { + const inactiveCourts = this.courts.inactive(); + console.log("Total reserved courts are " + inactiveCourts.length + "."); + for (let court of inactiveCourts) { + this.data.push(this.createItem(court.id, "RESERVED")); + } + } + addInactiveGames(courts) { + for (let court of courts) { + this.data.push(this.createItem(court.id)); + } + } + addActiveGames(courts, players) { + for (let index = 0; index < courts.length; index++) { + const startIndex = 4 * index; + const endIndex = startIndex + 4; + const courtPlayers = players.slice(startIndex, endIndex); + const teamOne = new Team(courtPlayers[0].name, courtPlayers[3].name); + const teamTwo = new Team(courtPlayers[1].name, courtPlayers[2].name); + const game = new Game(courts[index].id, false, teamOne, teamTwo); + game.print(); + this.data.push(game); + } + } + getPossibleGameCount() { + let activeRegularCount = this.players.active().length; + let activeGuestCount = this.guests.active().length; + let activePlayerCount = activeRegularCount + activeGuestCount; + console.log( + "There are a total of " + activePlayerCount + " players available." + ); + let activeCourtCount = this.courts.active().length; + console.log( + "There are a total of " + activeCourtCount + " courts available." + ); + let possibleMatches = Math.min( + Math.floor(activePlayerCount / 4), + activeCourtCount + ); + return possibleMatches; + } + selectPlayers(count) { + console.log("Required players are " + count); + let activePlayers = this.players.active(); + const activeGuests = this.guests.active(); + activePlayers = activePlayers.concat(activeGuests); + console.log("Active players are:"); + console.log( + activePlayers + .map((i) => { + return i.name; + }) + .join(",") + ); + const shuffledPlayers = shuffle(activePlayers); + shuffledPlayers.sort((a, b) => a.totalPlayed - b.totalPlayed); + const selectedPlayers = shuffledPlayers.slice(0, count); + selectedPlayers.sort((a, b) => a.level - b.level); + console.log("Selected players are:"); + console.log( + selectedPlayers + .map((i) => { + return i.name; + }) + .join(",") + ); + return selectedPlayers; + } + propose() { + console.log("Proposing new games!"); + let possibleMatches = this.getPossibleGameCount(); + console.log("Total possible matches are " + possibleMatches + "."); + this.data = []; + this.addBlockedGames(); + const activeCourts = this.courts.active(); + const unusedCourts = activeCourts.slice(possibleMatches + 1); + this.addInactiveGames(unusedCourts); + const usedCourts = activeCourts.slice(0, possibleMatches); + let requiredPlayerCount = possibleMatches * 4; + const selectedPlayers = this.selectPlayers(requiredPlayerCount); + console.log("The matches are:"); + this.addActiveGames(usedCourts, selectedPlayers); + this.data.sort((a, b) => a.id - b.id); + this.render(); + } + confirm() { + let confirmedStatus = this.data[0].status; + if (confirmedStatus) { + getNotifier().error("Create a proposal first!"); + return; + } + console.log("Confirm the proposal for round " + this.round + "."); + this.round += 1; + localStorage.setItem("round", this.round); + for (let i = 0; i < this.data.length; i++) { + this.data[i].status = true; + } + // Increment the number of games played + this.store(); + this.render(); + document.getElementById("timer-start").click(); + } + reset() { + localStorage.removeItem("round"); + localStorage.removeItem(this.description); + localStorage.removeItem(this.courts.description); + // Clear the number of games played + localStorage.removeItem(this.guests.description); + document.getElementById("timer-stop").click(); + window.location.reload(); + } +} + +function shuffle(array) { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +} + +function addGameBehaviour(g) { + document.getElementById("propose").addEventListener("click", () => { + g.propose(); + }); + document.getElementById("confirm").addEventListener("click", () => { + g.confirm(); + getNotifier().success("Round confirmed!"); + }); + document.getElementById("reset").addEventListener("click", () => { + g.reset(); + getNotifier().error("Session has been reset!"); + }); +} + +const g = new GameManager(); diff --git a/src/manage.js b/src/manage.js new file mode 100644 index 0000000..952c72f --- /dev/null +++ b/src/manage.js @@ -0,0 +1,179 @@ +import { Court, Guest, Player } from "./data"; + +export class Manager { + constructor(description, maxCount) { + this.description = description; + this.maxCount = maxCount; + this.data = []; + this.count = this.data.length; + let data = JSON.parse(localStorage.getItem(this.description)); + if (data === null || data.length === 0) { + this.generate(); + return; + } + this.load(data); + } + generate() { + for (let i = 0; i < this.maxCount; i++) { + this.data.push(this.createItem(i)); + } + console.log("Generated " + this.data.length + " " + this.description); + this.store(); + this.render(); + } + load(data) { + for (let i = 0; i < data.length; i++) { + this.data.push(this.loadItem(data[i])); + } + console.log("Loaded " + this.data.length + " " + this.description); + this.render(); + } + store() { + let stringData = JSON.stringify(this.data); + localStorage.setItem(this.data[0].description, stringData); + } + render() { + let renderedHTML = this.data + .map((i) => { + return i.render(); + }) + .join(""); + let elementId = this.data[0].description + "_list"; + document.getElementById(elementId).innerHTML = renderedHTML; + this.addBehaviour(); + } + addBehaviour() { + for (let idx = 0; idx < this.data.length; ++idx) { + const elementId = this.data[0].description + "_" + this.data[idx].id; + const p = document.getElementById(elementId); + p.addEventListener("click", () => { + this.data[idx].toggle(); + this.store(); + p.classList.toggle("is-success"); + p.classList.toggle("is-danger"); + }); + } + } + active() { + return this.data.filter(function (c) { + return c.status; + }); + } + inactive() { + return this.data.filter(function (c) { + return !c.status; + }); + } +} + +export class CourtManager extends Manager { + constructor() { + super("Court", 12); + } + createItem(i) { + return new Court(i, randomStatus()); + } + loadItem(data) { + let id = Number(data.id); + let status = Boolean(Number(data.status)); + return new Court(id, status); + } +} + +export function randomStatus() { + return Math.random() < 0.5; +} + +function normalize(data) { + let factor = Number(document.getElementById("skill").value); + console.log("Normalize skill by a factor of " + factor + "."); + return data.map(function (p) { + p.skill = Math.ceil(p.skill / factor); + return p; + }); +} + +export class GuestManager extends Manager { + constructor() { + super("Guest", 12); + } + createItem(i) { + return new Guest(i, randomStatus()); + } + loadItem(data) { + let id = Number(data.id); + let status = Boolean(Number(data.status)); + let games = data.games; + return new Guest(id, status, games); + } + active() { + return normalize(super.active()); + } +} + +export function randomLevel() { + const maxLevel = 10; + return Math.floor(Math.random() * maxLevel + 1); +} + +export class PlayerManager extends Manager { + constructor() { + super("Regular", 71); + addLoadBehaviour(this); + } + createItem(i) { + return new Player(i, randomStatus(), randomLevel()); + } + loadItem(data) { + let id = Number(data.id); + let name = data.name; + let level = Number(data.level); + let status = Boolean(Number(data.status)); + let games = Number(data.games); + return new Player(id, status, level, games, name); + } + active() { + return normalize(super.active()); + } +} + +function parseLineToPlayer(data) { + var info = data.split(","); + var id = Number(info[0].trim()); + var name = info[1].trim() + " " + info[2].trim(); + var level = Number(info[3].trim()); + return new Player(id, randomStatus(), level, 0, name); +} + +function parseCsv(data) { + let lines = data.split("\n"); + let players = []; + for (let i = 1; i < lines.length; i++) { + if (lines[i] == undefined || lines[i].trim() == "") { + continue; + } + players.push(parseLineToPlayer(lines[i])); + } + console.log("Loaded " + players.length + " Players"); + for (let i = 0; i < players.length; i++) { + players[i].print(); + } + return players; +} + +function addLoadBehaviour(p) { + const f = document.querySelector("#player-load input[type=file]"); + f.addEventListener("change", () => { + const file = f.files[0]; + let importData; + console.log("Loading data from " + file.name); + const reader = new FileReader(); + reader.onload = function (e) { + importData = reader.result; + p.data = parseCsv(importData); + p.store(); + p.render(); + }; + reader.readAsText(file); + }); +} diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..1bd8901 --- /dev/null +++ b/src/style.css @@ -0,0 +1,12 @@ +.button-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-gap: 15px; +} +#timer-display { + text-align: center; + width: fit-content; +} +table { + font-size: 1.5em; +} diff --git a/src/timer.js b/src/timer.js new file mode 100644 index 0000000..c00ffdc --- /dev/null +++ b/src/timer.js @@ -0,0 +1,84 @@ +const milliSecond = 1000; + +export class Timer { + pointer = 0; + mins = Number(sessionStorage.getItem("mins")) || 12; + running = Boolean(Number(sessionStorage.getItem("running"))) || false; + deadline = Number(sessionStorage.getItem("deadline")) || new Date().getTime(); + + constructor() { + this.running ? this.start(false) : this.reset(); + addTimerBehaviour(this); + } + store() { + sessionStorage.setItem("mins", this.mins); + sessionStorage.setItem("running", Number(this.running)); + sessionStorage.setItem("deadline", this.deadline); + } + set(m, s) { + const timer = document.getElementById("timer-display"); + m = m < 10 ? "0" + m : m; + s = s < 10 ? "0" + s : s; + timer.value = `${m}:${s}`; + } + reset() { + clearInterval(this.pointer); + const mins = Math.floor(this.mins); + const secs = (this.mins - mins) * 60; + this.set(mins, secs); + this.running = false; + this.store(); + } + update(ms) { + const m = Math.floor((ms % (milliSecond * 60 * 60)) / (milliSecond * 60)); + const s = Math.floor((ms % (milliSecond * 60)) / milliSecond); + this.set(m, s); + } + refresh(p) { + const now = new Date().getTime(); + const msToDeadline = p.deadline - now; + if (msToDeadline > 0) { + p.update(msToDeadline); + return; + } + p.reset(); + getNotifier().error("Round completed"); + } + start(fresh) { + if (fresh) { + const now = new Date().getTime(); + const msToDeadline = this.mins * 60 * 1000; + this.deadline = now + msToDeadline; + } + this.running = true; + this.store(); + this.pointer = setInterval(this.refresh, 1000, this); + } + change(value) { + this.mins += value; + if (this.mins < 0.5) { + this.mins = 0.5; + } + this.reset(); + } +} + +function addTimerBehaviour(t) { + document.getElementById("timer-start").addEventListener("click", () => { + if (t.running) return; + t.start(1); + }); + document.getElementById("timer-stop").addEventListener("click", () => { + t.reset(); + }); + document.getElementById("timer-inc").addEventListener("click", () => { + if (!t.running) { + t.change(0.5); + } + }); + document.getElementById("timer-dec").addEventListener("click", () => { + if (!t.running) { + t.change(-0.5); + } + }); +} |
