├── 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 | 
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 | | Rank |
12 | Contestants |
13 | Problems |
14 | Total Score |
15 |
16 |
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 |
--------------------------------------------------------------------------------