{
83 | const html = await fetchHtml(url);
84 | const $ = cheerio.load(html);
85 |
86 | // Team Swiss pairings table has nested tables
87 | if ($('.CRs1 td').find('table').length > 1) {
88 | return parsePairingsForTeamSwiss(html);
89 | }
90 |
91 | return parsePairingsForIndividualEvent(html, players);
92 | }
93 |
94 | function parsePairingsForTeamSwiss(html: string): Pairing[] {
95 | const $ = cheerio.load(html);
96 | const pairings: Pairing[] = [];
97 |
98 | const headers: string[] = $('.CRs1 tr th')
99 | .first()
100 | .parent()
101 | .children()
102 | .map((_index, element) => $(element).text().trim())
103 | .get();
104 |
105 | let teams: string[] = [];
106 |
107 | $('.CRs1 tr').each((_index, element) => {
108 | // find the header rows that include the team names
109 | if ($(element).hasClass('CRg1b') && $(element).find('th').length > 0) {
110 | teams = $(element)
111 | .find('th')
112 | .filter((_index, element) => $(element).text().includes('\u00a0\u00a0'))
113 | .map((_index, element) => $(element).text().trim())
114 | .get();
115 | return;
116 | }
117 |
118 | // ignore rows that do not have pairings
119 | if ($(element).find('table').length === 0) {
120 | return;
121 | }
122 |
123 | const boardNumber = $(element).children().eq(0).text().trim();
124 | const white = $(element).find('table').find('div.FarbewT').parentsUntil('table').last().text().trim();
125 | const black = $(element).find('table').find('div.FarbesT').parentsUntil('table').last().text().trim();
126 |
127 | const rating1 = headers.includes('Rtg')
128 | ? parseInt($(element).children().eq(headers.indexOf('Rtg')).text().trim())
129 | : undefined;
130 | const rating2 = headers.includes('Rtg')
131 | ? parseInt($(element).children().eq(headers.lastIndexOf('Rtg')).text().trim())
132 | : undefined;
133 |
134 | const username1 = headers.includes('Club/City')
135 | ? $(element).children().eq(headers.indexOf('Club/City')).text().trim()
136 | : undefined;
137 | const username2 = headers.includes('Club/City')
138 | ? $(element).children().eq(headers.lastIndexOf('Club/City')).text().trim()
139 | : undefined;
140 |
141 | // which color indicator comes first: div.FarbewT or div.FarbesT?
142 | const firstDiv = $(element).find('table').find('div.FarbewT, div.FarbesT').first();
143 |
144 | if ($(firstDiv).hasClass('FarbewT')) {
145 | pairings.push({
146 | white: {
147 | name: white,
148 | team: teams[0],
149 | rating: rating1,
150 | lichess: username1,
151 | },
152 | black: {
153 | name: black,
154 | team: teams[1],
155 | rating: rating2,
156 | lichess: username2,
157 | },
158 | reversed: false,
159 | board: boardNumber,
160 | });
161 | } else if ($(firstDiv).hasClass('FarbesT')) {
162 | pairings.push({
163 | white: {
164 | name: white,
165 | team: teams[1],
166 | rating: rating2,
167 | lichess: username2,
168 | },
169 | black: {
170 | name: black,
171 | team: teams[0],
172 | rating: rating1,
173 | lichess: username1,
174 | },
175 | reversed: true,
176 | board: boardNumber,
177 | });
178 | } else {
179 | throw new Error('Could not parse Pairings table');
180 | }
181 | });
182 |
183 | return pairings;
184 | }
185 |
186 | function parsePairingsForIndividualEvent(html: string, players?: Player[]): Pairing[] {
187 | const $ = cheerio.load(html);
188 | const pairings: Pairing[] = [];
189 |
190 | const headers: string[] = $('.CRs1 tr th')
191 | .first()
192 | .parent()
193 | .children()
194 | .map((_index, element) => $(element).text().trim())
195 | .get();
196 |
197 | $('.CRs1 tr').each((_index, element) => {
198 | // ignore certain table headings: rows with less than 2 's
199 | if ($(element).find('td').length <= 2) {
200 | return;
201 | }
202 |
203 | const boardNumber = $(element).children().eq(0).text().trim();
204 | const whiteName = $(element).children().eq(headers.indexOf('White')).text().trim();
205 | const blackName = $(element).children().eq(headers.lastIndexOf('Black')).text().trim();
206 |
207 | pairings.push({
208 | white: players?.find(player => player.name === whiteName) ?? { name: whiteName },
209 | black: players?.find(player => player.name === blackName) ?? { name: blackName },
210 | reversed: false,
211 | board: boardNumber,
212 | });
213 | });
214 |
215 | return pairings;
216 | }
217 |
218 | export function saveUrls(bulkPairingId: string, pairingsUrl: string, playersUrl?: string) {
219 | const urls = new Map();
220 |
221 | const savedUrls = localStorage.getItem('cr-urls');
222 | if (savedUrls) {
223 | const parsed: { [key: string]: SavedPlayerUrls } = JSON.parse(savedUrls);
224 | Object.keys(parsed).forEach(key => urls.set(key, parsed[key]));
225 | }
226 |
227 | urls.set(bulkPairingId, { pairingsUrl, playersUrl });
228 | localStorage.setItem('cr-urls', JSON.stringify(Object.fromEntries(urls)));
229 | }
230 |
231 | export function getUrls(bulkPairingId: string): SavedPlayerUrls | undefined {
232 | const savedUrls = localStorage.getItem('cr-urls');
233 |
234 | if (!savedUrls) {
235 | return undefined;
236 | }
237 |
238 | const parsed: { [key: string]: SavedPlayerUrls } = JSON.parse(savedUrls);
239 | return parsed[bulkPairingId];
240 | }
241 |
242 | export function filterRound(pairings: Pairing[], round: number): Pairing[] {
243 | const boardMap = new Map();
244 | const filtered: Pairing[] = [];
245 |
246 | for (const pairing of pairings) {
247 | const count = boardMap.get(pairing.board) ?? 0;
248 | if (count === round - 1) {
249 | filtered.push(pairing);
250 | }
251 | boardMap.set(pairing.board, count + 1);
252 | }
253 |
254 | return filtered;
255 | }
256 |
--------------------------------------------------------------------------------
/src/scraper/tests/fixtures/team-round-robin-pairings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | | Round 1 |
7 |
8 |
9 | | Bo. |
10 | 1 |
11 | KPMG Norway |
12 | Rtg |
13 | Club/City |
14 | - |
15 | 4 |
16 | Nanjing Spark Chess Technology Co.Ltd. |
17 | Rtg |
18 | Club/City |
19 | 0 : 4 |
20 |
21 |
22 | | 1.1 |
23 | |
24 |
25 |
26 |
27 |
28 | |
29 | Kyrkjebo, Hanna B. |
30 |
31 |
32 |
33 | |
34 | 1899 |
35 | watchmecheck |
36 | - |
37 | IM |
38 |
39 |
40 |
41 |
42 | |
43 | Liu, Zhaoqi |
44 |
45 |
46 |
47 | |
48 | 2337 |
49 | lzqupup |
50 | 0 - 1 |
51 |
52 |
53 | | 1.2 |
54 | |
55 |
56 |
57 |
58 |
59 | |
60 | Grimsrud, Oyvind |
61 |
62 |
63 |
64 | |
65 | 1836 |
66 | Bruneratseth |
67 | - |
68 | |
69 |
70 |
71 |
72 |
73 | |
74 | Du, Chunhui |
75 |
76 |
77 |
78 | |
79 | 2288 |
80 | duchunhui |
81 | 0 - 1 |
82 |
83 |
84 | | 1.3 |
85 | |
86 |
87 |
88 |
89 |
90 | |
91 | Bruvold, Cathrine |
92 |
93 |
94 |
95 | |
96 | 1611 |
97 | Cbruvold |
98 | - |
99 | |
100 |
101 |
102 |
103 |
104 | |
105 | Wei, Siyu |
106 |
107 |
108 |
109 | |
110 | 2106 |
111 | qwqwqyg |
112 | 0 - 1 |
113 |
114 |
115 | | 1.4 |
116 | |
117 |
118 |
119 |
120 |
121 | |
122 | Holmeide, Fredrik |
123 |
124 |
125 |
126 | |
127 | 0 |
128 | yolofredrik |
129 | - |
130 | |
131 |
132 |
133 |
134 |
135 | |
136 | Chen, Yiru |
137 |
138 |
139 |
140 | |
141 | 1897 |
142 | tongccc |
143 | 0 - 1 |
144 |
145 |
146 | | Bo. |
147 | 2 |
148 | Golomt Bank of Mongolia |
149 | Rtg |
150 | Club/City |
151 | - |
152 | 3 |
153 | PROBIT Sp. z o.o. |
154 | Rtg |
155 | Club/City |
156 | 2½:1½ |
157 |
158 |
159 | | 2.1 |
160 | FM |
161 |
162 |
163 |
164 |
165 | |
166 | Gan-Od, Sereenen |
167 |
168 |
169 |
170 | |
171 | 2294 |
172 | Gan-Od_Sereenen |
173 | - |
174 | WGM |
175 |
176 |
177 |
178 |
179 | |
180 | Zawadzka, Jolanta |
181 |
182 |
183 |
184 | |
185 | 2236 |
186 | Evil_Kitten |
187 | ½ - ½ |
188 |
189 |
190 | | 2.2 |
191 | |
192 |
193 |
194 |
195 |
196 | |
197 | Tuvshinbaatar, Dondovdorj |
198 |
199 |
200 |
201 | |
202 | 2080 |
203 | mongolian_monster |
204 | - |
205 | IM |
206 |
207 |
208 |
209 |
210 | |
211 | Zawadzki, Stanislaw |
212 |
213 |
214 |
215 | |
216 | 2451 |
217 | rgkkk |
218 | ½ - ½ |
219 |
220 |
221 | | 2.3 |
222 | |
223 |
224 |
225 |
226 |
227 | |
228 | Amgalan, Ganbaatar |
229 |
230 |
231 |
232 | |
233 | 1969 |
234 | Amaahai0602 |
235 | - |
236 | FM |
237 |
238 |
239 |
240 |
241 | |
242 | Miroslaw, Michal |
243 |
244 |
245 |
246 | |
247 | 2199 |
248 | mireq |
249 | ½ - ½ |
250 |
251 |
252 | | 2.4 |
253 | |
254 |
255 |
256 |
257 |
258 | |
259 | Munkhtur, Dagva |
260 |
261 |
262 |
263 | |
264 | 1853 |
265 | okchessboard |
266 | - |
267 | |
268 |
269 |
270 |
271 |
272 | |
273 | Gromek, Tomasz |
274 |
275 |
276 |
277 | |
278 | 2030 |
279 | tomsun92 |
280 | 1 - 0 |
281 |
282 |
283 |
284 |
--------------------------------------------------------------------------------
/src/page/bulkNew.ts:
--------------------------------------------------------------------------------
1 | import { h } from 'snabbdom';
2 | import page from 'page';
3 | import { App } from '../app';
4 | import type { Me } from '../auth';
5 | import { type Feedback, formData, isSuccess, responseToFeedback } from '../form';
6 | import { gameRuleKeys, gameRules } from '../util';
7 | import * as form from '../view/form';
8 | import layout from '../view/layout';
9 | import { type Pairing, filterRound, getPairings, getPlayers, saveUrls } from '../scraper/scraper';
10 | import { bulkPairing } from '../endpoints';
11 | import { href } from '../view/util';
12 | import createClient from 'openapi-fetch';
13 | import type { paths } from '@lichess-org/types';
14 |
15 | interface Tokens {
16 | [username: string]: string;
17 | }
18 | interface Result {
19 | id: string;
20 | games: {
21 | id: string;
22 | white: string;
23 | black: string;
24 | }[];
25 | pairAt: number;
26 | startClocksAt: number;
27 | }
28 |
29 | export class BulkNew {
30 | feedback: Feedback = undefined;
31 | readonly app: App;
32 | readonly me: Me;
33 | constructor(app: App, me: Me) {
34 | this.app = app;
35 | this.me = me;
36 | }
37 | redraw = () => this.app.redraw(this.render());
38 | render = () =>
39 | layout(
40 | this.app,
41 | h('div', [
42 | h('nav.mt-5.breadcrumb', [
43 | h('span.breadcrumb-item', h('a', { attrs: href(bulkPairing.path) }, 'Schedule games')),
44 | h('span.breadcrumb-item.active', 'New bulk pairing'),
45 | ]),
46 | h('h1.mt-5', 'Schedule games'),
47 | h('p.lead', [
48 | 'Uses the ',
49 | h(
50 | 'a',
51 | { attrs: { href: 'https://lichess.org/api#tag/Bulk-pairings/operation/bulkPairingCreate' } },
52 | 'Lichess bulk pairing API',
53 | ),
54 | ' to create a bunch of games at once.',
55 | ]),
56 | h('p', [
57 | 'Requires the ',
58 | h('strong', 'API Challenge admin'),
59 | ' permission to generate the player challenge tokens automatically.',
60 | ]),
61 | this.renderForm(),
62 | ]),
63 | );
64 |
65 | private onSubmit = async (form: FormData) => {
66 | const get = (key: string) => form.get(key) as string;
67 | const dateOf = (key: string) => get(key) && new Date(get(key)).getTime();
68 | try {
69 | const playersTxt = get('players');
70 | let pairingNames: [string, string][];
71 | try {
72 | pairingNames = playersTxt
73 | .toLowerCase()
74 | .split('\n')
75 | .map(line =>
76 | line
77 | .trim()
78 | .replace(/[\s,]+/g, ' ')
79 | .split(' '),
80 | )
81 | .map(names => [names[0].trim(), names[1].trim()]);
82 | } catch (err) {
83 | throw 'Invalid players format';
84 | }
85 | const tokens = await this.adminChallengeTokens(pairingNames.flat());
86 | const randomColor = !!get('randomColor');
87 | const sortFn = () => (randomColor ? Math.random() - 0.5 : 0);
88 | const pairingTokens: [string, string][] = pairingNames.map(
89 | duo =>
90 | duo
91 | .map(name => {
92 | if (!tokens[name]) throw `Missing token for ${name}, is that an active Lichess player?`;
93 | return tokens[name];
94 | })
95 | .sort(sortFn) as [string, string],
96 | );
97 | const rules = gameRuleKeys.filter(key => !!get(key));
98 | const req = this.me.httpClient(`${this.app.config.lichessHost}/api/bulk-pairing`, {
99 | method: 'POST',
100 | body: formData({
101 | players: pairingTokens.map(([white, black]) => `${white}:${black}`).join(','),
102 | 'clock.limit': parseFloat(get('clockLimit')) * 60,
103 | 'clock.increment': get('clockIncrement'),
104 | variant: get('variant'),
105 | rated: !!get('rated'),
106 | fen: get('fen'),
107 | message: get('message'),
108 | pairAt: dateOf('pairAt'),
109 | startClocksAt: dateOf('startClocksAt'),
110 | rules: rules.join(','),
111 | }),
112 | });
113 | this.feedback = await responseToFeedback(req);
114 |
115 | if (isSuccess(this.feedback)) {
116 | if (!!get('armageddon')) {
117 | const addTimeResponses = new Map();
118 | for (const game of this.feedback.result.games) {
119 | const client = createClient({
120 | baseUrl: this.app.config.lichessHost,
121 | headers: {
122 | Authorization: `Bearer ${tokens[game.black]}`,
123 | },
124 | });
125 | const resp = await client.POST('/api/round/{gameId}/add-time/{seconds}', {
126 | params: {
127 | path: {
128 | gameId: game.id,
129 | seconds: 60,
130 | },
131 | },
132 | });
133 | addTimeResponses.set(game.id, resp.response.status);
134 | }
135 |
136 | const alerts: string[] = [];
137 | addTimeResponses.forEach((status, gameId) => {
138 | if (status !== 200) {
139 | alerts.push(`Failed to add armageddon time to game ${gameId}, status ${status}`);
140 | }
141 | });
142 | if (alerts.length) {
143 | alert(alerts.join('\n'));
144 | }
145 | }
146 |
147 | saveUrls(this.feedback.result.id, get('cr-pairings-url'), get('cr-players-url'));
148 | page(`/endpoint/schedule-games/${this.feedback.result.id}`);
149 | }
150 | } catch (err) {
151 | console.warn(err);
152 | this.feedback = {
153 | error: { players: JSON.stringify(err) },
154 | };
155 | }
156 | this.redraw();
157 | document.getElementById('endpoint-form')?.scrollIntoView({ behavior: 'smooth' });
158 | };
159 |
160 | private adminChallengeTokens = async (users: string[]): Promise => {
161 | const res = await this.me.httpClient(`${this.app.config.lichessHost}/api/token/admin-challenge`, {
162 | method: 'POST',
163 | body: formData({
164 | users: users.join(','),
165 | description: 'Tournament pairings from the Lichess team',
166 | }),
167 | });
168 | const json = await res.json();
169 | if (json.error) throw json.error;
170 | return json;
171 | };
172 |
173 | private renderForm = () =>
174 | form.form(this.onSubmit, [
175 | form.feedback(this.feedback),
176 | h('div.mb-3', [
177 | h('div.row', [
178 | h('div.col-md-6', [
179 | form.label('Players', 'players'),
180 | h('textarea#players.form-control', {
181 | attrs: {
182 | name: 'players',
183 | style: 'height: 100px',
184 | required: true,
185 | spellcheck: 'false',
186 | },
187 | }),
188 | h('p.form-text', [
189 | 'Two usernames per line, each line is a game.',
190 | h('br'),
191 | 'First username gets the white pieces, unless randomized by the switch below.',
192 | ]),
193 | h('div.form-check.form-switch', form.checkboxWithLabel('randomColor', 'Randomize colors')),
194 | h(
195 | 'button.btn.btn-secondary.btn-sm.mt-2',
196 | {
197 | attrs: {
198 | type: 'button',
199 | },
200 | on: {
201 | click: () =>
202 | this.validateUsernames(document.getElementById('players') as HTMLTextAreaElement),
203 | },
204 | },
205 | 'Validate Lichess usernames',
206 | ),
207 | ]),
208 | h('div.col-md-6', [
209 | h('details', [
210 | h('summary.text-muted.form-label', 'Or load the players and pairings from another website'),
211 | h('div.card.card-body', [form.loadPlayersFromUrl()]),
212 | h(
213 | 'button.btn.btn-secondary.btn-sm.mt-3',
214 | {
215 | attrs: {
216 | type: 'button',
217 | },
218 | on: {
219 | click: () =>
220 | this.loadPairingsFromChessResults(
221 | document.getElementById('cr-pairings-url') as HTMLInputElement,
222 | document.getElementById('cr-players-url') as HTMLInputElement,
223 | ),
224 | },
225 | },
226 | 'Load pairings',
227 | ),
228 | ]),
229 | ]),
230 | ]),
231 | ]),
232 | form.clock(),
233 | h(
234 | 'div.form-check.form-switch.mb-3',
235 | form.checkboxWithLabel('armageddon', 'Armageddon? (+60 seconds for white)'),
236 | ),
237 | h('div.form-check.form-switch.mb-3', form.checkboxWithLabel('rated', 'Rated games', true)),
238 | form.variant(),
239 | form.fen(),
240 | h('div.mb-3', [
241 | form.label('Inbox message', 'message'),
242 | h(
243 | 'textarea#message.form-control',
244 | {
245 | attrs: {
246 | name: 'message',
247 | style: 'height: 100px',
248 | },
249 | },
250 | 'Your game with {opponent} is ready: {game}.',
251 | ),
252 | h('p.form-text', [
253 | 'Message that will be sent to each player, when the game is created. It is sent from your user account.',
254 | h('br'),
255 | h('code', '{opponent}'),
256 | ' and ',
257 | h('code', '{game}'),
258 | ' are placeholders that will be replaced with the opponent and the game URLs.',
259 | h('br'),
260 | 'The ',
261 | h('code', '{game}'),
262 | ' placeholder is mandatory.',
263 | ]),
264 | ]),
265 | form.specialRules(gameRules),
266 | h('div.mb-3', [
267 | form.label('When to create the games', 'pairAt'),
268 | h('input#pairAt.form-control', {
269 | attrs: {
270 | type: 'datetime-local',
271 | name: 'pairAt',
272 | },
273 | }),
274 | h('p.form-text', 'Leave empty to create the games immediately.'),
275 | ]),
276 | h('div.mb-3', [
277 | form.label('When to start the clocks', 'startClocksAt'),
278 | h('input#startClocksAt.form-control', {
279 | attrs: {
280 | type: 'datetime-local',
281 | name: 'startClocksAt',
282 | },
283 | }),
284 | h('p.form-text', [
285 | 'Date at which the clocks will be automatically started.',
286 | h('br'),
287 | 'Note that the clocks can start earlier than specified, if players start making moves in the game.',
288 | h('br'),
289 | 'Leave empty so that the clocks only start when players make moves.',
290 | ]),
291 | ]),
292 | form.submit('Schedule the games'),
293 | ]);
294 |
295 | private validateUsernames = async (textarea: HTMLTextAreaElement) => {
296 | const usernames = textarea.value.match(/(<.*?>)|(\S+)/g);
297 | if (!usernames) return;
298 |
299 | let validUsernames: string[] = [];
300 |
301 | const chunkSize = 300;
302 | for (let i = 0; i < usernames.length; i += chunkSize) {
303 | const res = await this.me.httpClient(`${this.app.config.lichessHost}/api/users`, {
304 | method: 'POST',
305 | body: usernames.slice(i, i + chunkSize).join(', '),
306 | headers: {
307 | 'Content-Type': 'text/plain',
308 | },
309 | });
310 | const users = await res.json();
311 | validUsernames = validUsernames.concat(users.filter((u: any) => !u.disabled).map((u: any) => u.id));
312 | }
313 |
314 | const invalidUsernames = usernames.filter(username => !validUsernames.includes(username.toLowerCase()));
315 | if (invalidUsernames.length) {
316 | alert(`Invalid usernames: ${invalidUsernames.join(', ')}`);
317 | } else {
318 | alert('All usernames are valid!');
319 | }
320 | };
321 |
322 | private loadPairingsFromChessResults = async (
323 | pairingsInput: HTMLInputElement,
324 | playersInput: HTMLInputElement,
325 | ) => {
326 | try {
327 | const pairingsUrl = pairingsInput.value;
328 | const playersUrl = playersInput.value;
329 |
330 | const players = playersUrl ? await getPlayers(playersUrl) : undefined;
331 | const pairings = await getPairings(pairingsUrl, players);
332 | this.insertPairings(this.selectRound(pairings));
333 | } catch (err) {
334 | alert(err);
335 | }
336 | };
337 |
338 | private selectRound(pairings: Pairing[]) {
339 | const numRounds = pairings.filter(p => p.board === '1.1').length;
340 | if (numRounds > 1) {
341 | const selectedRound = prompt(
342 | `There are ${numRounds} rounds in this tournament. Which round do you want to load? (1-${numRounds}, or "all" for no filtering)`,
343 | );
344 | if (selectedRound === null) {
345 | throw new Error('Invalid round number');
346 | } else if (selectedRound === 'all') {
347 | return pairings;
348 | }
349 | const roundNum = parseInt(selectedRound);
350 | if (isNaN(roundNum) || roundNum < 1 || roundNum > numRounds) {
351 | throw new Error('Invalid round number');
352 | }
353 | return filterRound(pairings, roundNum);
354 | }
355 | return pairings;
356 | }
357 |
358 | private insertPairings(pairings: Pairing[]) {
359 | pairings.forEach(pairing => {
360 | const playersTxt = (document.getElementById('players') as HTMLTextAreaElement).value;
361 |
362 | const white = pairing.white.lichess || `<${pairing.white.name}>`;
363 | const black = pairing.black.lichess || `<${pairing.black.name}>`;
364 |
365 | const newLine = `${white} ${black}`;
366 | (document.getElementById('players') as HTMLTextAreaElement).value =
367 | playersTxt + (playersTxt ? '\n' : '') + newLine;
368 | });
369 | }
370 | }
371 |
--------------------------------------------------------------------------------
/src/page/bulkShow.ts:
--------------------------------------------------------------------------------
1 | import { h } from 'snabbdom';
2 | import { App } from '../app';
3 | import type { Me } from '../auth';
4 | import layout from '../view/layout';
5 | import { href, timeFormat } from '../view/util';
6 | import type { Bulk, BulkId, Game, Player, Username } from '../model';
7 | import { type Stream, readStream } from '../ndJsonStream';
8 | import { bulkPairing } from '../endpoints';
9 | import { sleep, ucfirst } from '../util';
10 | import { loadPlayersFromUrl } from '../view/form';
11 | import { type Pairing, getPairings, getPlayers, getUrls, saveUrls } from '../scraper/scraper';
12 |
13 | type Result = '*' | '1-0' | '0-1' | '½-½' | '+--' | '--+';
14 | interface FormattedGame {
15 | id: string;
16 | moves: number;
17 | result: Result;
18 | players: { white: Player; black: Player };
19 | fullNames: { white?: string; black?: string };
20 | }
21 |
22 | export class BulkShow {
23 | bulk?: Bulk;
24 | games: FormattedGame[] = [];
25 | gameStream?: Stream;
26 | liveUpdate = true;
27 | fullNames = new Map();
28 | crPairings: Pairing[] = [];
29 | readonly app: App;
30 | readonly me: Me;
31 | readonly id: BulkId;
32 | constructor(app: App, me: Me, id: BulkId) {
33 | this.app = app;
34 | this.me = me;
35 | this.id = id;
36 | this.loadBulk().then(() => this.loadGames());
37 | }
38 | loadBulk = async () => {
39 | const res = await this.me.httpClient(`${this.app.config.lichessHost}/api/bulk-pairing/${this.id}`);
40 | this.bulk = await res.json();
41 | this.redraw();
42 | };
43 | loadGames = async (forceUpdate: boolean = false): Promise => {
44 | this.gameStream?.close();
45 | if (this.bulk) {
46 | const res = await this.me.httpClient(`${this.app.config.lichessHost}/api/games/export/_ids`, {
47 | method: 'POST',
48 | body: this.bulk.games.map(game => game.id).join(','),
49 | headers: { Accept: 'application/x-ndjson' },
50 | });
51 | const handler = (g: Game) => {
52 | const moves = g.moves ? g.moves.split(' ').length : 0;
53 | const game: FormattedGame = {
54 | id: g.id,
55 | players: g.players,
56 | fullNames: {
57 | white: this.fullNames.get(g.players.white.user.id),
58 | black: this.fullNames.get(g.players.black.user.id),
59 | },
60 | moves,
61 | result: this.gameResult(g.status, g.winner, moves),
62 | };
63 | const exists = this.games.findIndex(g => g.id === game.id);
64 | if (exists >= 0) this.games[exists] = game;
65 | else this.games.push(game);
66 | this.sortGames();
67 | this.redraw();
68 | };
69 | this.gameStream = readStream(res, handler);
70 | await this.gameStream.closePromise;
71 | const empty = this.games.length == 0;
72 | await sleep((empty ? 1 : 5) * 1000);
73 | this.liveUpdate = this.liveUpdate && (empty || !!this.games.find(g => g.result === '*'));
74 | if (this.liveUpdate || forceUpdate) return await this.loadGames();
75 | }
76 | };
77 | static renderClock = (bulk: Bulk) => `${bulk.clock.limit / 60}+${bulk.clock.increment}`;
78 | private sortGames = () =>
79 | this.games.sort((a, b) => {
80 | if (a.result === '*' && b.result !== '*') return -1;
81 | if (a.result !== '*' && b.result === '*') return 1;
82 | if (a.moves !== b.moves) return a.moves < b.moves ? -1 : 1;
83 | return a.id < b.id ? -1 : 1;
84 | });
85 | private gameResult = (status: string, winner: 'white' | 'black' | undefined, moves: number): Result =>
86 | status == 'created' || status == 'started'
87 | ? '*'
88 | : !winner
89 | ? '½-½'
90 | : moves > 1
91 | ? winner == 'white'
92 | ? '1-0'
93 | : '0-1'
94 | : winner == 'white'
95 | ? '+--'
96 | : '--+';
97 |
98 | private canStartClocks = () =>
99 | (!this.bulk?.startClocksAt || this.bulk.startClocksAt > Date.now()) && this.games.find(g => g.moves < 2);
100 | private startClocks = async () => {
101 | if (this.bulk && this.canStartClocks()) {
102 | const res = await this.me.httpClient(
103 | `${this.app.config.lichessHost}/api/bulk-pairing/${this.id}/start-clocks`,
104 | { method: 'POST' },
105 | );
106 | if (res.status === 200) this.bulk.startClocksAt = Date.now();
107 | }
108 | };
109 | private onDestroy = () => {
110 | this.gameStream?.close();
111 | this.liveUpdate = false;
112 | };
113 | redraw = () => this.app.redraw(this.render());
114 | render = () => {
115 | return layout(
116 | this.app,
117 | h('div', [
118 | h('nav.mt-5.breadcrumb', [
119 | h('span.breadcrumb-item', h('a', { attrs: href(bulkPairing.path) }, 'Schedule games')),
120 | h('span.breadcrumb-item.active', `#${this.id}`),
121 | ]),
122 | this.bulk
123 | ? h(`div.card.my-5`, [
124 | h('h1.card-header.text-body-emphasis.py-4', `Bulk pairing #${this.id}`),
125 | h('div.card-body', [
126 | h(
127 | 'table.table.table-borderless',
128 | h('tbody', [
129 | h('tr', [
130 | h('th', 'Setup'),
131 | h('td', [
132 | BulkShow.renderClock(this.bulk),
133 | ' ',
134 | ucfirst(this.bulk.variant),
135 | ' ',
136 | this.bulk.rated ? 'Rated' : 'Casual',
137 | ]),
138 | ]),
139 | h('tr', [
140 | h('th.w-25', 'Created at'),
141 | h('td', timeFormat(new Date(this.bulk.scheduledAt))),
142 | ]),
143 | h('tr', [
144 | h('th', 'Games scheduled at'),
145 | h('td', this.bulk.pairAt ? timeFormat(new Date(this.bulk.pairAt)) : 'Now'),
146 | ]),
147 | h('tr', [
148 | h('th', 'Clocks start at'),
149 | h('td', [
150 | this.bulk.startClocksAt
151 | ? timeFormat(new Date(this.bulk.startClocksAt))
152 | : 'When players make a move',
153 | this.canStartClocks()
154 | ? h(
155 | 'a.btn.btn-sm.btn-outline-warning.ms-3',
156 | {
157 | on: {
158 | click: () => {
159 | if (confirm('Start all clocks?')) this.startClocks();
160 | },
161 | },
162 | },
163 | 'Start all clocks now',
164 | )
165 | : undefined,
166 | ]),
167 | ]),
168 | h('tr', [
169 | h('th', 'Games started'),
170 | h('td.mono', [
171 | this.games.filter(g => g.moves > 1).length,
172 | ' / ' + this.bulk.games.length,
173 | ]),
174 | ]),
175 | h('tr', [
176 | h('th', 'Games completed'),
177 | h('td.mono', [
178 | this.games.filter(g => g.result !== '*').length,
179 | ' / ' + this.bulk.games.length,
180 | ]),
181 | ]),
182 | h('tr', [
183 | h('th', 'Player names'),
184 | h('td', [
185 | h('details', [
186 | h('summary.text-muted.form-label', 'Load player names from another site'),
187 | h('div.card.card-body', [loadPlayersFromUrl(getUrls(this.bulk.id))]),
188 | h(
189 | 'button.btn.btn-secondary.btn-sm.mt-3',
190 | {
191 | attrs: {
192 | type: 'button',
193 | },
194 | on: {
195 | click: () => {
196 | if (!this.bulk) return;
197 |
198 | const pairingsInput = document.getElementById(
199 | 'cr-pairings-url',
200 | ) as HTMLInputElement;
201 | const playersInput = document.getElementById(
202 | 'cr-players-url',
203 | ) as HTMLInputElement;
204 |
205 | saveUrls(this.bulk.id, pairingsInput.value, playersInput.value);
206 | this.loadNamesFromChessResults(pairingsInput, playersInput);
207 |
208 | this.loadGames(true);
209 | },
210 | },
211 | },
212 | 'Load names',
213 | ),
214 | ]),
215 | ]),
216 | ]),
217 | this.bulk.rules
218 | ? h('tr', [
219 | h('th', 'Extra rules'),
220 | h(
221 | 'td',
222 | this.bulk.rules.map(r => h('span.badge.rounded-pill.text-bg-secondary.mx-1', r)),
223 | ),
224 | ])
225 | : undefined,
226 | h('tr', [
227 | h('th', 'Game IDs'),
228 | h('td', [
229 | h('details', [
230 | h(
231 | 'summary.text-muted.form-label',
232 | 'Show individual game IDs for a Lichess Broadcast',
233 | ),
234 | h('div.card.card-body', [
235 | h(
236 | 'textarea.form-control',
237 | { attrs: { rows: 2, spellcheck: 'false', onfocus: 'this.select()' } },
238 | this.games.map(g => g.id).join(' '),
239 | ),
240 | h(
241 | 'small.form-text.text-muted',
242 | 'Copy and paste these when setting up a Lichess Broadcast Round',
243 | ),
244 | ]),
245 | ]),
246 | ]),
247 | ]),
248 | ]),
249 | ),
250 | ]),
251 | ])
252 | : h('div.m-5', h('div.spinner-border.d-block.mx-auto', { attrs: { role: 'status' } })),
253 | ,
254 | this.bulk ? h('div', [this.renderDefaultView(), this.renderChessResultsView()]) : undefined,
255 | ]),
256 | );
257 | };
258 |
259 | renderDefaultView = () => {
260 | const playerLink = (player: Player) =>
261 | this.lichessLink(
262 | '@/' + player.user.name,
263 | `${player.user.title ? player.user.title + ' ' : ''}${player.user.name}`,
264 | );
265 | return h(
266 | 'table.table.table-striped.table-hover',
267 | {
268 | hook: { destroy: () => this.onDestroy() },
269 | },
270 | [
271 | h('thead', [
272 | h('tr', [
273 | h('th', this.bulk?.games.length + ' games'),
274 | h('th', 'White'),
275 | h('th'),
276 | h('th', 'Black'),
277 | h('th'),
278 | h('th.text-center', 'Result'),
279 | h('th.text-end', 'Moves'),
280 | ]),
281 | ]),
282 | h(
283 | 'tbody',
284 | this.games.map(g =>
285 | h('tr', { key: g.id }, [
286 | h('td.mono', this.lichessLink(g.id, `#${g.id}`)),
287 | h('td', playerLink(g.players.white)),
288 | h('td', g.fullNames.white),
289 | h('td', playerLink(g.players.black)),
290 | h('td', g.fullNames.black),
291 | h('td.mono.text-center', g.result),
292 | h('td.mono.text-end', g.moves),
293 | ]),
294 | ),
295 | ),
296 | ],
297 | );
298 | };
299 |
300 | renderChessResultsView = () => {
301 | if (this.crPairings.length === 0) {
302 | return;
303 | }
304 |
305 | const results: {
306 | gameId?: string;
307 | board: string;
308 | name1: string;
309 | name2: string;
310 | team1?: string;
311 | team2?: string;
312 | result?: string;
313 | reversed: boolean;
314 | }[] = this.crPairings.map(pairing => {
315 | const game = this.games.find(
316 | game =>
317 | game.players.white.user.id === pairing.white.lichess?.toLowerCase() &&
318 | game.players.black.user.id === pairing.black.lichess?.toLowerCase(),
319 | );
320 |
321 | if (!pairing.reversed) {
322 | return {
323 | gameId: game?.id,
324 | board: pairing.board,
325 | name1: pairing.white.name,
326 | name2: pairing.black.name,
327 | team1: pairing.white.team,
328 | team2: pairing.black.team,
329 | result: game?.result,
330 | reversed: pairing.reversed,
331 | };
332 | } else {
333 | return {
334 | gameId: game?.id,
335 | board: pairing.board,
336 | name1: pairing.black.name,
337 | name2: pairing.white.name,
338 | team1: pairing.black.team,
339 | team2: pairing.white.team,
340 | result: game?.result.split('').reverse().join(''),
341 | reversed: pairing.reversed,
342 | };
343 | }
344 | });
345 |
346 | return h('div.mt-5', [
347 | h('h4', 'Chess Results View'),
348 | h('table.table.table-striped.table-hover', [
349 | h(
350 | 'tbody',
351 | results.map(result =>
352 | h('tr', { key: result.name1 }, [
353 | h('td.mono', result.gameId ? this.lichessLink(result.gameId) : null),
354 | h('td.mono', result.board),
355 | h('td', result.team1),
356 | h('td', result.reversed ? '' : 'w'),
357 | h('td', result.name1),
358 | h('td.mono.text-center.table-secondary', result.result),
359 | h('td', result.reversed ? 'w' : ''),
360 | h('td', result.name2),
361 | h('td', result.team2),
362 | ]),
363 | ),
364 | ),
365 | ]),
366 | ]);
367 | };
368 |
369 | private lichessLink = (path: string, text?: string) => {
370 | const href = `${this.app.config.lichessHost}/${path}`;
371 | return h('a', { attrs: { target: '_blank', href } }, text || href);
372 | };
373 |
374 | private loadNamesFromChessResults = async (
375 | pairingsInput: HTMLInputElement,
376 | playersInput: HTMLInputElement,
377 | ) => {
378 | try {
379 | const pairingsUrl = pairingsInput.value;
380 | const playersUrl = playersInput.value;
381 |
382 | const players = playersUrl ? await getPlayers(playersUrl) : undefined;
383 | this.crPairings = await getPairings(pairingsUrl, players);
384 |
385 | this.crPairings.forEach(p => {
386 | p.white.lichess && this.fullNames.set(p.white.lichess.toLowerCase(), p.white.name);
387 | p.black.lichess && this.fullNames.set(p.black.lichess.toLowerCase(), p.black.name);
388 | });
389 | } catch (err) {
390 | alert(err);
391 | }
392 | };
393 | }
394 |
--------------------------------------------------------------------------------
/src/scraper/tests/fixtures/individual-round-robin-pairings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | | Round 1 on 2023/06/24 at 14:00 |
7 |
8 |
9 | | Bo. |
10 | No. |
11 | Rtg |
12 | |
13 | White |
14 | Result |
15 | |
16 | Black |
17 | Rtg |
18 | No. |
19 |
20 |
21 | | 1 |
22 | 8 |
23 | 2586 |
24 | GM |
25 | Ponkratov, Pavel |
26 | 0 - 1 |
27 | FM |
28 | Galaktionov, Artem |
29 | 2379 |
30 | 4 |
31 | |
32 |
33 |
34 | | 2 |
35 | 7 |
36 | 2472 |
37 | IM |
38 | Drozdowski, Kacper |
39 | 0 - 1 |
40 | GM |
41 | Andreikin, Dmitry |
42 | 2628 |
43 | 1 |
44 | |
45 |
46 |
47 | | 3 |
48 | 2 |
49 | 2186 |
50 | FM |
51 | Aradhya, Garg |
52 | 0 - 1 |
53 | FM |
54 | Sevgi, Volkan |
55 | 2204 |
56 | 6 |
57 | |
58 |
59 |
60 | | 4 |
61 | 3 |
62 | 2500 |
63 | GM |
64 | Moroni, Luca Jr |
65 | 1 - 0 |
66 | |
67 | Sviridov, Valery |
68 | 2407 |
69 | 5 |
70 | |
71 |
72 |
73 | | Round 2 on 2023/06/24 at 14:30 |
74 |
75 |
76 | | Bo. |
77 | No. |
78 | Rtg |
79 | |
80 | White |
81 | Result |
82 | |
83 | Black |
84 | Rtg |
85 | No. |
86 |
87 |
88 | | 1 |
89 | 4 |
90 | 2379 |
91 | FM |
92 | Galaktionov, Artem |
93 | ½ - ½ |
94 | |
95 | Sviridov, Valery |
96 | 2407 |
97 | 5 |
98 | |
99 |
100 |
101 | | 2 |
102 | 6 |
103 | 2204 |
104 | FM |
105 | Sevgi, Volkan |
106 | 1 - 0 |
107 | GM |
108 | Moroni, Luca Jr |
109 | 2500 |
110 | 3 |
111 | |
112 |
113 |
114 | | 3 |
115 | 1 |
116 | 2628 |
117 | GM |
118 | Andreikin, Dmitry |
119 | 1 - 0 |
120 | FM |
121 | Aradhya, Garg |
122 | 2186 |
123 | 2 |
124 | |
125 |
126 |
127 | | 4 |
128 | 8 |
129 | 2586 |
130 | GM |
131 | Ponkratov, Pavel |
132 | ½ - ½ |
133 | IM |
134 | Drozdowski, Kacper |
135 | 2472 |
136 | 7 |
137 | |
138 |
139 |
140 | | Round 3 on 2023/06/24 at 15:00 |
141 |
142 |
143 | | Bo. |
144 | No. |
145 | Rtg |
146 | |
147 | White |
148 | Result |
149 | |
150 | Black |
151 | Rtg |
152 | No. |
153 |
154 |
155 | | 1 |
156 | 7 |
157 | 2472 |
158 | IM |
159 | Drozdowski, Kacper |
160 | 0 - 1 |
161 | FM |
162 | Galaktionov, Artem |
163 | 2379 |
164 | 4 |
165 | |
166 |
167 |
168 | | 2 |
169 | 2 |
170 | 2186 |
171 | FM |
172 | Aradhya, Garg |
173 | 1 - 0 |
174 | GM |
175 | Ponkratov, Pavel |
176 | 2586 |
177 | 8 |
178 | |
179 |
180 |
181 | | 3 |
182 | 3 |
183 | 2500 |
184 | GM |
185 | Moroni, Luca Jr |
186 | ½ - ½ |
187 | GM |
188 | Andreikin, Dmitry |
189 | 2628 |
190 | 1 |
191 | |
192 |
193 |
194 | | 4 |
195 | 5 |
196 | 2407 |
197 | |
198 | Sviridov, Valery |
199 | 1 - 0 |
200 | FM |
201 | Sevgi, Volkan |
202 | 2204 |
203 | 6 |
204 | |
205 |
206 |
207 | | Round 4 on 2023/06/24 at 15:30 |
208 |
209 |
210 | | Bo. |
211 | No. |
212 | Rtg |
213 | |
214 | White |
215 | Result |
216 | |
217 | Black |
218 | Rtg |
219 | No. |
220 |
221 |
222 | | 1 |
223 | 4 |
224 | 2379 |
225 | FM |
226 | Galaktionov, Artem |
227 | ½ - ½ |
228 | FM |
229 | Sevgi, Volkan |
230 | 2204 |
231 | 6 |
232 | |
233 |
234 |
235 | | 2 |
236 | 1 |
237 | 2628 |
238 | GM |
239 | Andreikin, Dmitry |
240 | ½ - ½ |
241 | |
242 | Sviridov, Valery |
243 | 2407 |
244 | 5 |
245 | |
246 |
247 |
248 | | 3 |
249 | 8 |
250 | 2586 |
251 | GM |
252 | Ponkratov, Pavel |
253 | 1 - 0 |
254 | GM |
255 | Moroni, Luca Jr |
256 | 2500 |
257 | 3 |
258 | |
259 |
260 |
261 | | 4 |
262 | 7 |
263 | 2472 |
264 | IM |
265 | Drozdowski, Kacper |
266 | 1 - 0 |
267 | FM |
268 | Aradhya, Garg |
269 | 2186 |
270 | 2 |
271 | |
272 |
273 |
274 | | Round 5 on 2023/06/24 at 16:00 |
275 |
276 |
277 | | Bo. |
278 | No. |
279 | Rtg |
280 | |
281 | White |
282 | Result |
283 | |
284 | Black |
285 | Rtg |
286 | No. |
287 |
288 |
289 | | 1 |
290 | 2 |
291 | 2186 |
292 | FM |
293 | Aradhya, Garg |
294 | 0 - 1 |
295 | FM |
296 | Galaktionov, Artem |
297 | 2379 |
298 | 4 |
299 | |
300 |
301 |
302 | | 2 |
303 | 3 |
304 | 2500 |
305 | GM |
306 | Moroni, Luca Jr |
307 | ½ - ½ |
308 | IM |
309 | Drozdowski, Kacper |
310 | 2472 |
311 | 7 |
312 | |
313 |
314 |
315 | | 3 |
316 | 5 |
317 | 2407 |
318 | |
319 | Sviridov, Valery |
320 | 1 - 0 |
321 | GM |
322 | Ponkratov, Pavel |
323 | 2586 |
324 | 8 |
325 | |
326 |
327 |
328 | | 4 |
329 | 6 |
330 | 2204 |
331 | FM |
332 | Sevgi, Volkan |
333 | 1 - 0 |
334 | GM |
335 | Andreikin, Dmitry |
336 | 2628 |
337 | 1 |
338 | |
339 |
340 |
341 | | Round 6 on 2023/06/24 at 16:30 |
342 |
343 |
344 | | Bo. |
345 | No. |
346 | Rtg |
347 | |
348 | White |
349 | Result |
350 | |
351 | Black |
352 | Rtg |
353 | No. |
354 |
355 |
356 | | 1 |
357 | 4 |
358 | 2379 |
359 | FM |
360 | Galaktionov, Artem |
361 | 0 - 1 |
362 | GM |
363 | Andreikin, Dmitry |
364 | 2628 |
365 | 1 |
366 | |
367 |
368 |
369 | | 2 |
370 | 8 |
371 | 2586 |
372 | GM |
373 | Ponkratov, Pavel |
374 | 1 - 0 |
375 | FM |
376 | Sevgi, Volkan |
377 | 2204 |
378 | 6 |
379 | |
380 |
381 |
382 | | 3 |
383 | 7 |
384 | 2472 |
385 | IM |
386 | Drozdowski, Kacper |
387 | ½ - ½ |
388 | |
389 | Sviridov, Valery |
390 | 2407 |
391 | 5 |
392 | |
393 |
394 |
395 | | 4 |
396 | 2 |
397 | 2186 |
398 | FM |
399 | Aradhya, Garg |
400 | ½ - ½ |
401 | GM |
402 | Moroni, Luca Jr |
403 | 2500 |
404 | 3 |
405 | |
406 |
407 |
408 | | Round 7 on 2023/06/24 at 17:00 |
409 |
410 |
411 | | Bo. |
412 | No. |
413 | Rtg |
414 | |
415 | White |
416 | Result |
417 | |
418 | Black |
419 | Rtg |
420 | No. |
421 |
422 |
423 | | 1 |
424 | 3 |
425 | 2500 |
426 | GM |
427 | Moroni, Luca Jr |
428 | ½ - ½ |
429 | FM |
430 | Galaktionov, Artem |
431 | 2379 |
432 | 4 |
433 | |
434 |
435 |
436 | | 2 |
437 | 5 |
438 | 2407 |
439 | |
440 | Sviridov, Valery |
441 | ½ - ½ |
442 | FM |
443 | Aradhya, Garg |
444 | 2186 |
445 | 2 |
446 | |
447 |
448 |
449 | | 3 |
450 | 6 |
451 | 2204 |
452 | FM |
453 | Sevgi, Volkan |
454 | 0 - 1 |
455 | IM |
456 | Drozdowski, Kacper |
457 | 2472 |
458 | 7 |
459 | |
460 |
461 |
462 | | 4 |
463 | 1 |
464 | 2628 |
465 | GM |
466 | Andreikin, Dmitry |
467 | 1 - 0 |
468 | GM |
469 | Ponkratov, Pavel |
470 | 2586 |
471 | 8 |
472 | |
473 |
474 |
475 |
476 |
--------------------------------------------------------------------------------
/src/scraper/tests/scrape.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, expect, test, vi, type Mock, beforeEach } from 'vitest';
2 | import { readFileSync } from 'fs';
3 | import {
4 | getPlayers,
5 | getPairings,
6 | setResultsPerPage,
7 | type Player,
8 | getUrls,
9 | saveUrls,
10 | setCacheBuster,
11 | type Pairing,
12 | filterRound,
13 | } from '../scraper';
14 |
15 | global.fetch = vi.fn(proxyUrl => {
16 | let url = new URL(decodeURIComponent(proxyUrl.split('?')[1]));
17 | let path = url.pathname;
18 |
19 | return Promise.resolve({
20 | text: () => Promise.resolve(readFileSync(`src/scraper/tests/fixtures${path}`)),
21 | });
22 | }) as Mock;
23 |
24 | describe('fetch players', () => {
25 | test('with lichess usernames', async () => {
26 | const players = await getPlayers('https://example.com/players-list-with-usernames.html');
27 |
28 | expect(players[1]).toEqual({
29 | name: 'Navara, David',
30 | fideId: '309095',
31 | rating: 2679,
32 | lichess: 'RealDavidNavara',
33 | });
34 | expect(players).toHaveLength(71);
35 | });
36 |
37 | test('with team columns', async () => {
38 | const players = await getPlayers('https://example.com/players-list-without-usernames.html');
39 |
40 | expect(players[0]).toEqual({
41 | name: 'Nepomniachtchi Ian',
42 | fideId: '4168119',
43 | rating: 2789,
44 | lichess: undefined,
45 | });
46 | expect(players).toHaveLength(150);
47 | });
48 | });
49 |
50 | describe('fetch pairings', () => {
51 | test('team swiss', async () => {
52 | const pairings = await getPairings('https://example.com/team-swiss-pairings-with-usernames.html');
53 |
54 | expect(pairings).toStrictEqual([
55 | {
56 | black: {
57 | lichess: 'test4',
58 | name: 'Hris, Panagiotis',
59 | team: 'Team C',
60 | rating: 2227,
61 | },
62 | white: {
63 | lichess: 'test134',
64 | name: 'Testing, Test',
65 | team: 'Team B',
66 | rating: 1985,
67 | },
68 | reversed: false,
69 | board: '1.1',
70 | },
71 | {
72 | black: {
73 | lichess: 'test3',
74 | name: 'Someone, Else',
75 | team: 'Team B',
76 | rating: 2400,
77 | },
78 | white: {
79 | lichess: 'test5',
80 | name: 'Trevlar, Someone',
81 | team: 'Team C',
82 | rating: 0,
83 | },
84 | reversed: true,
85 | board: '1.2',
86 | },
87 | {
88 | black: {
89 | lichess: 'test6',
90 | name: 'TestPlayer, Mary',
91 | team: 'Team C',
92 | rating: 1600,
93 | },
94 | white: {
95 | lichess: 'test1',
96 | name: 'Another, Test',
97 | team: 'Team B',
98 | rating: 1900,
99 | },
100 | reversed: false,
101 | board: '1.3',
102 | },
103 | {
104 | black: {
105 | lichess: 'test2',
106 | name: 'Ignore, This',
107 | team: 'Team B',
108 | rating: 1400,
109 | },
110 | white: {
111 | lichess: 'test7',
112 | name: 'Testing, Tester',
113 | team: 'Team C',
114 | rating: 0,
115 | },
116 | reversed: true,
117 | board: '1.4',
118 | },
119 | {
120 | black: {
121 | lichess: 'TestAccount1',
122 | name: 'SomeoneElse, Michael',
123 | team: 'Team D',
124 | rating: 2230,
125 | },
126 | white: {
127 | lichess: 'Cynosure',
128 | name: 'Wait, Theophilus',
129 | team: 'Team A',
130 | rating: 0,
131 | },
132 | reversed: false,
133 | board: '2.1',
134 | },
135 | {
136 | black: {
137 | lichess: 'Thibault',
138 | name: 'Thibault, D',
139 | team: 'Team A',
140 | rating: 0,
141 | },
142 | white: {
143 | lichess: 'TestAccount2',
144 | name: 'YetSomeoneElse, Lilly',
145 | team: 'Team D',
146 | rating: 2070,
147 | },
148 | reversed: true,
149 | board: '2.2',
150 | },
151 | {
152 | black: {
153 | lichess: 'TestAccount3',
154 | name: 'Unknown, Player',
155 | team: 'Team D',
156 | rating: 1300,
157 | },
158 | white: {
159 | lichess: 'Puzzlingpuzzler',
160 | name: 'Gkizi, Konst',
161 | team: 'Team A',
162 | rating: 1270,
163 | },
164 | reversed: false,
165 | board: '2.3',
166 | },
167 | {
168 | black: {
169 | lichess: 'ThisAccountDoesntExist',
170 | name: 'Placeholder, Player',
171 | team: 'Team A',
172 | rating: 0,
173 | },
174 | white: {
175 | lichess: 'TestAccount4',
176 | name: 'Also, Unknown',
177 | team: 'Team D',
178 | rating: 1111,
179 | },
180 | reversed: true,
181 | board: '2.4',
182 | },
183 | ]);
184 | expect(pairings).toHaveLength(8);
185 | });
186 |
187 | test('team another swiss', async () => {
188 | const pairings = await getPairings('https://example.com/team-swiss-pairings-with-usernames-2.html');
189 |
190 | expect(pairings[0]).toStrictEqual({
191 | black: {
192 | lichess: 'PeterAcs',
193 | name: 'Acs Peter',
194 | team: 'Morgan Stanley',
195 | rating: 2582,
196 | },
197 | white: {
198 | lichess: 'joe1714',
199 | name: 'Karan J P',
200 | team: 'Accenture',
201 | rating: 1852,
202 | },
203 | reversed: false,
204 | board: '1.1',
205 | });
206 | expect(pairings[pairings.length - 1]).toStrictEqual({
207 | black: {
208 | lichess: 'Dimash8888',
209 | name: 'Jexekov Dimash',
210 | team: 'Freedom Holding',
211 | rating: 0,
212 | },
213 | white: {
214 | lichess: 'hhauks',
215 | name: 'Hauksdottir Hrund',
216 | team: 'Islandsbanki',
217 | rating: 1814,
218 | },
219 | reversed: true,
220 | board: '7.4',
221 | });
222 | expect(pairings).toHaveLength(28);
223 | });
224 |
225 | test('team swiss w/o lichess usernames on the same page', async () => {
226 | const pairings = await getPairings('https://example.com/team-swiss-pairings-without-usernames.html');
227 |
228 | expect(pairings[0]).toEqual({
229 | white: {
230 | name: 'Berend Elvira',
231 | team: 'European Investment Bank',
232 | rating: 2326,
233 | lichess: undefined,
234 | },
235 | black: {
236 | name: 'Nepomniachtchi Ian',
237 | team: 'SBER',
238 | rating: 2789,
239 | lichess: undefined,
240 | },
241 | reversed: false,
242 | board: '1.1',
243 | });
244 | expect(pairings[1]).toEqual({
245 | black: {
246 | name: 'Sebe-Vodislav Razvan-Alexandru',
247 | team: 'European Investment Bank',
248 | rating: 2270,
249 | lichess: undefined,
250 | },
251 | white: {
252 | name: 'Kadatsky Alexander',
253 | team: 'SBER',
254 | rating: 2368,
255 | lichess: undefined,
256 | },
257 | reversed: true,
258 | board: '1.2',
259 | });
260 |
261 | // check the next set of Teams
262 | expect(pairings[8]).toEqual({
263 | black: {
264 | name: 'Delchev Alexander',
265 | team: 'Tigar Tyres',
266 | rating: 2526,
267 | lichess: undefined,
268 | },
269 | white: {
270 | name: 'Chernikova Iryna',
271 | team: 'Airbus (FRA)',
272 | rating: 1509,
273 | lichess: undefined,
274 | },
275 | reversed: false,
276 | board: '3.1',
277 | });
278 | expect(pairings).toHaveLength(76);
279 | });
280 |
281 | test('individual round robin', async () => {
282 | const pairings = await getPairings('https://example.com/individual-round-robin-pairings.html');
283 |
284 | expect(pairings[0]).toEqual({
285 | white: {
286 | name: 'Ponkratov, Pavel',
287 | },
288 | black: {
289 | name: 'Galaktionov, Artem',
290 | },
291 | reversed: false,
292 | board: '1',
293 | });
294 | expect(pairings).toHaveLength(28);
295 | });
296 |
297 | test('team round robin', async () => {
298 | const pairings = await getPairings('https://example.com/team-round-robin-pairings.html');
299 |
300 | expect(pairings[0]).toEqual({
301 | white: {
302 | name: 'Kyrkjebo, Hanna B.',
303 | team: 'KPMG Norway',
304 | lichess: 'watchmecheck',
305 | rating: 1899,
306 | },
307 | black: {
308 | name: 'Liu, Zhaoqi',
309 | team: 'Nanjing Spark Chess Technology Co.Ltd.',
310 | lichess: 'lzqupup',
311 | rating: 2337,
312 | },
313 | reversed: false,
314 | board: '1.1',
315 | });
316 | expect(pairings[1]).toEqual({
317 | white: {
318 | name: 'Du, Chunhui',
319 | team: 'Nanjing Spark Chess Technology Co.Ltd.',
320 | lichess: 'duchunhui',
321 | rating: 2288,
322 | },
323 | black: {
324 | name: 'Grimsrud, Oyvind',
325 | team: 'KPMG Norway',
326 | lichess: 'Bruneratseth',
327 | rating: 1836,
328 | },
329 | reversed: true,
330 | board: '1.2',
331 | });
332 | expect(pairings).toHaveLength(8);
333 | });
334 |
335 | test('individual swiss', async () => {
336 | const pairings = await getPairings('https://example.com/individual-swiss-pairings.html');
337 |
338 | expect(pairings[0]).toEqual({
339 | white: {
340 | name: 'Gunina, Valentina',
341 | },
342 | black: {
343 | name: 'Mammadzada, Gunay',
344 | },
345 | reversed: false,
346 | board: '1',
347 | });
348 | expect(pairings).toHaveLength(59);
349 | });
350 |
351 | test('individual swiss w/ player substitution', async () => {
352 | const players: Player[] = [
353 | {
354 | name: 'Gunina, Valentina',
355 | lichess: 'test-valentina',
356 | },
357 | {
358 | name: 'Mammadzada, Gunay',
359 | lichess: 'test-gunay',
360 | },
361 | ];
362 | const pairings = await getPairings('https://example.com/individual-swiss-pairings.html', players);
363 |
364 | expect(pairings[0]).toEqual({
365 | white: {
366 | name: 'Gunina, Valentina',
367 | lichess: 'test-valentina',
368 | },
369 | black: {
370 | name: 'Mammadzada, Gunay',
371 | lichess: 'test-gunay',
372 | },
373 | reversed: false,
374 | board: '1',
375 | });
376 | expect(pairings).toHaveLength(59);
377 | });
378 |
379 | test('team ko pairings w/o usernames', async () => {
380 | const pairings = await getPairings('https://example.com/team-ko-pairings-without-usernames.html');
381 |
382 | expect(pairings[0]).toEqual({
383 | white: {
384 | name: 'Andriasian, Zaven',
385 | rating: 2624,
386 | team: 'Chessify',
387 | },
388 | black: {
389 | name: 'Schneider, Igor',
390 | rating: 2206,
391 | team: 'Deutsche Bank',
392 | },
393 | reversed: false,
394 | board: '1.1',
395 | });
396 |
397 | expect(pairings[pairings.length - 1]).toEqual({
398 | white: {
399 | name: 'van Eerde, Matthew',
400 | rating: 0,
401 | team: 'Microsoft E',
402 | },
403 | black: {
404 | name: 'Asbjornsen, Oyvind Von Doren',
405 | rating: 1594,
406 | team: 'Von Doren Watch Company AS',
407 | },
408 | reversed: true,
409 | board: '3.4',
410 | });
411 | expect(pairings).toHaveLength(24);
412 | });
413 |
414 | test('team ko pairings w/ usernames', async () => {
415 | const pairings = await getPairings('https://example.com/team-ko-pairings-with-usernames.html');
416 |
417 | expect(pairings[0]).toEqual({
418 | white: {
419 | name: 'T2, P1',
420 | lichess: 't2pl1',
421 | rating: 1678,
422 | team: 'Team 2',
423 | },
424 | black: {
425 | name: 'T4, P1',
426 | lichess: 't4pl1',
427 | rating: 0,
428 | team: 'Team 4',
429 | },
430 | reversed: false,
431 | board: '1.1',
432 | });
433 |
434 | expect(pairings[pairings.length - 1]).toEqual({
435 | white: {
436 | name: 'T3, P4',
437 | lichess: 't3pl4',
438 | rating: 0,
439 | team: 'Team 3',
440 | },
441 | black: {
442 | name: 'T1, P4',
443 | lichess: 't1pl4',
444 | rating: 2453,
445 | team: 'Team 1',
446 | },
447 | reversed: true,
448 | board: '2.4',
449 | });
450 | expect(pairings).toHaveLength(16);
451 | });
452 | });
453 |
454 | describe('round filtering', () => {
455 | const pairings: Pairing[] = [
456 | // round 1
457 | { board: '1.1', white: { name: 'w1' }, black: { name: 'b1' }, reversed: false },
458 | { board: '1.2', white: { name: 'w2' }, black: { name: 'b2' }, reversed: false },
459 | { board: '2.1', white: { name: 'w3' }, black: { name: 'b3' }, reversed: false },
460 | { board: '2.2', white: { name: 'w4' }, black: { name: 'b4' }, reversed: false },
461 | // round 2
462 | { board: '1.1', white: { name: 'w5' }, black: { name: 'b5' }, reversed: false },
463 | { board: '1.2', white: { name: 'w6' }, black: { name: 'b6' }, reversed: false },
464 | { board: '2.1', white: { name: 'w7' }, black: { name: 'b7' }, reversed: false },
465 | { board: '2.2', white: { name: 'w8' }, black: { name: 'b8' }, reversed: false },
466 | ];
467 |
468 | test('round 1', () => {
469 | expect(filterRound(pairings, 1)).toEqual([
470 | { board: '1.1', white: { name: 'w1' }, black: { name: 'b1' }, reversed: false },
471 | { board: '1.2', white: { name: 'w2' }, black: { name: 'b2' }, reversed: false },
472 | { board: '2.1', white: { name: 'w3' }, black: { name: 'b3' }, reversed: false },
473 | { board: '2.2', white: { name: 'w4' }, black: { name: 'b4' }, reversed: false },
474 | ]);
475 | });
476 |
477 | test('round 2', () => {
478 | expect(filterRound(pairings, 2)).toEqual([
479 | { board: '1.1', white: { name: 'w5' }, black: { name: 'b5' }, reversed: false },
480 | { board: '1.2', white: { name: 'w6' }, black: { name: 'b6' }, reversed: false },
481 | { board: '2.1', white: { name: 'w7' }, black: { name: 'b7' }, reversed: false },
482 | { board: '2.2', white: { name: 'w8' }, black: { name: 'b8' }, reversed: false },
483 | ]);
484 | });
485 | });
486 |
487 | test('set results per page', () => {
488 | expect(setResultsPerPage('https://example.com')).toBe('https://example.com/?zeilen=99999');
489 | expect(setResultsPerPage('https://example.com', 10)).toBe('https://example.com/?zeilen=10');
490 | expect(setResultsPerPage('https://example.com/?foo=bar', 10)).toBe(
491 | 'https://example.com/?foo=bar&zeilen=10',
492 | );
493 | expect(setResultsPerPage('https://example.com/players.aspx?zeilen=10', 20)).toBe(
494 | 'https://example.com/players.aspx?zeilen=20',
495 | );
496 | expect(setResultsPerPage('https://example.com/players.aspx?zeilen=10', 99999)).toBe(
497 | 'https://example.com/players.aspx?zeilen=99999',
498 | );
499 | });
500 |
501 | describe('get/set urls from local storage', () => {
502 | beforeEach(() => {
503 | localStorage.clear();
504 | });
505 |
506 | test('get', () => {
507 | expect(getUrls('abc1')).toBeUndefined();
508 | });
509 |
510 | test('set', () => {
511 | saveUrls('abc2', 'https://example.com/pairings2.html');
512 | expect(getUrls('abc2')).toStrictEqual({
513 | pairingsUrl: 'https://example.com/pairings2.html',
514 | });
515 | });
516 |
517 | test('append', () => {
518 | saveUrls('abc3', 'https://example.com/pairings3.html');
519 | saveUrls('abc4', 'https://example.com/pairings4.html');
520 |
521 | expect(getUrls('abc3')).toStrictEqual({
522 | pairingsUrl: 'https://example.com/pairings3.html',
523 | });
524 |
525 | expect(getUrls('abc4')).toStrictEqual({
526 | pairingsUrl: 'https://example.com/pairings4.html',
527 | });
528 | });
529 | });
530 |
531 | describe('test cache buster', () => {
532 | test('set cache buster', () => {
533 | expect(setCacheBuster('https://example.com')).toContain('https://example.com/?cachebust=1');
534 | });
535 |
536 | test('append cache buster', () => {
537 | expect(setCacheBuster('https://example.com/?foo=bar')).toContain(
538 | 'https://example.com/?foo=bar&cachebust=1',
539 | );
540 | });
541 | });
542 |
--------------------------------------------------------------------------------
/src/scraper/tests/fixtures/team-ko-pairings-with-usernames.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | | Round 1 |
7 |
8 |
9 | | Bo. |
10 | 1 |
11 | Team 2 |
12 | Rtg |
13 | Club/City |
14 | - |
15 | 4 |
16 | Team 4 |
17 | Rtg |
18 | Club/City |
19 | 3½: ½ |
20 |
21 |
22 | | 1.1 |
23 | |
24 |
25 |
26 |
27 |
28 | |
29 |
30 | T2, P1
33 | |
34 |
35 |
36 |
37 | |
38 | 1678 |
39 | t2pl1 |
40 | - |
41 | |
42 |
43 |
44 |
45 |
46 | |
47 |
48 | T4, P1
51 | |
52 |
53 |
54 |
55 | |
56 | 0 |
57 | t4pl1 |
58 | 1 - 0 |
59 |
60 |
61 | | 1.2 |
62 | CM |
63 |
64 |
65 |
66 |
67 | |
68 |
69 | T2, P2
72 | |
73 |
74 |
75 |
76 | |
77 | 2220 |
78 | t2pl2 |
79 | - |
80 | |
81 |
82 |
83 |
84 |
85 | |
86 |
87 | T4, P2
90 | |
91 |
92 |
93 |
94 | |
95 | 1500 |
96 | t4pl2 |
97 | ½ - ½ |
98 |
99 |
100 | | 1.3 |
101 | FM |
102 |
103 |
104 |
105 |
106 | |
107 |
108 | T2, P3
111 | |
112 |
113 |
114 |
115 | |
116 | 2234 |
117 | twpl3 |
118 | - |
119 | |
120 |
121 |
122 |
123 |
124 | |
125 |
126 | T4, P3
129 | |
130 |
131 |
132 |
133 | |
134 | 0 |
135 | t4pl3 |
136 | 1 - 0 |
137 |
138 |
139 | | 1.4 |
140 | WGM |
141 |
142 |
143 |
144 |
145 | |
146 |
147 | T2, P4
150 | |
151 |
152 |
153 |
154 | |
155 | 2350 |
156 | t2pl4 |
157 | - |
158 | |
159 |
160 |
161 |
162 |
163 | |
164 |
165 | T4, P4
168 | |
169 |
170 |
171 |
172 | |
173 | 1890 |
174 | t4pl4 |
175 | 1 - 0 |
176 |
177 |
178 | | Bo. |
179 | 3 |
180 | Team 3 |
181 | Rtg |
182 | Club/City |
183 | - |
184 | 2 |
185 | Team 1 |
186 | Rtg |
187 | Club/City |
188 | 3½: ½ |
189 |
190 |
191 | | 2.1 |
192 | IM |
193 |
194 |
195 |
196 |
197 | |
198 |
199 | T3, P1
202 | |
203 |
204 |
205 |
206 | |
207 | 2311 |
208 | t3pl1 |
209 | - |
210 | WIM |
211 |
212 |
213 |
214 |
215 | |
216 |
217 | T1, P1
220 | |
221 |
222 |
223 |
224 | |
225 | 2100 |
226 | t1pl1 |
227 | 1 - 0 |
228 |
229 |
230 | | 2.2 |
231 | |
232 |
233 |
234 |
235 |
236 | |
237 |
238 | T3, P2
241 | |
242 |
243 |
244 |
245 | |
246 | 1890 |
247 | t3pl2 |
248 | - |
249 | |
250 |
251 |
252 |
253 |
254 | |
255 |
256 | T1, P2
259 | |
260 |
261 |
262 |
263 | |
264 | 2200 |
265 | t1pl2 |
266 | ½ - ½ |
267 |
268 |
269 | | 2.3 |
270 | |
271 |
272 |
273 |
274 |
275 | |
276 |
277 | T3, P3
280 | |
281 |
282 |
283 |
284 | |
285 | 2110 |
286 | t3pl3 |
287 | - |
288 | |
289 |
290 |
291 |
292 |
293 | |
294 |
295 | T1, P3
298 | |
299 |
300 |
301 |
302 | |
303 | 1459 |
304 | t1pl3 |
305 | 1 - 0 |
306 |
307 |
308 | | 2.4 |
309 | |
310 |
311 |
312 |
313 |
314 | |
315 |
316 | T3, P4
319 | |
320 |
321 |
322 |
323 | |
324 | 0 |
325 | t3pl4 |
326 | - |
327 | GM |
328 |
329 |
330 |
331 |
332 | |
333 |
334 | T1, P4
337 | |
338 |
339 |
340 |
341 | |
342 | 2453 |
343 | t1pl4 |
344 | 1 - 0 |
345 |
346 |
347 | | Round 2 |
348 |
349 |
350 | | Bo. |
351 | 4 |
352 | Team 4 |
353 | Rtg |
354 | Club/City |
355 | - |
356 | 1 |
357 | Team 2 |
358 | Rtg |
359 | Club/City |
360 | 2 : 2 |
361 |
362 |
363 | | 1.1 |
364 | |
365 |
366 |
367 |
368 |
369 | |
370 |
371 | T4, P1
374 | |
375 |
376 |
377 |
378 | |
379 | 0 |
380 | t4pl1 |
381 | - |
382 | |
383 |
384 |
385 |
386 |
387 | |
388 |
389 | T2, P1
392 | |
393 |
394 |
395 |
396 | |
397 | 1678 |
398 | t2pl1 |
399 | 1 - 0 |
400 |
401 |
402 | | 1.2 |
403 | |
404 |
405 |
406 |
407 |
408 | |
409 |
410 | T4, P2
413 | |
414 |
415 |
416 |
417 | |
418 | 1500 |
419 | t4pl2 |
420 | - |
421 | CM |
422 |
423 |
424 |
425 |
426 | |
427 |
428 | T2, P2
431 | |
432 |
433 |
434 |
435 | |
436 | 2220 |
437 | t2pl2 |
438 | 0 - 1 |
439 |
440 |
441 | | 1.3 |
442 | |
443 |
444 |
445 |
446 |
447 | |
448 |
449 | T4, P3
452 | |
453 |
454 |
455 |
456 | |
457 | 0 |
458 | t4pl3 |
459 | - |
460 | FM |
461 |
462 |
463 |
464 |
465 | |
466 |
467 | T2, P3
470 | |
471 |
472 |
473 |
474 | |
475 | 2234 |
476 | twpl3 |
477 | 1 - 0 |
478 |
479 |
480 | | 1.4 |
481 | |
482 |
483 |
484 |
485 |
486 | |
487 |
488 | T4, P4
491 | |
492 |
493 |
494 |
495 | |
496 | 1890 |
497 | t4pl4 |
498 | - |
499 | WGM |
500 |
501 |
502 |
503 |
504 | |
505 |
506 | T2, P4
509 | |
510 |
511 |
512 |
513 | |
514 | 2350 |
515 | t2pl4 |
516 | 0 - 1 |
517 |
518 |
519 | | Bo. |
520 | 2 |
521 | Team 1 |
522 | Rtg |
523 | Club/City |
524 | - |
525 | 3 |
526 | Team 3 |
527 | Rtg |
528 | Club/City |
529 | 2 : 2 |
530 |
531 |
532 | | 2.1 |
533 | WIM |
534 |
535 |
536 |
537 |
538 | |
539 |
540 | T1, P1
543 | |
544 |
545 |
546 |
547 | |
548 | 2100 |
549 | t1pl1 |
550 | - |
551 | IM |
552 |
553 |
554 |
555 |
556 | |
557 |
558 | T3, P1
561 | |
562 |
563 |
564 |
565 | |
566 | 2311 |
567 | t3pl1 |
568 | ½ - ½ |
569 |
570 |
571 | | 2.2 |
572 | |
573 |
574 |
575 |
576 |
577 | |
578 |
579 | T1, P2
582 | |
583 |
584 |
585 |
586 | |
587 | 2200 |
588 | t1pl2 |
589 | - |
590 | |
591 |
592 |
593 |
594 |
595 | |
596 |
597 | T3, P2
600 | |
601 |
602 |
603 |
604 | |
605 | 1890 |
606 | t3pl2 |
607 | ½ - ½ |
608 |
609 |
610 | | 2.3 |
611 | |
612 |
613 |
614 |
615 |
616 | |
617 |
618 | T1, P3
621 | |
622 |
623 |
624 |
625 | |
626 | 1459 |
627 | t1pl3 |
628 | - |
629 | |
630 |
631 |
632 |
633 |
634 | |
635 |
636 | T3, P3
639 | |
640 |
641 |
642 |
643 | |
644 | 2110 |
645 | t3pl3 |
646 | ½ - ½ |
647 |
648 |
649 | | 2.4 |
650 | GM |
651 |
652 |
653 |
654 |
655 | |
656 |
657 | T1, P4
660 | |
661 |
662 |
663 |
664 | |
665 | 2453 |
666 | t1pl4 |
667 | - |
668 | |
669 |
670 |
671 |
672 |
673 | |
674 |
675 | T3, P4
678 | |
679 |
680 |
681 |
682 | |
683 | 0 |
684 | t3pl4 |
685 | ½ - ½ |
686 |
687 |
688 |
689 |
--------------------------------------------------------------------------------
/src/scraper/tests/fixtures/players-list-with-usernames.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | | No. |
7 | |
8 | Name |
9 | FideID |
10 | FED |
11 | Rtg |
12 | Club/City |
13 |
14 |
15 | | 1 |
16 | GM |
17 | Saric, Ivan |
18 | 14508150 |
19 | CRO |
20 | 2685 |
21 | dalmatinac101 |
22 |
23 |
24 | | 2 |
25 | GM |
26 | Navara, David |
27 | 309095 |
28 | CZE |
29 | 2679 |
30 | RealDavidNavara |
31 |
32 |
33 | | 3 |
34 | GM |
35 | Pechac, Jergus |
36 | 14926970 |
37 | SVK |
38 | 2570 |
39 | GracchusJeep |
40 |
41 |
42 | | 4 |
43 | GM |
44 | Balogh, Csaba |
45 | 718939 |
46 | HUN |
47 | 2551 |
48 | Csimpula |
49 |
50 |
51 | | 5 |
52 | IM |
53 | Sahidi, Samir |
54 | 14936372 |
55 | SVK |
56 | 2476 |
57 | samisahi |
58 |
59 |
60 | | 6 |
61 | IM |
62 | Haring, Filip |
63 | 14933080 |
64 | SVK |
65 | 2442 |
66 | BestStelker |
67 |
68 |
69 | | 7 |
70 | FM |
71 | Bochnicka, Vladimir |
72 | 14943450 |
73 | SVK |
74 | 2362 |
75 | vladoboch |
76 |
77 |
78 | | 8 |
79 | FM |
80 | Danada, Tomas |
81 | 14919362 |
82 | SVK |
83 | 2229 |
84 | NAARD |
85 |
86 |
87 | | 9 |
88 | |
89 | Matejovic, Juraj |
90 | 14906040 |
91 | SVK |
92 | 2228 |
93 | CSSML-NDSMD |
94 |
95 |
96 | | 10 |
97 | WIM |
98 | Maslikova, Veronika |
99 | 14904888 |
100 | SVK |
101 | 2185 |
102 | Vercinka |
103 |
104 |
105 | | 11 |
106 | |
107 | Tropp, Oliver |
108 | 14913003 |
109 | SVK |
110 | 2159 |
111 | KKtreningKK |
112 |
113 |
114 | | 12 |
115 | CM |
116 | Fizer, Marek |
117 | 391980 |
118 | CZE |
119 | 2126 |
120 | Mafi08 |
121 |
122 |
123 | | 13 |
124 | |
125 | Salgovic, Simon |
126 | 14943581 |
127 | SVK |
128 | 2126 |
129 | s2305 |
130 |
131 |
132 | | 14 |
133 | |
134 | Verbovsky, Michal |
135 | 14934213 |
136 | SVK |
137 | 2124 |
138 | mikhailooooo |
139 |
140 |
141 | | 15 |
142 | WFM |
143 | Sankova, Stella |
144 | 14944200 |
145 | SVK |
146 | 2108 |
147 | pinkunicorn |
148 |
149 |
150 | | 16 |
151 | |
152 | Koval, Jakub |
153 | 14933993 |
154 | SVK |
155 | 2106 |
156 | Kuklac858 |
157 |
158 |
159 | | 17 |
160 | |
161 | Cisko, Matej |
162 | 14930412 |
163 | SVK |
164 | 2105 |
165 | mc12345mc |
166 |
167 |
168 | | 18 |
169 | |
170 | Krupa, Miroslav |
171 | 14944154 |
172 | SVK |
173 | 2103 |
174 | HiAmUndaDaWata |
175 |
176 |
177 | | 19 |
178 | |
179 | Bochnickova, Andrea |
180 | 14901781 |
181 | SVK |
182 | 2064 |
183 | Andrea1977 |
184 |
185 |
186 | | 20 |
187 | |
188 | Pericka, Tomas |
189 | 14909081 |
190 | SVK |
191 | 2008 |
192 | pery85 |
193 |
194 |
195 | | 21 |
196 | |
197 | Hurtuk, Radovan |
198 | 14935031 |
199 | SVK |
200 | 2000 |
201 | RadovanHurtuk |
202 |
203 |
204 | | 22 |
205 | |
206 | Slamena, Michal |
207 | 14908840 |
208 | SVK |
209 | 2000 |
210 | Snowy_At_tacker |
211 |
212 |
213 | | 23 |
214 | |
215 | Kolar, Tomas |
216 | 14909448 |
217 | SVK |
218 | 1995 |
219 | TOSO17 |
220 |
221 |
222 | | 24 |
223 | |
224 | Vrba, Michal |
225 | 14909790 |
226 | SVK |
227 | 1984 |
228 | michalv |
229 |
230 |
231 | | 25 |
232 | |
233 | Nemergut, Patrik |
234 | 14919524 |
235 | SVK |
236 | 1960 |
237 | NemkoP |
238 |
239 |
240 | | 26 |
241 | |
242 | Mizicka, Matus |
243 | 14946238 |
244 | SVK |
245 | 1958 |
246 | nwb14 |
247 |
248 |
249 | | 27 |
250 | |
251 | Diviak, Rastislav |
252 | 14908204 |
253 | SVK |
254 | 1956 |
255 | divoky |
256 |
257 |
258 | | 28 |
259 | |
260 | Paverova, Zuzana |
261 | 14905310 |
262 | SVK |
263 | 1873 |
264 | suzi81 |
265 |
266 |
267 | | 29 |
268 | WCM |
269 | Novomeska, Karin |
270 | 14952300 |
271 | SVK |
272 | 1851 |
273 | Karinka09 |
274 |
275 |
276 | | 30 |
277 | |
278 | Horvath, Marek |
279 | 14913682 |
280 | SVK |
281 | 1801 |
282 | stein111 |
283 |
284 |
285 | | 31 |
286 | |
287 | Fizerova, Lucie |
288 | 385115 |
289 | CZE |
290 | 1794 |
291 | Luckafizerova |
292 |
293 |
294 | | 32 |
295 | |
296 | Matejka, Simon |
297 | 14974541 |
298 | SVK |
299 | 1792 |
300 | SIMONmatejka |
301 |
302 |
303 | | 33 |
304 | |
305 | Brida, Lukas |
306 | 14920239 |
307 | SVK |
308 | 1779 |
309 | LukasB1988 |
310 |
311 |
312 | | 34 |
313 | |
314 | Vozarova, Adriana |
315 | 14940752 |
316 | SVK |
317 | 1772 |
318 | avozarova |
319 |
320 |
321 | | 35 |
322 | |
323 | Pericka, Pavol |
324 | 14914298 |
325 | SVK |
326 | 1743 |
327 | Palo60 |
328 |
329 |
330 | | 36 |
331 | |
332 | Kramar, Matus |
333 | 14944618 |
334 | SVK |
335 | 1741 |
336 | Matko11111 |
337 |
338 |
339 | | 37 |
340 | |
341 | Bucor, Mark |
342 | 14976340 |
343 | SVK |
344 | 1725 |
345 | FitzTheWorldHopper |
346 |
347 |
348 | | 38 |
349 | |
350 | Pisarcik, Richard |
351 | 14967499 |
352 | SVK |
353 | 1708 |
354 | VanGerven |
355 |
356 |
357 | | 39 |
358 | |
359 | Kukan, Michal |
360 | 14951967 |
361 | SVK |
362 | 1693 |
363 | mikodnv |
364 |
365 |
366 | | 40 |
367 | |
368 | Hrdlicka, Martin |
369 | 14940876 |
370 | SVK |
371 | 1687 |
372 | hrdlis |
373 |
374 |
375 | | 41 |
376 | |
377 | Mikula, Martin |
378 | 14941058 |
379 | SVK |
380 | 1657 |
381 | REW19 |
382 |
383 |
384 | | 42 |
385 | |
386 | Jura, Michal |
387 | 73620882 |
388 | SVK |
389 | 1631 |
390 | Osim81 |
391 |
392 |
393 | | 43 |
394 | |
395 | Kapinaj, Matus |
396 | 14980703 |
397 | SVK |
398 | 1606 |
399 | TheJeromeGambit |
400 |
401 |
402 | | 44 |
403 | |
404 | Nemergut, Jan |
405 | 14948079 |
406 | SVK |
407 | 1573 |
408 | Nemergutsl |
409 |
410 |
411 | | 45 |
412 | |
413 | Kison, Jan |
414 | 14949156 |
415 | SVK |
416 | 1571 |
417 | Kison |
418 |
419 |
420 | | 46 |
421 | AFM |
422 | Nosal, Tobias |
423 | 14976293 |
424 | SVK |
425 | 1562 |
426 | Tobnos |
427 |
428 |
429 | | 47 |
430 | |
431 | Paracka, Patrik |
432 | 14975467 |
433 | SVK |
434 | 1521 |
435 | x220616 |
436 |
437 |
438 | | 48 |
439 | |
440 | Hutyra, Tomas |
441 | 23762683 |
442 | CZE |
443 | 1450 |
444 | JezhkovyVochi |
445 |
446 |
447 | | 49 |
448 | |
449 | Karaba, Rudolf |
450 | 14967537 |
451 | SVK |
452 | 1335 |
453 | Rudy22 |
454 |
455 |
456 | | 50 |
457 | |
458 | Gvizdova, Michala |
459 | 23713828 |
460 | CZE |
461 | 1305 |
462 | MishaGvi |
463 |
464 |
465 | | 51 |
466 | |
467 | Koscelnikova, Monika |
468 | 14967073 |
469 | SVK |
470 | 1267 |
471 | monicka7 |
472 |
473 |
474 | | 52 |
475 | |
476 | Ksenzigh, Adam |
477 | 14976471 |
478 | SVK |
479 | 1226 |
480 | AdamKO10 |
481 |
482 |
483 | | 53 |
484 | |
485 | Rozboril, Alex |
486 | 14972638 |
487 | SVK |
488 | 1221 |
489 | alexr199 |
490 |
491 |
492 | | 54 |
493 | |
494 | Novotny, Tomas |
495 | 14951126 |
496 | SVK |
497 | 1135 |
498 | Tomas_Novotny |
499 |
500 |
501 | | 55 |
502 | |
503 | Kicura, Richard |
504 | 14985756 |
505 | SVK |
506 | 1111 |
507 | RichardKicura |
508 |
509 |
510 | | 56 |
511 | |
512 | Busfy, Svetozar |
513 | |
514 | SVK |
515 | 0 |
516 | svebus |
517 |
518 |
519 | | 57 |
520 | |
521 | Cafal, Pavol |
522 | |
523 | SVK |
524 | 0 |
525 | calfa7 |
526 |
527 |
528 | | 58 |
529 | |
530 | Falat, Tomas |
531 | |
532 | SVK |
533 | 0 |
534 | Tomas1000 |
535 |
536 |
537 | | 59 |
538 | |
539 | Flajs, Marek |
540 | |
541 | SVK |
542 | 0 |
543 | fidzi |
544 |
545 |
546 | | 60 |
547 | |
548 | Hrdy, Milos |
549 | 14984792 |
550 | SVK |
551 | 0 |
552 | Milos1661 |
553 |
554 |
555 | | 61 |
556 | |
557 | Jancik, Peter |
558 | |
559 | SVK |
560 | 0 |
561 | Petrik124 |
562 |
563 |
564 | | 62 |
565 | |
566 | Kohutiar, Milan |
567 | 14965011 |
568 | SVK |
569 | 0 |
570 | milkoh |
571 |
572 |
573 | | 63 |
574 | |
575 | Mlynarcik, Marian |
576 | |
577 | SVK |
578 | 0 |
579 | silason |
580 |
581 |
582 | | 64 |
583 | |
584 | Noga, Marian |
585 | |
586 | SVK |
587 | 0 |
588 | noger79 |
589 |
590 |
591 | | 65 |
592 | |
593 | Nosal, Peter |
594 | 14982102 |
595 | SVK |
596 | 0 |
597 | Nosso7 |
598 |
599 |
600 | | 66 |
601 | |
602 | Roman, Milan |
603 | 14958651 |
604 | SVK |
605 | 0 |
606 | MilanRoman |
607 |
608 |
609 | | 67 |
610 | |
611 | Roth, Marian |
612 | |
613 | SVK |
614 | 0 |
615 | hrac123456 |
616 |
617 |
618 | | 68 |
619 | |
620 | Sausa, Jakub |
621 | |
622 | SVK |
623 | 0 |
624 | ChokablockSK |
625 |
626 |
627 | | 69 |
628 | |
629 | Sramko, Tobias |
630 | |
631 | SVK |
632 | 0 |
633 | TobiBrooo |
634 |
635 |
636 | | 70 |
637 | |
638 | Tatarin, Mykola |
639 | |
640 | |
641 | 0 |
642 | Nik20002353 |
643 |
644 |
645 | | 71 |
646 | |
647 | Toriska, Juraj |
648 | |
649 | SVK |
650 | 0 |
651 | xQ72w |
652 |
653 |
654 |
655 |
--------------------------------------------------------------------------------
/src/scraper/tests/fixtures/individual-swiss-pairings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | | Bo. |
7 | No. |
8 | |
9 | White |
10 | Rtg |
11 | Pts. |
12 | Result |
13 | Pts. |
14 | |
15 | Black |
16 | Rtg |
17 | No. |
18 |
19 |
20 | | 1 |
21 | 24 |
22 | GM |
23 | Gunina, Valentina |
24 | 2348 |
25 | 6 |
26 | 1 - 0 |
27 | 5 |
28 | IM |
29 | Mammadzada, Gunay |
30 | 2408 |
31 | 15 |
32 |
33 |
34 | | 2 |
35 | 32 |
36 | WGM |
37 | Divya, Deshmukh |
38 | 2315 |
39 | 5 |
40 | 0 - 1 |
41 | 5 |
42 | GM |
43 | Kosteniuk, Alexandra |
44 | 2455 |
45 | 7 |
46 |
47 |
48 | | 3 |
49 | 12 |
50 | GM |
51 | Dronavalli, Harika |
52 | 2420 |
53 | 5 |
54 | 1 - 0 |
55 | 5 |
56 | WIM |
57 | Omonova, Umida |
58 | 2304 |
59 | 36 |
60 |
61 |
62 | | 4 |
63 | 18 |
64 | GM |
65 | Zhu, Jiner |
66 | 2384 |
67 | 5 |
68 | 0 - 1 |
69 | 5 |
70 | IM |
71 | Bodnaruk, Anastasia |
72 | 2260 |
73 | 48 |
74 |
75 |
76 | | 5 |
77 | 40 |
78 | IM |
79 | Narva, Mai |
80 | 2292 |
81 | 5 |
82 | ½ - ½ |
83 | 4½ |
84 | GM |
85 | Lagno, Kateryna |
86 | 2522 |
87 | 3 |
88 |
89 |
90 | | 6 |
91 | 6 |
92 | GM |
93 | Goryachkina, Aleksandra |
94 | 2475 |
95 | 4½ |
96 | 1 - 0 |
97 | 4½ |
98 | IM |
99 | Salimova, Nurgyul |
100 | 2343 |
101 | 27 |
102 |
103 |
104 | | 7 |
105 | 108 |
106 | WCM |
107 | Lesbekova, Assel |
108 | 1861 |
109 | 4½ |
110 | 0 - 1 |
111 | 4½ |
112 | IM |
113 | Munguntuul, Batkhuyag |
114 | 2348 |
115 | 25 |
116 |
117 |
118 | | 8 |
119 | 2 |
120 | GM |
121 | Ju, Wenjun |
122 | 2522 |
123 | 4 |
124 | ½ - ½ |
125 | 4 |
126 | IM |
127 | Injac, Teodora |
128 | 2287 |
129 | 41 |
130 |
131 |
132 | | 9 |
133 | 55 |
134 | GM |
135 | Batsiashvili, Nino |
136 | 2233 |
137 | 4 |
138 | 0 - 1 |
139 | 4 |
140 | IM |
141 | Assaubayeva, Bibisara |
142 | 2476 |
143 | 5 |
144 |
145 |
146 | | 10 |
147 | 8 |
148 | GM |
149 | Koneru, Humpy |
150 | 2452 |
151 | 4 |
152 | ½ - ½ |
153 | 4 |
154 | WGM |
155 | Rakshitta, Ravi |
156 | 2225 |
157 | 57 |
158 |
159 |
160 | | 11 |
161 | 60 |
162 | IM |
163 | Garifullina, Leya |
164 | 2216 |
165 | 4 |
166 | 1 - 0 |
167 | 4 |
168 | IM |
169 | Matnadze Bujiashvili, Ann |
170 | 2428 |
171 | 11 |
172 |
173 |
174 | | 12 |
175 | 14 |
176 | IM |
177 | Vaishali, Rameshbabu |
178 | 2410 |
179 | 4 |
180 | 0 - 1 |
181 | 4 |
182 | WGM |
183 | Munkhzul, Turmunkh |
184 | 2211 |
185 | 63 |
186 |
187 |
188 | | 13 |
189 | 67 |
190 | WGM |
191 | Yu, Jennifer |
192 | 2197 |
193 | 4 |
194 | 0 - 1 |
195 | 4 |
196 | WGM |
197 | Wagner, Dinara |
198 | 2350 |
199 | 23 |
200 |
201 |
202 | | 14 |
203 | 28 |
204 | IM |
205 | Shuvalova, Polina |
206 | 2342 |
207 | 4 |
208 | 1 - 0 |
209 | 4 |
210 | WGM |
211 | Berend, Elvira |
212 | 2202 |
213 | 65 |
214 |
215 |
216 | | 15 |
217 | 30 |
218 | GM |
219 | Ushenina, Anna |
220 | 2334 |
221 | 4 |
222 | 1 - 0 |
223 | 4 |
224 | |
225 | Velpula, Sarayu |
226 | 1747 |
227 | 112 |
228 |
229 |
230 | | 16 |
231 | 101 |
232 | FM |
233 | Kazarian, Anna-Maja |
234 | 2023 |
235 | 4 |
236 | 0 - 1 |
237 | 4 |
238 | WGM |
239 | Kamalidenova, Meruert |
240 | 2314 |
241 | 33 |
242 |
243 |
244 | | 17 |
245 | 103 |
246 | WFM |
247 | Aydin, Gulenay |
248 | 2009 |
249 | 4 |
250 | 0 - 1 |
251 | 3½ |
252 | GM |
253 | Muzychuk, Anna |
254 | 2447 |
255 | 9 |
256 |
257 |
258 | | 18 |
259 | 39 |
260 | WFM |
261 | Khamdamova, Afruza |
262 | 2295 |
263 | 3½ |
264 | 0 - 1 |
265 | 3½ |
266 | GM |
267 | Tan, Zhongyi |
268 | 2519 |
269 | 4 |
270 |
271 |
272 | | 19 |
273 | 20 |
274 | GM |
275 | Krush, Irina |
276 | 2359 |
277 | 3½ |
278 | 1 - 0 |
279 | 3½ |
280 | IM |
281 | Nomin-Erdene, Davaademberel |
282 | 2299 |
283 | 38 |
284 |
285 |
286 | | 20 |
287 | 22 |
288 | GM |
289 | Danielian, Elina |
290 | 2352 |
291 | 3½ |
292 | 1 - 0 |
293 | 3½ |
294 | IM |
295 | Guichard, Pauline |
296 | 2281 |
297 | 43 |
298 |
299 |
300 | | 21 |
301 | 50 |
302 | IM |
303 | Bivol, Alina |
304 | 2250 |
305 | 3½ |
306 | 1 - 0 |
307 | 3½ |
308 | IM |
309 | Buksa, Nataliya |
310 | 2316 |
311 | 31 |
312 |
313 |
314 | | 22 |
315 | 37 |
316 | IM |
317 | Padmini, Rout |
318 | 2302 |
319 | 3½ |
320 | 0 - 1 |
321 | 3½ |
322 | WFM |
323 | Shukhman, Anna |
324 | 2093 |
325 | 87 |
326 |
327 |
328 | | 23 |
329 | 88 |
330 | WIM |
331 | Gaboyan, Susanna |
332 | 2086 |
333 | 3½ |
334 | ½ - ½ |
335 | 3 |
336 | GM |
337 | Lei, Tingjie |
338 | 2530 |
339 | 1 |
340 |
341 |
342 | | 24 |
343 | 10 |
344 | GM |
345 | Muzychuk, Mariya |
346 | 2443 |
347 | 3 |
348 | 1 - 0 |
349 | 3 |
350 | WGM |
351 | Pourkashiyan, Atousa |
352 | 2221 |
353 | 59 |
354 |
355 |
356 | | 25 |
357 | 16 |
358 | GM |
359 | Stefanova, Antoaneta |
360 | 2398 |
361 | 3 |
362 | 1 - 0 |
363 | 3 |
364 | WIM |
365 | Balabayeva, Xeniya |
366 | 2202 |
367 | 66 |
368 |
369 |
370 | | 26 |
371 | 72 |
372 | IM |
373 | Gvetadze, Sofio |
374 | 2170 |
375 | 3 |
376 | 0 - 1 |
377 | 3 |
378 | IM |
379 | Khademalsharieh, Sarasadat |
380 | 2395 |
381 | 17 |
382 |
383 |
384 | | 27 |
385 | 68 |
386 | FM |
387 | Sahithi, Varshini M |
388 | 2189 |
389 | 3 |
390 | 0 - 1 |
391 | 3 |
392 | GM |
393 | Khotenashvili, Bella |
394 | 2358 |
395 | 21 |
396 |
397 |
398 | | 28 |
399 | 76 |
400 | IM |
401 | Ovod, Evgenija |
402 | 2151 |
403 | 3 |
404 | 0 - 1 |
405 | 3 |
406 | WGM |
407 | Zhai, Mo |
408 | 2342 |
409 | 29 |
410 |
411 |
412 | | 29 |
413 | 34 |
414 | IM |
415 | Kiolbasa, Oliwia |
416 | 2313 |
417 | 3 |
418 | 1 - 0 |
419 | 3 |
420 | IM |
421 | Soumya, Swaminathan |
422 | 2154 |
423 | 75 |
424 |
425 |
426 | | 30 |
427 | 44 |
428 | WGM |
429 | Ni, Shiqun |
430 | 2270 |
431 | 3 |
432 | 1 - 0 |
433 | 3 |
434 | WGM |
435 | Savitha, Shri B |
436 | 2183 |
437 | 69 |
438 |
439 |
440 | | 31 |
441 | 110 |
442 | WFM |
443 | Kaliakhmet, Elnaz |
444 | 1839 |
445 | 3 |
446 | 0 - 1 |
447 | 3 |
448 | IM |
449 | Badelka, Olga |
450 | 2262 |
451 | 47 |
452 |
453 |
454 | | 32 |
455 | 78 |
456 | WFM |
457 | Tohirjonova, Hulkar |
458 | 2144 |
459 | 3 |
460 | 0 - 1 |
461 | 3 |
462 | IM |
463 | Charochkina, Daria |
464 | 2254 |
465 | 49 |
466 |
467 |
468 | | 33 |
469 | 82 |
470 | FM |
471 | Borisova, Ekaterina |
472 | 2130 |
473 | 3 |
474 | 0 - 1 |
475 | 3 |
476 | WIM |
477 | Nurmanova, Alua |
478 | 2249 |
479 | 51 |
480 |
481 |
482 | | 34 |
483 | 54 |
484 | WGM |
485 | Kovanova, Baira |
486 | 2234 |
487 | 3 |
488 | 0 - 1 |
489 | 3 |
490 | WIM |
491 | Mkrtchyan, Mariam |
492 | 2150 |
493 | 77 |
494 |
495 |
496 | | 35 |
497 | 56 |
498 | WGM |
499 | Hejazipour, Mitra |
500 | 2228 |
501 | 3 |
502 | 1 - 0 |
503 | 3 |
504 | WCM |
505 | Shohradova, Leyla |
506 | 1882 |
507 | 107 |
508 |
509 |
510 | | 36 |
511 | 58 |
512 | WGM |
513 | Priyanka, Nutakki |
514 | 2224 |
515 | 3 |
516 | 1 - 0 |
517 | 3 |
518 | WIM |
519 | Nurgali, Nazerke |
520 | 2068 |
521 | 95 |
522 |
523 |
524 | | 37 |
525 | 102 |
526 | WFM |
527 | Getman, Tatyana |
528 | 2014 |
529 | 2½ |
530 | 0 - 1 |
531 | 2½ |
532 | IM |
533 | Cori T., Deysi |
534 | 2304 |
535 | 35 |
536 |
537 |
538 | | 38 |
539 | 74 |
540 | FM |
541 | Toncheva, Nadya |
542 | 2162 |
543 | 2½ |
544 | 0 - 1 |
545 | 2½ |
546 | IM |
547 | Fataliyeva, Ulviyya |
548 | 2268 |
549 | 45 |
550 |
551 |
552 | | 39 |
553 | 46 |
554 | WGM |
555 | Tokhirjonova, Gulrukhbegim |
556 | 2265 |
557 | 2½ |
558 | 1 - 0 |
559 | 2½ |
560 | WIM |
561 | Serikbay, Assel |
562 | 2126 |
563 | 83 |
564 |
565 |
566 | | 40 |
567 | 52 |
568 | IM |
569 | Mammadova, Gulnar |
570 | 2247 |
571 | 2½ |
572 | ½ - ½ |
573 | 2½ |
574 | WIM |
575 | Kairbekova, Amina |
576 | 2131 |
577 | 81 |
578 |
579 |
580 | | 41 |
581 | 84 |
582 | WGM |
583 | Abdulla, Khayala |
584 | 2118 |
585 | 2½ |
586 | 1 - 0 |
587 | 2½ |
588 | WGM |
589 | Voit, Daria |
590 | 2215 |
591 | 62 |
592 |
593 |
594 | | 42 |
595 | 64 |
596 | IM |
597 | Balajayeva, Khanim |
598 | 2207 |
599 | 2½ |
600 | ½ - ½ |
601 | 2½ |
602 | WFM |
603 | Poliakova, Varvara |
604 | 2049 |
605 | 99 |
606 |
607 |
608 | | 43 |
609 | 90 |
610 | WFM |
611 | Ovezdurdyyeva, Jemal |
612 | 2086 |
613 | 2½ |
614 | 0 - 1 |
615 | 2½ |
616 | WGM |
617 | Strutinskaia, Galina |
618 | 2174 |
619 | 71 |
620 |
621 |
622 | | 44 |
623 | 114 |
624 | |
625 | Aktamova, Zilola |
626 | 1671 |
627 | 2½ |
628 | 0 - 1 |
629 | 2 |
630 | IM |
631 | Arabidze, Meri |
632 | 2416 |
633 | 13 |
634 |
635 |
636 | | 45 |
637 | 89 |
638 | WIM |
639 | Yan, Tianqi |
640 | 2086 |
641 | 2 |
642 | 1 - 0 |
643 | 2 |
644 | GM |
645 | Zhu, Chen |
646 | 2379 |
647 | 19 |
648 |
649 |
650 | | 46 |
651 | 26 |
652 | IM |
653 | Tsolakidou, Stavroula |
654 | 2345 |
655 | 2 |
656 | 1 - 0 |
657 | 2 |
658 | FM |
659 | Kurmangaliyeva, Liya |
660 | 2077 |
661 | 93 |
662 |
663 |
664 | | 47 |
665 | 42 |
666 | WIM |
667 | Lu, Miaoyi |
668 | 2284 |
669 | 2 |
670 | 1 - 0 |
671 | 2 |
672 | WFM |
673 | Bobomurodova, Maftuna |
674 | 1814 |
675 | 111 |
676 |
677 |
678 | | 48 |
679 | 104 |
680 | WFM |
681 | Nurgaliyeva, Zarina |
682 | 2001 |
683 | 2 |
684 | 0 - 1 |
685 | 2 |
686 | WGM |
687 | Yakubbaeva, Nilufar |
688 | 2237 |
689 | 53 |
690 |
691 |
692 | | 49 |
693 | 94 |
694 | WFM |
695 | Hajiyeva, Laman |
696 | 2076 |
697 | 2 |
698 | 0 - 1 |
699 | 2 |
700 | FM |
701 | Peycheva, Gergana |
702 | 2216 |
703 | 61 |
704 |
705 |
706 | | 50 |
707 | 70 |
708 | WGM |
709 | Mamedjarova, Turkan |
710 | 2175 |
711 | 2 |
712 | 1 - 0 |
713 | 2 |
714 | WIM |
715 | Malikova, Marjona |
716 | 1730 |
717 | 113 |
718 |
719 |
720 | | 51 |
721 | 116 |
722 | |
723 | Karimova, Guldona |
724 | 1547 |
725 | 2 |
726 | 0 - 1 |
727 | 2 |
728 | WGM |
729 | Beydullayeva, Govhar |
730 | 2132 |
731 | 79 |
732 |
733 |
734 | | 52 |
735 | 92 |
736 | WFM |
737 | Zhao, Shengxin |
738 | 2079 |
739 | 1½ |
740 | 1 - 0 |
741 | 2 |
742 | |
743 | Altynbek, Aiaru |
744 | 1641 |
745 | 115 |
746 |
747 |
748 | | 53 |
749 | 106 |
750 | WIM |
751 | Hamrakulova, Yulduz |
752 | 1908 |
753 | 1½ |
754 | 0 - 1 |
755 | 1½ |
756 | WFM |
757 | Yakimova, Mariya |
758 | 2098 |
759 | 86 |
760 |
761 |
762 | | 54 |
763 | 117 |
764 | |
765 | Zhunusbekova, Aimonchok |
766 | 1448 |
767 | 1½ |
768 | 0 - 1 |
769 | 1½ |
770 | WFM |
771 | Druzhinina, Olga |
772 | 2055 |
773 | 97 |
774 |
775 |
776 | | 55 |
777 | 98 |
778 | WGM |
779 | Kurbonboeva, Sarvinoz |
780 | 2054 |
781 | 1½ |
782 | 0 - 1 |
783 | 1½ |
784 | WIM |
785 | Franco Valencia, Angela |
786 | 2048 |
787 | 100 |
788 |
789 |
790 | | 56 |
791 | 80 |
792 | |
793 | Khamrakulova, Iroda |
794 | 2132 |
795 | 1 |
796 | 0 - 1 |
797 | 1 |
798 | WFM |
799 | Shohradova, Lala |
800 | 1983 |
801 | 105 |
802 |
803 |
804 | | 57 |
805 | 96 |
806 | WGM |
807 | Zaksaite, Salomeja |
808 | 2065 |
809 | 1 |
810 | 1 - 0 |
811 | 1 |
812 | WIM |
813 | Rudzinska, Michalina |
814 | 2113 |
815 | 85 |
816 |
817 |
818 | | 58 |
819 | 91 |
820 | |
821 | Khamrakulova, Shakhnoza |
822 | 2086 |
823 | 1 |
824 | 0 - 1 |
825 | 1 |
826 | WFM |
827 | Allahverdiyeva, Ayan |
828 | 1852 |
829 | 109 |
830 |
831 |
832 | | 59 |
833 | 118 |
834 | |
835 | Naisanga, Sheba Valentine |
836 | 1358 |
837 | 0 |
838 | 0 - 1 |
839 | ½ |
840 | WIM |
841 | Nadirjanova, Nodira |
842 | 2164 |
843 | 73 |
844 |
845 |
846 |
847 |
--------------------------------------------------------------------------------
|