|
Registered User
Join Date: Jul 2014
Posts: 783
|
Created a JS w/HTML testing version. This version is notable for being incomplete and untested code.
I believe that a player leaving should add their opponent to the front of the tier queue, although right now this code puts them at the end of the tier queue. I'm also not sure whether I like the auto-promotion behavior when there are other players on the tier.
This code is particularly untested compared to what I uploaded yesterday. If you save it as an HTML file and open it up in Firefox (it may or may not work in other browsers), you can play with it even if you don't know Javascript. The biggest caveat is that I haven't tested it very much. Another big caveat is that I didn't get around to styling yet, so it's not pretty to look at. I've got a lot of other things to do right now so nothing new will be happening tonight, and probably not tomorrow night either.
PHP Code:
<!DOCTYPE html>
<html>
<head>
<title>Tiered Spar Manager Test</title>
<meta charset="utf-8">
<script>
class Tier {
constructor(index) {
if (!Number.isInteger(index) || index < 0) {
throw new RangeError('Tier index must be a non-negative integer.');
}
this.index = index;
this.level = index + 1;
this.slots = Math.pow(2, index + 1);
this.roster = [];
}
getLevel() {
return this.level;
}
// The total number of slots
getSlotCapacity() {
return this.slots;
}
// the
getMatchCapacity() {
return this.slots / 2;
}
getSlotOccupancy() {
return Math.min(this.slots, this.roster.length);
}
getMatchOccupancy() {
return Math.floor(this.getSlotOccupancy() / 2);
}
getSlotParticipant(offset) {
if (!Number.isInteger(offset) || offset < 0 || this.slots <= offset) {
throw new RangeError(`Slot offset must be a non-negative integer less than ${this.slots}`);
}
return this.roster[offset];
}
getMatchParticipants(offset) {
let participants;
if (!Number.isInteger(offset) || offset < 0 || this.slots / 2 <= offset) {
throw new RangeError(`Match offset must be a non-negative integer less than ${this.slots / 2}`);
}
if (offset < this.getMatchOccupancy()) {
participants = this.roster.slice(2 * offset, 2 * offset + 2);
}
return participants;
}
getMatchOpponent(p) {
let rosterIndex = this.roster.indexOf(p);
if (rosterIndex < 0) {
throw new ReferenceError(`${p} not found among participants`);
}
let queue = this.getQueue();
let queueIndex = queue.indexOf(p);
if (queueIndex >= 0) {
throw new ReferenceError(`${p} is not in a match; p is in queue`);
}
let opponent = this.roster[rosterIndex + (rosterIndex % 2 == 1 ? -1 : 1)];
return opponent;
}
getQueue() {
let queue = [];
if (this.roster.length >= this.slots) {
queue = this.roster.slice(this.slots);
} else if (this.roster.length % 2 == 1) {
queue = [this.roster[this.roster.length - 1]];
}
return queue;
}
requeueParticipant(p) {
let rosterIndex = this.roster.indexOf(p);
if (rosterIndex < 0) {
throw new ReferenceError(`${p} not found among participants`);
}
this.roster.push(...this.roster.splice(rosterIndex, 1))
}
}
class TierManager {
constructor(tiers = 4) {
// establish participant map to look up tier and associated roster by participant
this.pMap = new Map();
// establish tiers
this.tiers = Array.from({length: tiers}, (_, i) => new Tier(i));
}
getTiers() {
return this.tiers;
}
getTierByLevel(level) {
if (!Number.isInteger(level) || level < 1 || this.tiers.length <= level) {
throw new RangeError(`Tier level must be a non-negative integer greater than 0 and less than ${this.tiers.length}`);
}
return this.tiers[level - 1]
}
getTierByParticipant(p) {
if (!this.pMap.has(p)) {
throw new ReferenceError(`${p} not found among participants`);
}
return this.tiers[this.pMap.get(p)];
}
getBottomTier() {
// determine the highest tier in which the roster size is less than the slots available
for (let i = 0; i < this.tiers.length; i++) {
if (this.tiers[i].roster.length < this.tiers[i].slots) {
return this.tiers[i];
}
}
// otherwise the absolute bottom tier
return this.tiers[this.tiers.length - 1];
}
promote(p) {
let bottom = this.getBottomTier();
if (this.pMap.has(p)) {
// determine source tier
let src = this.tiers[this.pMap.get(p)] ;
// only promote if the tier is not already the top
if (src.index > 0) {
// update source roster
src.roster = src.roster.filter(x => x != p);
// promote to at least one tier above the relative bottom tier, at most the top tier, ideally up one tier
let refIndex = Math.max(0, Math.min(src.index - 1, bottom.index - 1));
// determine destination tier
let dst = this.tiers[refIndex];
// update destination roster
dst.roster.push(p);
// track tier reference in participant map
this.pMap.set(p, dst.index);
}
} else {
// determine destination tier
let dst = bottom;
// update roster
dst.roster.push(p);
// track tier reference in participant map
this.pMap.set(p, dst.index);
}
// promote idle participants
this.collapse();
}
demote(p) {
// determine relative bottom tier (removing the player before would affect this)
let bottom = this.getBottomTier()
// determine source tier
let src = this.tiers[this.pMap.get(p)];
// update source roster
src.roster = src.roster.filter(x => x != p);
// promote idle participants
this.collapse();
// determine whether participant remains tiered
if (bottom.index > src.index && bottom.roster.length > 0) {
// determine destination tier
let dst = this.tiers[src.index + 1];
// update destination roster
dst.roster.push(p);
// track tier reference in participant map
this.pMap.set(p, dst.index);
} else {
// remove participant
this.remove(p);
}
}
remove(p) {
// determine source tier
let src = this.tiers[this.pMap.get(p)];
// update source roster
src.roster = src.roster.filter(x => x != p);
// remove reference in participant map
this.pMap.delete(p);
// promote idle participants
this.collapse();
}
collapse() {
// to establish a list of all idle participants
let idle = [];
// determine bottom tier (origin of promotion)
let bottom = this.getBottomTier();
for (let i = bottom.index + 1; i < this.tiers.length; i++) {
// determine source tier
let src = this.tiers[i];
// a participant is idle if the roster is larger than the number of slots
idle = idle.concat(src.roster.slice(src.slots - 1));
// a participant is idle if there are an odd number of roster spots
if (src.roster.length % 2 == 1) {
idle.push(src.roster[src.roster.length - 1]);
}
}
// promote all idle participants in order from top to bottom
for (let i = 0; i < idle.length; i++) {
this.promote(idle[i]);
}
}
}
class TieredSparManager {
constructor(tiers = 4) {
this.tm = new TierManager(tiers);
}
addParticipant(p) {
this.tm.promote(p);
}
removeParticipant(p) {
let tier = this.tm.getTierByParticipant(p);
try {
let opponent = tier.getMatchOpponent();
tier.requeueParticipant(opponent);
this.tm.remove(p);
} catch (e) {
this.tm.remove(p);
}
}
declareSparResult(p1, p2, cmp = -1) {
let p1Wins = cmp < 0;
let p2Wins = cmp > 0;
let draw = cmp == 0;
if (p1Wins || draw) {
this.tm.demote(p2);
} else {
this.tm.promote(p2);
}
if (p2Wins || draw) {
this.tm.demote(p1);
} else {
this.tm.promote(p1);
}
}
getTiers() {
return this.tm.getTiers();
}
toString() {
return JSON.stringify(this.tm.getTiers());
}
}
</script>
<body>
<h1>Tiered Sparring</h1>
<div>
<form id="add-player">
<fieldset>
<legend>Join</legend>
<input type="text" id="add-player-name" name="add-player-name" placeholder="Player Name">
<button type="submit">Add Player</button>
</fieldset>
</form>
</div>
<script>
// display
let tsm = new TieredSparManager();
{
let playerDivs = new Map();
let pListener = event => {
let formData = new FormData(event.target);
if (formData.has('match-win')) {
let winner = formData.get('match-win');
let p1 = formData.get('match-p1');
let p2 = formData.get('match-p2');
let cmp = winner == p1 ? -1 : 1;
tsm.declareSparResult(p1, p2, cmp);
populateHTMLTiers();
} else if (formData.has('match-draw') && formData.get('match-draw') == 'true') {
let p1 = formData.get('match-p1');
let p2 = formData.get('match-p2');
tsm.declareSparResult(p1, p2, 0);
populateHTMLTiers();
} else if (formData.has('player-leave')) {
tsm.removeParticipant(formData.get('player-leave'));
populateHTMLTiers();
}
event.preventDefault();
return false;
};
let createPlayerDiv = playerName => {
let playerDiv = document.createElement('div');
playerDiv.setAttribute('id', `p-${playerDivs.length}`);
playerDiv.setAttribute('class', 'player');
let playerH5 = document.createElement('h5');
playerH5.textContent = playerName;
playerDiv.appendChild(playerH5);
let playerLeaveForm = document.createElement('form');
playerLeaveForm.addEventListener('submit', pListener);
let playerLeaveInput = document.createElement('input');
playerLeaveInput.setAttribute('type', 'hidden');
playerLeaveInput.setAttribute('name', 'player-leave');
playerLeaveInput.setAttribute('value', playerName);
playerLeaveForm.appendChild(playerLeaveInput);
let playerLeaveButton = document.createElement('button');
playerLeaveButton.setAttribute('type', 'submit');
playerLeaveButton.textContent = `${playerName} Leaves`;
playerLeaveForm.appendChild(playerLeaveButton);
playerDiv.appendChild(playerLeaveForm);
playerDivs.set(playerName, playerDiv);
return playerDiv;
};
let createMatchDiv = (tier, matchIndex, p1Name, p2Name) => {
let matchDiv = document.createElement('form');
matchDiv.setAttribute('class','match');
let matchH4 = document.createElement('h4');
matchH4.textContent = `${p1Name} vs ${p2Name}`;
matchDiv.appendChild(matchH4);
let p1Div = playerDivs.get(p1Name);
let p2Div = playerDivs.get(p2Name);
matchDiv.appendChild(p1Div);
matchDiv.appendChild(p2Div);
let p1Input = document.createElement('input');
p1Input.setAttribute('type', 'hidden');
p1Input.setAttribute('name', 'match-p1');
p1Input.setAttribute('value', p1Name);
matchDiv.appendChild(p1Input);
let p2Input = document.createElement('input');
p2Input.setAttribute('type', 'hidden');
p2Input.setAttribute('name', 'match-p2');
p2Input.setAttribute('value', p2Name);
matchDiv.appendChild(p2Input);
let p1WinInput = document.createElement('input');
p1WinInput.setAttribute('type', 'hidden');
p1WinInput.setAttribute('name', 'match-win');
p1WinInput.setAttribute('value', p1Name);
let p2WinInput = document.createElement('input');
p2WinInput.setAttribute('type', 'hidden');
p2WinInput.setAttribute('name', 'match-win');
p2WinInput.setAttribute('value', p2Name);
let matchDrawInput = document.createElement('input');
matchDrawInput.setAttribute('type', 'hidden');
matchDrawInput.setAttribute('name', 'match-draw');
matchDrawInput.setAttribute('value', 'true');
let p1MatchWinButton = document.createElement('button');
p1MatchWinButton.setAttribute('type', 'submit');
p1MatchWinButton.textContent = `${p1Name} Wins`;
let p2MatchWinButton = document.createElement('button');
p2MatchWinButton.setAttribute('type', 'submit');
p2MatchWinButton.textContent = `${p2Name} Wins`;
let matchDrawButton = document.createElement('button');
matchDrawButton.setAttribute('type', 'submit');
matchDrawButton.textContent = `Draw`;
let p1MatchWinForm = document.createElement('form');
p1MatchWinForm.appendChild(p1Input.cloneNode());
p1MatchWinForm.appendChild(p2Input.cloneNode());
p1MatchWinForm.appendChild(p1WinInput);
p1MatchWinForm.appendChild(p1MatchWinButton);
p1MatchWinForm.addEventListener('submit', pListener);
matchDiv.appendChild(p1MatchWinForm);
let p2MatchWinForm = document.createElement('form');
p2MatchWinForm.appendChild(p1Input.cloneNode());
p2MatchWinForm.appendChild(p2Input.cloneNode());
p2MatchWinForm.appendChild(p2WinInput);
p2MatchWinForm.appendChild(p2MatchWinButton);
p2MatchWinForm.addEventListener('submit', pListener);
matchDiv.appendChild(p2MatchWinForm);
let matchDrawForm = document.createElement('form');
matchDrawForm.appendChild(p1Input.cloneNode());
matchDrawForm.appendChild(p2Input.cloneNode());
matchDrawForm.appendChild(matchDrawInput);
matchDrawForm.appendChild(matchDrawButton);
matchDrawForm.addEventListener('submit', pListener);
matchDiv.appendChild(matchDrawForm);
return matchDiv;
};
let createQueueForm = (tier, players) => {
let queueForm = document.createElement('form');
queueForm.setAttribute('class', 'queue');
players.forEach(player => { queueForm.appendChild(playerDivs.get(player)); });
queueForm.addEventListener('submit', pListener);
return queueForm;
}
let populateHTMLTiers = () => {
let matchDivs = document.getElementsByClassName('match');
Array.from(matchDivs).forEach(matchDiv => matchDiv.remove());
let queueForms = document.getElementsByClassName('queue');
Array.from(queueForms).forEach(queueForm => queueForm.remove());
let tiers = tsm.getTiers();
tiers.forEach(function (tier) {
for (let i = 0; i < tier.getMatchOccupancy(); i++) {
let [p1, p2] = tier.getMatchParticipants(i);
let matchDiv = createMatchDiv(tier, i, p1, p2);
let parentId = `t${tier.level}m${i}`;
let parent = document.getElementById(parentId);
parent.appendChild(matchDiv);
}
let queueForm = createQueueForm(tier, tier.getQueue());
let parentId= `t${tier.level}q`;
let parent = document.getElementById(parentId);
parent.appendChild(queueForm);
});
};
let body = document.getElementsByTagName('body')[0];
let tiers = tsm.getTiers();
tiers.forEach(tier => {
let tierDiv = document.createElement('div');
tierDiv.setAttribute('id', `t${tier.level}`);
let tierH2 = document.createElement('h2');
tierH2.textContent = `Tier ${tier.getLevel()}`;
tierDiv.appendChild(tierH2);
let matchCapacity = tier.getMatchCapacity();
for (let i = 0; i < matchCapacity; i++) {
let matchDiv = document.createElement('div');
matchDiv.setAttribute('id', `t${tier.level}m${i}`);
let matchH3 = document.createElement('h3');
matchH3.textContent = `Room ${i + 1}`;
matchDiv.appendChild(matchH3);
tierDiv.appendChild(matchDiv);
}
let queueDiv = document.createElement('div');
queueDiv.setAttribute('id', `t${tier.level}q`);
let queueH3 = document.createElement('h3');
queueH3.textContent = 'Queue';
queueDiv.appendChild(queueH3);
tierDiv.appendChild(queueDiv);
body.appendChild(tierDiv);
});
addPlayerForm = document.getElementById('add-player');
addPlayerForm.addEventListener('submit', function (event) {
let playerName = document.getElementById('add-player-name').value;
if (playerName.length == 0) {
event.preventDefault();
return false;
}
createPlayerDiv(playerName);
tsm.addParticipant(playerName);
populateHTMLTiers();
event.preventDefault();
return false;
});
}
</script>
|