Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
T
tabulator-another
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Locked Files
Issues
0
Issues
0
List
Boards
Labels
Service Desk
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Security & Compliance
Security & Compliance
Dependency List
License Compliance
Packages
Packages
List
Container Registry
Analytics
Analytics
CI / CD
Code Review
Insights
Issues
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
MyCard
tabulator-another
Commits
1b55307c
Commit
1b55307c
authored
Feb 21, 2026
by
nanahira
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
rework swiss
parent
5f832c5a
Pipeline
#43364
failed with stages
in 2 minutes and 41 seconds
Changes
3
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
343 additions
and
44 deletions
+343
-44
.dockerignore
.dockerignore
+1
-0
.gitignore
.gitignore
+2
-1
src/tournament-rules/rules/swiss.ts
src/tournament-rules/rules/swiss.ts
+340
-43
No files found.
.dockerignore
View file @
1b55307c
...
...
@@ -12,6 +12,7 @@ lerna-debug.log*
# OS
.DS_Store
._*
# Tests
/coverage
...
...
.gitignore
View file @
1b55307c
...
...
@@ -12,6 +12,7 @@ lerna-debug.log*
# OS
.DS_Store
._*
# Tests
/coverage
...
...
@@ -35,4 +36,4 @@ lerna-debug.log*
/data
/output
/config.yaml
\ No newline at end of file
/config.yaml
src/tournament-rules/rules/swiss.ts
View file @
1b55307c
...
...
@@ -33,14 +33,28 @@ export class Swiss extends TournamentRuleBase {
}
if
(
r
===
1
)
{
const
participants
=
_
.
sortBy
(
this
.
tournament
.
participants
,
this
.
tournament
.
participants
.
filter
((
p
)
=>
!
p
.
quit
)
,
(
p
)
=>
-
p
.
seq
,
(
p
)
=>
-
p
.
id
,
);
for
(
const
match
of
matches
)
{
if
(
participants
.
length
%
2
===
1
)
{
// Lowest seed gets a bye in round 1.
participants
.
shift
();
}
const
seeded
=
participants
.
slice
().
reverse
();
const
half
=
Math
.
floor
(
seeded
.
length
/
2
);
const
top
=
seeded
.
slice
(
0
,
half
);
const
bottom
=
seeded
.
slice
(
half
);
for
(
let
i
=
0
;
i
<
matches
.
length
;
++
i
)
{
const
match
=
matches
[
i
];
match
.
status
=
MatchStatus
.
Running
;
match
.
player1Id
=
participants
.
pop
().
id
;
match
.
player2Id
=
participants
.
pop
().
id
;
match
.
player1Id
=
top
[
i
]?.
id
;
match
.
player2Id
=
bottom
[
i
]?.
id
;
if
(
!
match
.
player1Id
||
!
match
.
player2Id
)
{
match
.
status
=
MatchStatus
.
Abandoned
;
match
.
player1Id
=
null
;
match
.
player2Id
=
null
;
}
}
}
allMatches
.
push
(...
matches
);
...
...
@@ -50,44 +64,37 @@ export class Swiss extends TournamentRuleBase {
nextRound
():
Partial
<
Match
[]
>
{
this
.
tournament
.
calculateScore
();
// score asc
const
participants
=
this
.
tournament
.
participants
.
filter
((
p
)
=>
!
p
.
quit
)
.
reverse
()
.
map
((
p
)
=>
{
const
opponentIds
=
this
.
getOpponentMap
().
get
(
p
.
id
)
??
new
Map
<
number
,
number
>
();
return
{
p
,
opponentIds
,
};
});
const
nextRoundCount
=
this
.
nextRoundCount
();
const
matches
=
this
.
tournament
.
matches
.
filter
(
(
m
)
=>
m
.
round
===
nextRoundCount
,
const
matches
=
_
.
sortBy
(
this
.
tournament
.
matches
.
filter
((
m
)
=>
m
.
round
===
nextRoundCount
),
(
m
)
=>
m
.
id
,
);
for
(
const
match
of
matches
)
{
match
.
status
=
MatchStatus
.
Running
;
const
player1
=
participants
.
pop
();
if
(
player1
&&
participants
.
length
)
{
match
.
player1Id
=
player1
.
p
.
id
;
let
player2Index
=
-
1
;
for
(
let
i
=
0
;
i
<
10
&&
player2Index
===
-
1
;
++
i
)
{
player2Index
=
participants
.
findLastIndex
((
p
)
=>
{
const
metCount
=
player1
.
opponentIds
.
get
(
p
.
p
.
id
);
return
!
metCount
||
metCount
<=
i
;
});
}
if
(
player2Index
===
-
1
)
{
// No suitable player found, so ignore condition of non-met
player2Index
=
participants
.
length
-
1
;
}
const
player2
=
participants
.
splice
(
player2Index
,
1
)[
0
];
if
(
player2
)
{
match
.
player2Id
=
player2
.
p
.
id
;
}
}
if
(
!
match
.
player1Id
||
!
match
.
player2Id
)
{
const
sortedParticipants
=
_
.
sortBy
(
this
.
tournament
.
participants
.
filter
((
p
)
=>
!
p
.
quit
),
(
p
)
=>
-
(
p
.
score
?.
score
??
0
),
(
p
)
=>
-
(
p
.
score
?.
tieBreaker
??
0
),
(
p
)
=>
-
p
.
seq
,
(
p
)
=>
-
p
.
id
,
);
const
active
=
sortedParticipants
.
map
((
p
)
=>
({
p
,
opponentIds
:
this
.
getOpponentMap
().
get
(
p
.
id
)
??
new
Map
<
number
,
number
>
(),
}));
const
byePlayer
=
this
.
pickByePlayer
(
active
);
const
pool
=
byePlayer
?
active
.
filter
((
entry
)
=>
entry
.
p
.
id
!==
byePlayer
.
p
.
id
)
:
active
;
const
pairs
=
this
.
pairSwiss
(
pool
);
for
(
let
i
=
0
;
i
<
matches
.
length
;
++
i
)
{
const
match
=
matches
[
i
];
const
pair
=
pairs
[
i
];
if
(
pair
)
{
match
.
status
=
MatchStatus
.
Running
;
match
.
player1Id
=
pair
[
0
].
p
.
id
;
match
.
player2Id
=
pair
[
1
].
p
.
id
;
}
else
{
match
.
status
=
MatchStatus
.
Abandoned
;
match
.
player1Id
=
null
;
match
.
player2Id
=
null
;
...
...
@@ -96,6 +103,225 @@ export class Swiss extends TournamentRuleBase {
return
matches
;
}
private
pickByePlayer
(
participants
:
Array
<
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
;
}
>
,
)
{
if
(
participants
.
length
%
2
===
0
)
return
null
;
const
roundsMap
=
this
.
getParticipantRoundsMap
();
const
currentRounds
=
this
.
currentRoundCount
();
const
byeCount
=
(
participantId
:
number
)
=>
{
let
count
=
0
;
for
(
let
i
=
1
;
i
<=
currentRounds
;
++
i
)
{
const
roundSet
=
roundsMap
.
get
(
i
);
if
(
!
roundSet
?.
has
(
participantId
))
++
count
;
}
return
count
;
};
// Prefer lowest-ranked players with the fewest byes.
const
candidates
=
participants
.
slice
().
sort
((
a
,
b
)
=>
{
const
byeDiff
=
byeCount
(
a
.
p
.
id
)
-
byeCount
(
b
.
p
.
id
);
if
(
byeDiff
!==
0
)
return
byeDiff
;
const
scoreDiff
=
(
a
.
p
.
score
.
score
??
0
)
-
(
b
.
p
.
score
.
score
??
0
);
if
(
scoreDiff
!==
0
)
return
scoreDiff
;
const
rankDiff
=
(
b
.
p
.
score
.
rank
??
0
)
-
(
a
.
p
.
score
.
rank
??
0
);
if
(
rankDiff
!==
0
)
return
rankDiff
;
const
seqDiff
=
(
b
.
p
.
seq
??
0
)
-
(
a
.
p
.
seq
??
0
);
if
(
seqDiff
!==
0
)
return
seqDiff
;
return
b
.
p
.
id
-
a
.
p
.
id
;
});
return
candidates
[
0
]
??
null
;
}
private
pairSwiss
(
participants
:
Array
<
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
;
}
>
,
):
Array
<
[
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
},
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
},
]
>
{
if
(
!
participants
.
length
)
return
[];
const
byScore
=
_
.
groupBy
(
participants
,
(
entry
)
=>
entry
.
p
.
score
.
score
??
0
);
const
scoreKeys
=
Object
.
keys
(
byScore
)
.
map
(
Number
)
.
sort
((
a
,
b
)
=>
b
-
a
);
const
pairs
:
Array
<
[
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
},
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
},
]
>
=
[];
let
floater
:
|
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
}
|
null
=
null
;
for
(
const
score
of
scoreKeys
)
{
const
bracket
=
_
.
sortBy
(
(
byScore
[
score
]
??
[]).
concat
(
floater
?
[
floater
]
:
[]),
(
entry
)
=>
entry
.
p
.
score
.
rank
??
Number
.
MAX_SAFE_INTEGER
,
(
entry
)
=>
-
(
entry
.
p
.
seq
??
0
),
(
entry
)
=>
-
(
entry
.
p
.
id
??
0
),
);
floater
=
null
;
let
working
=
bracket
.
slice
();
if
(
working
.
length
%
2
===
1
)
{
// Try to float the lowest-ranked player that keeps the bracket pairable.
let
floatIndex
=
working
.
length
-
1
;
for
(
let
i
=
working
.
length
-
1
;
i
>=
0
;
--
i
)
{
const
candidate
=
working
[
i
];
const
rest
=
working
.
slice
(
0
,
i
).
concat
(
working
.
slice
(
i
+
1
));
const
preview
=
this
.
pairEvenBracket
(
rest
,
true
);
if
(
preview
.
length
*
2
===
rest
.
length
)
{
floatIndex
=
i
;
break
;
}
if
((
candidate
.
p
.
score
.
rank
??
0
)
>
(
working
[
floatIndex
].
p
.
score
.
rank
??
0
))
{
floatIndex
=
i
;
}
}
floater
=
working
.
splice
(
floatIndex
,
1
)[
0
];
}
const
bracketPairs
=
this
.
pairEvenBracket
(
working
,
false
);
pairs
.
push
(...
bracketPairs
);
}
if
(
floater
)
{
// Should rarely happen when player count is even after bye allocation.
// Leave the leftover as an implicit bye by not creating a match.
}
return
pairs
;
}
private
pairEvenBracket
(
players
:
Array
<
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
;
}
>
,
strictOnly
:
boolean
,
):
Array
<
[
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
},
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
},
]
>
{
if
(
!
players
.
length
)
return
[];
if
(
players
.
length
%
2
===
1
)
return
[];
const
maxMet
=
_
.
max
(
players
.
flatMap
((
a
)
=>
players
.
map
((
b
)
=>
(
a
.
p
.
id
===
b
.
p
.
id
?
0
:
this
.
metCount
(
a
,
b
))),
),
);
const
tolerances
=
strictOnly
?
[
0
]
:
_
.
range
(
0
,
(
maxMet
??
0
)
+
1
);
const
half
=
players
.
length
/
2
;
const
top
=
players
.
slice
(
0
,
half
);
const
bottom
=
players
.
slice
(
half
);
for
(
const
tolerance
of
tolerances
)
{
for
(
let
offset
=
0
;
offset
<
half
;
++
offset
)
{
const
candidate
:
Array
<
[
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
},
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
},
]
>
=
[];
let
ok
=
true
;
for
(
let
i
=
0
;
i
<
half
;
++
i
)
{
const
a
=
top
[
i
];
const
b
=
bottom
[(
i
+
offset
)
%
half
];
if
(
this
.
metCount
(
a
,
b
)
>
tolerance
)
{
ok
=
false
;
break
;
}
candidate
.
push
([
a
,
b
]);
}
if
(
ok
)
return
candidate
;
}
}
// Fallback greedy pairing with minimum rematch count.
const
remaining
=
players
.
slice
();
const
greedy
:
Array
<
[
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
},
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
},
]
>
=
[];
while
(
remaining
.
length
>=
2
)
{
const
a
=
remaining
.
shift
();
let
bestIndex
=
0
;
for
(
let
i
=
1
;
i
<
remaining
.
length
;
++
i
)
{
const
current
=
remaining
[
i
];
const
best
=
remaining
[
bestIndex
];
const
metDiff
=
this
.
metCount
(
a
,
current
)
-
this
.
metCount
(
a
,
best
);
if
(
metDiff
<
0
)
{
bestIndex
=
i
;
continue
;
}
if
(
metDiff
>
0
)
continue
;
const
rankDiffCurrent
=
Math
.
abs
(
(
a
.
p
.
score
.
rank
??
Number
.
MAX_SAFE_INTEGER
)
-
(
current
.
p
.
score
.
rank
??
Number
.
MAX_SAFE_INTEGER
),
);
const
rankDiffBest
=
Math
.
abs
(
(
a
.
p
.
score
.
rank
??
Number
.
MAX_SAFE_INTEGER
)
-
(
best
.
p
.
score
.
rank
??
Number
.
MAX_SAFE_INTEGER
),
);
if
(
rankDiffCurrent
<
rankDiffBest
)
bestIndex
=
i
;
}
const
b
=
remaining
.
splice
(
bestIndex
,
1
)[
0
];
greedy
.
push
([
a
,
b
]);
}
return
greedy
;
}
private
metCount
(
a
:
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
},
b
:
{
p
:
Participant
;
opponentIds
:
Map
<
number
,
number
>
},
)
{
return
a
.
opponentIds
.
get
(
b
.
p
.
id
)
??
0
;
}
private
asNonNegativeInt
(
value
:
number
,
fallback
=
0
)
{
const
parsed
=
Number
(
value
);
if
(
!
Number
.
isFinite
(
parsed
))
return
fallback
;
return
Math
.
max
(
0
,
Math
.
floor
(
parsed
));
}
private
bitsForMax
(
maxValue
:
number
)
{
const
normalized
=
this
.
asNonNegativeInt
(
maxValue
,
0
);
return
Math
.
max
(
1
,
Math
.
ceil
(
Math
.
log2
(
normalized
+
1
)));
}
private
compressValueForBits
(
value
:
number
,
fullBits
:
number
,
useBits
:
number
)
{
if
(
useBits
<=
0
)
return
0
;
const
normalized
=
this
.
asNonNegativeInt
(
value
,
0
);
if
(
useBits
>=
fullBits
)
return
normalized
;
return
normalized
>>
(
fullBits
-
useBits
);
}
private
pushBits
(
base
:
bigint
,
value
:
number
,
bits
:
number
)
{
if
(
bits
<=
0
)
return
base
;
const
b
=
BigInt
(
bits
);
const
mask
=
(
1
n
<<
b
)
-
1
n
;
return
(
base
<<
b
)
|
(
BigInt
(
value
)
&
mask
);
}
private
participantRoundsMap
:
Map
<
number
,
Set
<
number
>>
|
null
=
null
;
private
getParticipantRoundsMap
():
Map
<
number
,
Set
<
number
>>
{
...
...
@@ -175,11 +401,82 @@ export class Swiss extends TournamentRuleBase {
participantScoreAfter
(
participant
:
Participant
):
Partial
<
ParticipantScore
>
{
const
opponentIds
=
this
.
getOpponentMap
().
get
(
participant
.
id
)
??
new
Map
();
const
opponents
=
Array
.
from
(
opponentIds
.
keys
()).
map
((
id
)
=>
this
.
participantMap
.
get
(
id
),
const
opponentScores
:
number
[]
=
[];
for
(
const
[
opponentId
,
metCount
]
of
opponentIds
.
entries
())
{
const
opponent
=
this
.
participantMap
.
get
(
opponentId
);
if
(
!
opponent
)
continue
;
for
(
let
i
=
0
;
i
<
metCount
;
++
i
)
{
opponentScores
.
push
(
this
.
asNonNegativeInt
(
opponent
.
score
?.
score
??
0
));
}
}
const
sorted
=
opponentScores
.
slice
().
sort
((
a
,
b
)
=>
a
-
b
);
const
buchholz
=
this
.
asNonNegativeInt
(
_
.
sum
(
sorted
));
const
medianBuchholz
=
this
.
asNonNegativeInt
(
sorted
.
length
>=
3
?
_
.
sum
(
sorted
.
slice
(
1
,
-
1
))
:
buchholz
,
);
const
rounds
=
this
.
asNonNegativeInt
(
this
.
settings
.
rounds
??
this
.
currentRoundCount
(),
1
,
);
const
maxRoundScore
=
this
.
asNonNegativeInt
(
Math
.
max
(
this
.
settings
.
winScore
??
0
,
this
.
settings
.
drawScore
??
0
,
this
.
settings
.
byeScore
??
0
,
),
0
,
);
// Worst-case opponent score sum upper bound across configured rounds.
const
maxPlayerScore
=
rounds
*
maxRoundScore
;
const
maxBuchholz
=
rounds
*
maxPlayerScore
;
const
fullBuchholzBits
=
this
.
bitsForMax
(
maxBuchholz
);
const
winsBits
=
this
.
bitsForMax
(
rounds
);
const
drawsBits
=
this
.
bitsForMax
(
rounds
);
let
medianBits
=
fullBuchholzBits
;
let
buchholzBits
=
fullBuchholzBits
;
let
overflow
=
medianBits
+
buchholzBits
+
winsBits
+
drawsBits
-
32
;
if
(
overflow
>
0
)
{
const
reduceSecondary
=
Math
.
min
(
overflow
,
buchholzBits
);
buchholzBits
-=
reduceSecondary
;
overflow
-=
reduceSecondary
;
}
if
(
overflow
>
0
)
{
medianBits
=
Math
.
max
(
1
,
medianBits
-
overflow
);
}
const
packedMedian
=
this
.
compressValueForBits
(
medianBuchholz
,
fullBuchholzBits
,
medianBits
,
);
const
packedBuchholz
=
this
.
compressValueForBits
(
buchholz
,
fullBuchholzBits
,
buchholzBits
,
);
const
wins
=
Math
.
min
(
this
.
asNonNegativeInt
(
participant
.
score
?.
win
??
0
),
rounds
,
);
const
draws
=
Math
.
min
(
this
.
asNonNegativeInt
(
participant
.
score
?.
draw
??
0
),
rounds
,
);
let
tieBreaker
=
0
n
;
tieBreaker
=
this
.
pushBits
(
tieBreaker
,
packedMedian
,
medianBits
);
tieBreaker
=
this
.
pushBits
(
tieBreaker
,
packedBuchholz
,
buchholzBits
);
tieBreaker
=
this
.
pushBits
(
tieBreaker
,
wins
,
winsBits
);
tieBreaker
=
this
.
pushBits
(
tieBreaker
,
draws
,
drawsBits
);
return
{
tieBreaker
:
_
.
sumBy
(
opponents
,
(
p
)
=>
p
.
score
.
score
),
// u32 packed priority:
// median Buchholz > Buchholz > wins > draws.
tieBreaker
:
Number
(
tieBreaker
&
0xffffffff
n
),
...(
participant
.
quit
?
{
score
:
-
1
}
:
{}),
};
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment