├── snapshots └── beta.PNG ├── javascript ├── init.js ├── Group.js ├── Problem.js ├── KeyPressListener.js ├── Submission.js ├── Resolver.js └── Contestant.js ├── README.md ├── index.html ├── LICENSE └── style └── core.css /snapshots/beta.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamuelCarinhas/MooshakResolver/HEAD/snapshots/beta.PNG -------------------------------------------------------------------------------- /javascript/init.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | const resolver = new Resolver({ 4 | element: document.querySelector('.contestants'), 5 | content: './contest/Content.xml' 6 | }); 7 | 8 | resolver.init(); 9 | 10 | })(); 11 | -------------------------------------------------------------------------------- /javascript/Group.js: -------------------------------------------------------------------------------- 1 | class Group { 2 | 3 | constructor(config) { 4 | this.id = config.getAttribute('xml:id'); 5 | this.name = config.getAttribute('Name'); 6 | this.acronym = config.getAttribute('Acronym'); 7 | this.color = config.getAttribute('Color'); 8 | this.contestants = []; 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MooshakResolver 2 | This is a very simple version of ICPC Resolver for Mooshak contests 3 | 4 | # How to use 5 | After your contest is over, download your contest's zip from Mooshak, and save the Content.xml in the "contest" Folder. 6 | 7 | After this, you only need to run a webserver with the current files. 8 | ## Python http server example 9 | ```bash 10 | python3 -m http.server -d MooshakResolver 11 | ``` 12 | 13 | In order to reveal the submissions, you should use the [ENTER] key. 14 | 15 | # Preview 16 | ![alt text](./snapshots/beta.PNG) 17 | -------------------------------------------------------------------------------- /javascript/Problem.js: -------------------------------------------------------------------------------- 1 | class Problem { 2 | 3 | constructor(config) { 4 | this.id = config.getAttribute('xml:id'); 5 | this.name = config.getAttribute('Name'); 6 | this.color = config.getAttribute('Color'); 7 | } 8 | 9 | compareTo(other) { 10 | if(this.name < other.name) return -1; 11 | if(this.name > other.name) return 1; 12 | return 0; 13 | } 14 | 15 | getElement() { 16 | let header = document.createElement('th'); 17 | header.classList.add('problem'); 18 | header.innerHTML = this.name; 19 | 20 | return header; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /javascript/KeyPressListener.js: -------------------------------------------------------------------------------- 1 | class KeyPressListener { 2 | 3 | constructor(keycode, callback) { 4 | let keySafe = true; 5 | 6 | this.keydownFunction = (e) => { 7 | if (e.code === keycode && keySafe) { 8 | keySafe = false; 9 | callback(); 10 | } 11 | }; 12 | 13 | this.keyupFunction = (e) => { 14 | if (e.code === keycode) { 15 | keySafe = true; 16 | } 17 | }; 18 | 19 | document.addEventListener('keydown', this.keydownFunction); 20 | document.addEventListener('keyup', this.keyupFunction); 21 | } 22 | 23 | unbind() { 24 | document.removeEventListener('keydown', this.keydownFunction); 25 | document.removeEventListener('keyup', this.keyupFunction); 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /javascript/Submission.js: -------------------------------------------------------------------------------- 1 | class Submission { 2 | 3 | constructor({config, contestants, problemSet, contestData}) { 4 | this.contestant = contestants.find(c => c.nameId == config.getAttribute('Team')); 5 | this.problem = problemSet.find(p => p.id == `problems.${config.getAttribute('Problem')}`); 6 | this.classify = config.getAttribute('Classify'); 7 | this.date = config.getAttribute('Date'); 8 | this.contestData = contestData; 9 | } 10 | 11 | update() { 12 | this.contestant.addSubmission(this); 13 | } 14 | 15 | get accepted() { 16 | return this.classify == 'Accepted'; 17 | } 18 | 19 | get pending() { 20 | return this.contestData.stop - this.date < this.contestData.freeze * 60; 21 | } 22 | 23 | get score() { 24 | return parseInt(this.date) - parseInt(this.contestData.start); 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Resolver 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
RankContestantsProblemsTotal Score
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Samuel Carinhas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /style/core.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #FFFFCC; 3 | margin: 20px; 4 | padding: 0; 5 | } 6 | 7 | table { 8 | font-family: arial, sans-serif; 9 | border-collapse: collapse; 10 | width: 100%; 11 | } 12 | 13 | td, th { 14 | border: 1px solid lightgray; 15 | text-align: left; 16 | padding: 8px; 17 | } 18 | 19 | tr:nth-child(even) { 20 | background-color: lightgray; 21 | } 22 | 23 | tr:nth-child(odd) { 24 | background-color: white; 25 | } 26 | 27 | tr.problems { 28 | background-color: gray; 29 | } 30 | 31 | th.problem { 32 | text-align: center; 33 | } 34 | 35 | td { 36 | height: 80px; 37 | width: 15px; 38 | } 39 | 40 | td.not-submitted { 41 | background-color: lightgray; 42 | text-align: center; 43 | font-weight: bold; 44 | } 45 | 46 | td.correct { 47 | background-color: #028a00; 48 | text-align: center; 49 | color: white; 50 | font-weight: bold; 51 | font-size: 11px; 52 | } 53 | 54 | td.pending p { 55 | font-size: 16px; 56 | } 57 | 58 | td.wrong { 59 | background-color: #a80014; 60 | text-align: center; 61 | color: white; 62 | font-weight: bold; 63 | font-size: 11px; 64 | } 65 | 66 | td.pending p { 67 | font-size: 16px; 68 | } 69 | 70 | td.pending { 71 | background-color: #AAAAFF; 72 | text-align: center; 73 | color: white; 74 | font-weight: bold; 75 | font-size: 11px; 76 | } 77 | 78 | td.pending p { 79 | font-size: 16px; 80 | } 81 | 82 | tr.selected { 83 | background-color: #CC9999; 84 | } 85 | 86 | .revealing { 87 | animation: scaling 2s infinite alternate; 88 | } 89 | 90 | @keyframes scaling { 91 | 0% { 92 | background-color: #AAAAFF; 93 | } 94 | 33% { 95 | background-color: #028a00; 96 | } 97 | 66% { 98 | background-color: #a80014; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /javascript/Resolver.js: -------------------------------------------------------------------------------- 1 | class Resolver { 2 | 3 | constructor(config) { 4 | this.element = config.element; 5 | this.content = config.content; 6 | } 7 | 8 | setup(text) { 9 | // Remove garbage from observations 10 | text = text.replace(/Observations="([^\"]|\r|\n)*"/g, ''); 11 | 12 | let parser = new DOMParser(); 13 | let xml = parser.parseFromString(text, 'text/xml'); 14 | let contest = xml.firstChild; 15 | let contestData = { 16 | name: contest.getAttribute('Designation'), 17 | open: contest.getAttribute('Open'), 18 | start: contest.getAttribute('Start'), 19 | stop: contest.getAttribute('Stop'), 20 | close: contest.getAttribute('Close'), 21 | freeze: contest.getAttribute('HideListings') 22 | } 23 | 24 | let problems = contest.getElementsByTagName('Problems')[0]; 25 | this.problemSet = []; 26 | for(let problem of problems.children) 27 | this.problemSet.push(new Problem(problem)); 28 | this.problemSet.sort((a, b) => a.compareTo(b)); 29 | 30 | let groups = contest.getElementsByTagName('Groups')[0]; 31 | this.groups = []; 32 | this.contestants = []; 33 | for(let group of groups.children) { 34 | let g = new Group(group); 35 | this.groups.push(g); 36 | for(let team of group.children) { 37 | let contestant = new Contestant({ 38 | config: team, 39 | group: g, 40 | problems: this.problemSet, 41 | penalty: 20 * 60, 42 | rank: this.contestants.length+1 43 | }); 44 | 45 | g.contestants.push(contestant); 46 | this.contestants.push(contestant); 47 | } 48 | } 49 | 50 | let submissions = contest.getElementsByTagName('Submissions')[0]; 51 | this.submissions = []; 52 | for(let submission of submissions.children) { 53 | this.submissions.push(new Submission({ 54 | config: submission, 55 | contestants: this.contestants, 56 | problemSet: this.problemSet, 57 | contestData: contestData 58 | })); 59 | } 60 | 61 | this.submissions.sort((a, b) => a.date - b.date); 62 | for(let submission of this.submissions) { 63 | submission.update(); 64 | } 65 | 66 | this.contestants.sort((a, b) => a.compareTo(b)); 67 | 68 | this.updateContestantsRank(); 69 | 70 | this.addElements(); 71 | } 72 | 73 | updateContestantsRank() { 74 | let updateRank = 0; 75 | for(let contestant of this.contestants) 76 | if(contestant.rankElement) 77 | contestant.rankElement.innerHTML = ++updateRank; 78 | } 79 | 80 | addElements() { 81 | for(let problem of this.problemSet) { 82 | let problems = this.element.getElementsByClassName('problems')[0]; 83 | problem.htmlElement = problems.appendChild(problem.getElement()); 84 | } 85 | 86 | for(let contestant of this.contestants) { 87 | contestant.htmlElement = this.element.appendChild(contestant.getElement()); 88 | } 89 | } 90 | 91 | bindActions() { 92 | new KeyPressListener('Enter', () => { 93 | this.reveal(); 94 | }); 95 | } 96 | 97 | updateContestantsOrder() { 98 | this.contestants.sort((a, b) => a.compareTo(b)); 99 | this.updateContestantsRank(); 100 | for(let contestant of this.contestants) { 101 | this.element.removeChild(contestant.htmlElement); 102 | } 103 | for(let contestant of this.contestants) { 104 | contestant.htmlElement = this.element.appendChild(contestant.htmlElement); 105 | } 106 | this.updateContestantsRank(); 107 | } 108 | 109 | reveal() { 110 | this.updateContestantsOrder(); 111 | let revealContestant = undefined; 112 | for(let contestant of this.contestants) { 113 | if(contestant.submissionsQueue.length > 0 || contestant.final) 114 | revealContestant = contestant; 115 | } 116 | if(revealContestant) { 117 | if(revealContestant.submissionsQueue.length == 0 && revealContestant.final) { 118 | if(this.last) 119 | this.last.deselect(); 120 | revealContestant.final = false; 121 | revealContestant.select(); 122 | this.last = revealContestant; 123 | } else { 124 | if(this.reviewing) { 125 | revealContestant.reveal(); 126 | this.reviewing = false; 127 | } else { 128 | if(this.last) 129 | this.last.deselect(); 130 | revealContestant.select(); 131 | revealContestant.reveal(); 132 | this.reviewing = true; 133 | this.last = revealContestant; 134 | } 135 | } 136 | this.updateContestantsOrder(); 137 | revealContestant.scrollIntoView(); 138 | } else { 139 | this.last.deselect(); 140 | } 141 | } 142 | 143 | init() { 144 | console.log('Resolver started'); 145 | 146 | fetch(this.content) 147 | .then(response => response.text()) 148 | .then(text => { 149 | this.setup(text); 150 | this.bindActions(); 151 | this.updateContestantsOrder(); 152 | }); 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /javascript/Contestant.js: -------------------------------------------------------------------------------- 1 | let scoreUtils = { 2 | noSubmission: -2, 3 | wrong: -1 4 | } 5 | 6 | class Contestant { 7 | 8 | constructor({config, group, problems, penalty, rank}) { 9 | this.id = config.getAttribute('xml:id'); 10 | this.name = config.getAttribute('Name'); 11 | this.group = group; 12 | this.problems = problems; 13 | this.penalty = penalty; 14 | this.rank = rank; 15 | this.score = Array(this.problems.length).fill(scoreUtils.noSubmission); 16 | this.submissionTimes = Array(this.problems.length).fill(0); 17 | this.pending = Array(this.problems.length).fill(false); 18 | this.attempts = Array(this.problems.length).fill(0); 19 | this.htmlSubmissions = []; 20 | this.submissionsQueue = []; 21 | this.nameId = this.id.replace(`${this.group.id}.`, ''); 22 | this.final = true; 23 | } 24 | 25 | addSubmission(submission) { 26 | let index = this.problems.indexOf(submission.problem); 27 | 28 | // This will ignore every submission after the first accepted 29 | // Remove this condition if you don't want this feature 30 | if(this.score[index] > 0) 31 | return; 32 | 33 | this.attempts[index] += !submission.accepted; 34 | 35 | this.score[index] = submission.accepted ? submission.score : scoreUtils.wrong; 36 | this.submissionTimes[index] = submission.score; 37 | this.pending[index] = submission.pending; 38 | 39 | if(submission.pending) { 40 | let aux = this.submissionsQueue.indexOf(index); 41 | if(aux !== -1) 42 | this.submissionsQueue.splice(aux, 1); 43 | this.submissionsQueue.push(index); 44 | } 45 | } 46 | 47 | reveal() { 48 | if(this.submissionsQueue.length === 0) 49 | return false; 50 | 51 | let index = this.submissionsQueue[0]; 52 | if(this.subRevealing) { 53 | this.pending[index] = false; 54 | this.subRevealing = false; 55 | this.htmlSubmissions[index].classList.remove('revealing'); 56 | this.htmlSubmissions[index].classList.remove('pending'); 57 | this.evaluate(this.htmlSubmissions[index], index); 58 | this.submissionsQueue.shift(); 59 | } else { 60 | this.subRevealing = true; 61 | this.htmlSubmissions[index].classList.add('revealing'); 62 | } 63 | return true; 64 | } 65 | 66 | get allScore() { 67 | let score = 0; 68 | for(let i = 0; i < this.problems.length; i++) 69 | if(!this.pending[i] && this.score[i] > 0) 70 | score += this.score[i] + this.penalty * this.attempts[i]; 71 | return score; 72 | } 73 | 74 | get problemsSolved() { 75 | let solved = 0; 76 | for(let i = 0; i < this.problems.length; i++) 77 | solved += !this.pending[i] && this.score[i] > 0; 78 | return solved; 79 | } 80 | 81 | evaluate(data, i) { 82 | let score = this.score[i]; 83 | if(this.pending[i]) { 84 | data.classList.add('pending'); 85 | data.innerHTML = `

?

\n${new Date(this.submissionTimes[i] * 1000).toISOString().substr(11, 8)} (${this.attempts[i]})

`; 86 | } else if(score == scoreUtils.noSubmission) { 87 | data.classList.add('not-submitted'); 88 | data.innerHTML = '-'; 89 | } else if(score == scoreUtils.wrong) { 90 | data.classList.add('wrong'); 91 | data.innerHTML = `

\n${new Date(this.submissionTimes[i] * 1000).toISOString().substr(11, 8)} (${this.attempts[i]})

`; 92 | } else { 93 | data.classList.add('correct'); 94 | this.problemsSolved + '/' + this.problems.length; 95 | this.htmlScoreData.innerHTML = new Date(this.allScore * 1000).toISOString().substr(11, 8); 96 | this.htmlProblemData.innerHTML = this.problemsSolved + '/' + this.problems.length; 97 | data.innerHTML = `

\n${new Date(this.submissionTimes[i] * 1000).toISOString().substr(11, 8)} (${this.attempts[i]})

`; 98 | } 99 | } 100 | 101 | scrollIntoView() { 102 | this.htmlElement.scrollIntoView({ 103 | behavior: 'smooth', 104 | block: 'center' 105 | }); 106 | } 107 | 108 | select() { 109 | this.selected = true; 110 | this.htmlElement.classList.add('selected'); 111 | this.scrollIntoView(); 112 | } 113 | 114 | deselect() { 115 | this.selected = false; 116 | this.htmlElement.classList.remove('selected'); 117 | } 118 | 119 | getElement() { 120 | let row = document.createElement('tr'); 121 | row.classList.add('contestant'); 122 | 123 | if(this.selected) 124 | row.classList.add('selected'); 125 | 126 | let rank = document.createElement('td'); 127 | rank.classList.add('name'); 128 | rank.innerHTML = this.rank; 129 | 130 | this.rankElement = rank; 131 | 132 | row.appendChild(rank); 133 | 134 | let data = document.createElement('td'); 135 | data.classList.add('name'); 136 | data.innerHTML = this.group.acronym + ' ' + this.name; 137 | row.appendChild(data); 138 | 139 | let problemsData = document.createElement('td'); 140 | problemsData.classList.add('problemData'); 141 | problemsData.innerHTML = this.problemsSolved + '/' + this.problems.length; 142 | this.htmlProblemData = row.appendChild(problemsData); 143 | 144 | let scoreData = document.createElement('td'); 145 | scoreData.classList.add('scoreData'); 146 | scoreData.innerHTML = new Date(this.allScore * 1000).toISOString().substr(11, 8); 147 | this.htmlScoreData = row.appendChild(scoreData); 148 | 149 | for(let i = 0; i < this.problems.length; i++) { 150 | let data = document.createElement('td'); 151 | 152 | this.evaluate(data, i); 153 | 154 | this.htmlSubmissions.push(row.appendChild(data)); 155 | } 156 | 157 | return row; 158 | } 159 | 160 | compareTo(other) { 161 | if(this.problemsSolved != other.problemsSolved) 162 | return other.problemsSolved - this.problemsSolved; 163 | return this.allScore - other.allScore; 164 | } 165 | } 166 | --------------------------------------------------------------------------------