aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKaran Jayachandra <mail@karanjayachandra.com>2025-11-13 21:48:40 +0100
committerKaran Jayachandra <mail@karanjayachandra.com>2025-11-13 21:48:40 +0100
commit944fc2715e967a51e4ccca49d87e987ee2727bc2 (patch)
tree13ed83b34182d54f9aa85a63c2474a927a6e06b2 /src
parentb2befab1d7c0bb19ac7373ea27d22cec0eea5163 (diff)
Transitioned to npm
Diffstat (limited to 'src')
-rw-r--r--src/court.mjs53
-rw-r--r--src/game.mjs186
-rw-r--r--src/guest.mjs69
-rw-r--r--src/main.js20
-rw-r--r--src/player.mjs150
-rw-r--r--src/style.css12
-rw-r--r--src/timer.js88
-rw-r--r--src/utils.mjs51
8 files changed, 629 insertions, 0 deletions
diff --git a/src/court.mjs b/src/court.mjs
new file mode 100644
index 0000000..713c488
--- /dev/null
+++ b/src/court.mjs
@@ -0,0 +1,53 @@
+import { Manager } from "./utils.mjs";
+
+class Court {
+ constructor(id, status) {
+ this.id = id;
+ this.status = typeof status !== "undefined" ? status : true;
+ this.name = "Court " + (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}"`;
+ return `<button id=${"court_" + this.id} ${type}>${this.name}</button>`;
+ }
+}
+
+export class CourtManager extends Manager {
+ maxCourtCount = 12;
+
+ constructor() {
+ super("court", []);
+ console.log("Loading court data.");
+ let data = JSON.parse(localStorage.getItem(this.description));
+ if (data === null || data.length != this.maxCourtCount) {
+ console.log("Court data unavailable: " + data);
+ this.generate();
+ return;
+ }
+ for (let i = 0; i < this.maxCourtCount; i++) {
+ this.listData.push(new Court(data[i].id, data[i].status));
+ }
+ console.log("Loaded " + this.listData.length + " courts.");
+ this.render();
+ this.addBehaviour();
+ }
+ generate() {
+ console.log("Generating court data.");
+ for (let i = 0; i < this.maxCourtCount; i++) {
+ this.listData.push(new Court(i));
+ }
+ console.log("Generated data for " + this.listData.length + " courts");
+ this.store();
+ this.render();
+ this.addBehaviour();
+ }
+}
diff --git a/src/game.mjs b/src/game.mjs
new file mode 100644
index 0000000..c8931ac
--- /dev/null
+++ b/src/game.mjs
@@ -0,0 +1,186 @@
+import { Notyf } from 'notyf';
+import { Manager } from "./utils.mjs";
+import { CourtManager } from "./court.mjs";
+import { PlayerManager } from "./player.mjs";
+
+class Team {
+ constructor(playerOne, playerTwo) {
+ this.playerOne = playerOne;
+ this.playerTwo = playerTwo;
+ }
+ print() {
+ console.log(this.playerOne + " and " + this.playerTwo);
+ }
+ render() {
+ let p1 = `<td style="width: 23%; text-align: right;">${this.playerOne}</td>`;
+ let p2 = `<td style="width: 23%; text-align: right;">${this.playerTwo}</td>`;
+ return p1 + p2;
+ }
+}
+
+class Game {
+ constructor(id, teamOne, teamTwo, status) {
+ this.id = id;
+ this.teamOne = teamOne;
+ this.teamTwo = teamTwo;
+ this.status = typeof status !== "undefined" ? status : false;
+ }
+ 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"` : ``;
+ return `<tr ${type}><td style="width: 8%; text-align: center;">${
+ this.id + 1
+ }</td>${this.teamOne.render()}${this.teamTwo.render()}</tr>`;
+ }
+}
+
+function generateDummyGames(courts, description) {
+ let team = new Team(description, description);
+ let games = [];
+ for (const court of courts) {
+ let game = new Game(court.id, team, team);
+ games.push(game);
+ }
+ return games;
+}
+
+export class GameManager extends Manager {
+ courts = new CourtManager();
+ players = new PlayerManager();
+
+ constructor() {
+ super("game", []);
+ this.round = Number(localStorage.getItem("round")) || 0;
+ console.log("Loading game data.");
+ let data = JSON.parse(localStorage.getItem(this.description));
+ if (data === null || data.length === 0) {
+ console.log("Game data unavailable: " + data);
+ this.generate();
+ return;
+ }
+ for (let i = 0; i < data.length; i++) {
+ let id = Number(data[i].id);
+ let teamOne = new Team(
+ data[i].teamOne.playerOne,
+ data[i].teamOne.playerTwo
+ );
+ let teamTwo = new Team(
+ data[i].teamTwo.playerOne,
+ data[i].teamTwo.playerTwo
+ );
+ let status = Boolean(data[i].status);
+ this.listData.push(new Game(id, teamOne, teamTwo, status));
+ }
+ console.log("Loaded " + this.listData.length + " games.");
+ this.render();
+ }
+ isConfirmed() {
+ return this.listData[0].status;
+ }
+ generate() {
+ console.log("Generating the dummy round.");
+ this.listData = generateDummyGames(this.courts.listData, "---");
+ console.log("Loaded " + this.listData.length + " dummy round.");
+ for (let i = 0; i < this.listData.length; i++) {
+ this.listData[i].status = true;
+ }
+ console.log(this.listData);
+ this.store();
+ this.render();
+ }
+ addBehaviour() {
+ let description = this.isConfirmed() ? "Round " + this.round : "Proposal";
+ document.getElementById("game_description").innerHTML = description;
+ }
+ getBlockedGames() {
+ const inactiveCourts = this.courts.inactive();
+ console.log("Total reserved courts are " + inactiveCourts.length + ".");
+ return generateDummyGames(inactiveCourts, "RESERVED");
+ }
+ inactive(courts) {
+ return generateDummyGames(courts, "---");
+ }
+ active(courts, players) {
+ let games = [];
+ for (let index = 0; index < courts.length; index++) {
+ const startIndex = 4 * index;
+ const endIndex = startIndex + 4;
+ const courtPlayers = players.slice(startIndex, endIndex);
+ const team_1 = new Team(courtPlayers[0].name, courtPlayers[3].name);
+ const team_2 = new Team(courtPlayers[1].name, courtPlayers[2].name);
+ const game = new Game(courts[index].id, team_1, team_2);
+ game.print();
+ games.push(game);
+ }
+ return games;
+ }
+ propose() {
+ console.log("Proposing new games!");
+ let possibleMatches = getMatchCount(
+ this.players.listData,
+ this.courts.listData
+ );
+ console.log("Total possible matches are " + possibleMatches + ".");
+ this.listData = this.getBlockedGames();
+ const activeCourts = this.courts.active();
+ const unusedCourts = activeCourts.slice(possibleMatches + 1);
+ this.listData = this.listData.concat(this.inactive(unusedCourts));
+ const usedCourts = activeCourts.slice(0, possibleMatches);
+ let requiredPlayerCount = possibleMatches * 4;
+ const selectedPlayers = this.players.propose(requiredPlayerCount);
+ console.log("The matches are:");
+ this.listData = this.listData.concat(
+ this.active(usedCourts, selectedPlayers)
+ );
+ this.listData.sort((a, b) => a.id - b.id);
+ this.render();
+ }
+ confirm() {
+ if (this.isConfirmed()) {
+ const notification = new Notyf();
+ notification.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.listData.length; i++) {
+ this.listData[i].status = true;
+ }
+ // Need to update the number of games played here
+ this.store();
+ this.render();
+ document.getElementById("timer-start").click();
+ const notification = new Notyf();
+ notification.success("Round confirmed!");
+ }
+ reset() {
+ localStorage.removeItem("round");
+ localStorage.removeItem(this.description);
+ localStorage.removeItem(this.courts.description);
+ document.getElementById("timer-stop").click();
+ window.location.reload();
+ const notification = new Notyf();
+ notification.error("Session has been reset!");
+ }
+}
+
+function getMatchCount(players, courts) {
+ courts = courts.filter(function (c) {
+ return c.status;
+ });
+ console.log("There are a total of " + courts.length + " courts available.");
+ console.log(players);
+ players = players.filter(function (p) {
+ return p.status;
+ });
+ console.log("There are a total of " + players.length + " players available.");
+ var possibleMatches = Math.floor(players.length / 4);
+ possibleMatches = Math.min(possibleMatches, courts.length);
+ return possibleMatches;
+}
diff --git a/src/guest.mjs b/src/guest.mjs
new file mode 100644
index 0000000..ba9e8cc
--- /dev/null
+++ b/src/guest.mjs
@@ -0,0 +1,69 @@
+import { Manager } from "./utils.mjs";
+
+export class Guest {
+ constructor(id, name, level, status, totalPlayed) {
+ this.id = id;
+ this.status = typeof status !== "undefined" ? status : false;
+ this.name = name;
+ this.level = typeof level !== "undefined" ? level : 1;
+ this.totalPlayed = typeof totalPlayed !== "undefined" ? status : 0;
+ }
+ 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}"`;
+ return `<button id=${"regular_" + this.id} ${type}>${this.name}</button>`;
+ }
+}
+
+export class GuestManager extends Manager {
+ constructor() {
+ super("guest", []);
+ console.log("Loading guest data.");
+ let data = JSON.parse(localStorage.getItem(this.description));
+ if (data === null || data.length === 0) {
+ console.log("Guest data unavailable: " + data);
+ this.generate();
+ return;
+ }
+ for (let i = 0; i < data.length; i++) {
+ let id = Number(data[i].id);
+ let name = data[i].name;
+ let level = data[i].level;
+ let status = data[i].status;
+ let totalPlayed = data[i].totalPlayed;
+ this.listData.push(new Player(id, name, level, status, totalPlayed));
+ }
+ console.log("Loaded " + this.listData.length + " players.");
+ this.render();
+ }
+ generate() {
+ console.log("Generating guest data.");
+ let maxPlayerCount = 12;
+ for (let i = 0; i < maxPlayerCount; i++) {
+ let level = i < 3 ? 1 : ( i < 6 ? 3 : 7);
+ let p = new Player(i, "Player " + i, level, false, 0);
+ this.listData.push(p);
+ }
+ console.log("Generated data for " + this.listData.length + " guest");
+ this.store();
+ this.render();
+ }
+ active(factor) {
+ factor = typeof factor !== "undefined" ? factor : 1;
+ let data = super.active();
+ console.log("Normalize skill by a factor of " + factor + ".");
+ let players = data.map(function (p) {
+ p.skill = Math.ceil(p.skill / factor);
+ return p;
+ });
+ return players;
+ }
+} \ No newline at end of file
diff --git a/src/main.js b/src/main.js
new file mode 100644
index 0000000..91ac386
--- /dev/null
+++ b/src/main.js
@@ -0,0 +1,20 @@
+import 'notyf/notyf.min.css'
+import 'bulma/css/bulma.css'
+import '@fortawesome/fontawesome-free/css/all.css'
+import './style.css'
+
+import { GameManager } from "./game.mjs";
+
+// Court and player handling
+let g = new GameManager();
+
+// Game handling
+document.getElementById("propose").addEventListener("click", () => {
+ g.propose();
+});
+document.getElementById("confirm").addEventListener("click", () => {
+ g.confirm();
+});
+document.getElementById("reset").addEventListener("click", () => {
+ g.reset();
+});
diff --git a/src/player.mjs b/src/player.mjs
new file mode 100644
index 0000000..adf874d
--- /dev/null
+++ b/src/player.mjs
@@ -0,0 +1,150 @@
+import { GuestManager } from "./guest.mjs";
+import { shuffle, Manager } from "./utils.mjs";
+
+class Player {
+ constructor(id, name, level, status, totalPlayed) {
+ this.id = id;
+ this.status = typeof status !== "undefined" ? status : false;
+ this.name = name;
+ this.level = typeof level !== "undefined" ? level : 1;
+ this.totalPlayed = typeof totalPlayed !== "undefined" ? status : 0;
+ }
+ 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}"`;
+ return `<button id=${"regular_" + this.id} ${type}>${this.name}</button>`;
+ }
+}
+
+function randomLevel() {
+ const max_level = 10;
+ return Math.floor(Math.random() * max_level + 1);
+}
+
+function randomStatus() {
+ return Math.random() < 0.5;
+}
+
+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, name, level, true, 0);
+}
+
+export class PlayerManager extends Manager {
+ // g = new GuestManager();
+
+ constructor() {
+ super("regular", []);
+ console.log("Loading player data.");
+ let data = JSON.parse(localStorage.getItem(this.description));
+ if (data === null || data.length === 0) {
+ console.log("Player data unavailable: " + data);
+ this.generate();
+ return;
+ }
+ for (let i = 0; i < data.length; i++) {
+ let id = Number(data[i].id);
+ let name = data[i].name;
+ let level = data[i].level;
+ let status = data[i].status;
+ let totalPlayed = data[i].totalPlayed;
+ this.listData.push(new Player(id, name, level, status, totalPlayed));
+ }
+ console.log("Loaded " + this.listData.length + " players.");
+ this.render();
+ this.addBehaviour();
+ addLoadBehaviour(this);
+ }
+ generate() {
+ console.log("Generating player data.");
+ let maxPlayerCount = 71;
+ for (let i = 0; i < maxPlayerCount; i++) {
+ let p = new Player(i, "Player " + i, randomLevel(), randomStatus(), 0);
+ this.listData.push(p);
+ }
+ console.log("Generated data for " + this.listData.length + " players");
+ this.store();
+ this.render();
+ }
+ import(data) {
+ var lines = data.split("\n");
+ var players = [];
+ for (var 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 (var i = 0; i < players.length; i++) {
+ players[i].print();
+ }
+ this.listData = players;
+ this.store();
+ this.render();
+ }
+ active(factor) {
+ factor = typeof factor !== "undefined" ? factor : 1;
+ let data = super.active();
+ console.log("Normalize skill by a factor of " + factor + ".");
+ let players = data.map(function (p) {
+ p.skill = Math.ceil(p.skill / factor);
+ return p;
+ });
+ return players;
+ }
+ propose(count) {
+ console.log("Required players are " + count);
+ let factor = Number(document.getElementById("skill").value);
+ console.log("Skill normalized by a factor of " + factor + ".");
+ const activePlayers = this.active(factor);
+ 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;
+ }
+}
+
+// TODO Add load behaviour to constructor
+export 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.import(importData);
+ };
+ reader.readAsText(file);
+ });
+}
diff --git a/src/style.css b/src/style.css
new file mode 100644
index 0000000..8e3b713
--- /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;
+} \ No newline at end of file
diff --git a/src/timer.js b/src/timer.js
new file mode 100644
index 0000000..09189c0
--- /dev/null
+++ b/src/timer.js
@@ -0,0 +1,88 @@
+let minsPerRound = Number(sessionStorage.getItem("minsPerRound")) || 12;
+let timerRunningFlag = Number(sessionStorage.getItem("timerRunningFlag")) || 0;
+let deadline =
+ Number(sessionStorage.getItem("deadline")) || new Date().getTime();
+let clock = 0;
+
+function setTimer(m, s) {
+ let timer = document.getElementById("timer-display");
+ m = m < 10 ? "0" + m : m;
+ s = s < 10 ? "0" + s : s;
+ timer.value = `${m}:${s}`;
+}
+
+function setSessionData() {
+ sessionStorage.setItem("deadline", deadline);
+ sessionStorage.setItem("minsPerRound", minsPerRound);
+ sessionStorage.setItem("timerRunningFlag", timerRunningFlag);
+}
+
+function resetTimer() {
+ clearInterval(clock);
+ let mins = Math.floor(minsPerRound);
+ let secs = (minsPerRound - mins) * 60;
+ setTimer(mins, secs);
+ timerRunningFlag = 0;
+ setSessionData();
+}
+
+function updateTimer(milliseconds) {
+ const m = Math.floor((milliseconds % (1000 * 60 * 60)) / (1000 * 60));
+ const s = Math.floor((milliseconds % (1000 * 60)) / 1000);
+ setTimer(m, s);
+}
+
+function refreshTimer() {
+ const now = new Date().getTime();
+ const millisecondsToDeadline = deadline - now;
+ if (millisecondsToDeadline > 0) {
+ updateTimer(millisecondsToDeadline);
+ return;
+ }
+ resetTimer();
+ const notification = new Notyf({
+ duration: 0,
+ position: { x: "center", y: "top" },
+ dismissible: true,
+ });
+ notification.error("Round completed");
+}
+
+function startTimer(fresh) {
+ if (fresh) {
+ let now = new Date().getTime();
+ const millisecondsToDeadline = minsPerRound * 60 * 1000;
+ deadline = now + millisecondsToDeadline;
+ }
+ timerRunningFlag = 1;
+ setSessionData();
+ clock = setInterval(refreshTimer, 1000);
+}
+
+window.onload = function () {
+ if (timerRunningFlag) startTimer(0);
+ else resetTimer();
+
+ document.getElementById("timer-start").addEventListener("click", () => {
+ if (timerRunningFlag != 0) return;
+ startTimer(1);
+ });
+
+ document.getElementById("timer-stop").addEventListener("click", () => {
+ resetTimer();
+ });
+
+ document.getElementById("timer-inc").addEventListener("click", () => {
+ if (timerRunningFlag == 0) {
+ minsPerRound += 0.5;
+ resetTimer();
+ }
+ });
+
+ document.getElementById("timer-dec").addEventListener("click", () => {
+ if (timerRunningFlag == 0 && minsPerRound != 0) {
+ minsPerRound -= 0.5;
+ resetTimer();
+ }
+ });
+};
diff --git a/src/utils.mjs b/src/utils.mjs
new file mode 100644
index 0000000..06a2519
--- /dev/null
+++ b/src/utils.mjs
@@ -0,0 +1,51 @@
+export class Manager {
+ constructor(description, listData) {
+ this.description = description;
+ this.listData = listData;
+ this.count = listData.length;
+ }
+ store() {
+ let stringData = JSON.stringify(this.listData);
+ localStorage.setItem(this.description, stringData);
+ }
+ render() {
+ let renderedHTML = this.listData
+ .map((i) => {
+ return i.render();
+ })
+ .join("");
+ let elementId = this.description + "_list";
+ document.getElementById(elementId).innerHTML = renderedHTML;
+ this.addBehaviour();
+ }
+ addBehaviour() {
+ for (let idx = 0; idx < this.listData.length; ++idx) {
+ const elementId = this.description + "_" + this.listData[idx].id;
+ const p = document.getElementById(elementId);
+ p.addEventListener("click", () => {
+ this.listData[idx].toggle();
+ this.store();
+ p.classList.toggle("is-success");
+ p.classList.toggle("is-danger");
+ });
+ }
+ }
+ active() {
+ return this.listData.filter(function (c) {
+ return c.status;
+ });
+ }
+ inactive() {
+ return this.listData.filter(function (c) {
+ return !c.status;
+ });
+ }
+}
+
+export 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;
+}