Skip to main content

Scoring

EU/EFTA teams and guest teams will have seperate scoreboards, but will use the same platforms.

Jeopardy

Jeopardy challenges will using dynamic scoring.

Parameters

solved_by_teams is the number of teams that have solved a challenge.

total_teams is the total number of teams.

max_score is the maximum score for the challenge. In ECSC 2023 this is set 1000 points.

min_score is the minimum score for the challenge. In ECSC 2023 this is set 400 points.

If only one teams solves a challenge, the challenge will be worth the maximum score.

If 75% or more teams solves a challenge, it will be worth the minimum score.

If there are between 1 and 75% of teams that have solved a challenge, the points for that challenge is calculated by a simple linear interpolation between these two endpoints.

Reference implementation

# ECSC 2023 jeopardy dynamic scoring algorithm

def dynamic_score(solved_by_teams: int, total_teams: int, max_score: int, min_score: int):

# Calculate the ratio of solved teams to total teams
ratio = solved_by_teams / total_teams

# Ensure that if only one team solves the challenge, they'll get the max_score
fraction_high_score = 1 / total_teams

# If more than 3/4 of teams solves a challenge it drops to min_score
# "+ fraction_high_score" is there make sure this also works correctly when total_teams
# is an integer multiple of 4
fraction_low_score = 3/4 + fraction_high_score

match ratio:
# Set to the maximum score if less than the fraction for highest score have solved challenge
case ratio if ratio <= fraction_high_score:
score = max_score

# Set to the minimum score if more than the fraction for lowest score have solved challenge
case ratio if ratio >= fraction_low_score:
score = min_score

# All other score must be between max_score and min_score
# Here we use a simple linear interpolation between min_score and max_score when the fraction
# of teams that have solved a challenge is between fraction_hig_score and fraction_low_score
case _:
score = max_score - (max_score - min_score) * (ratio - fraction_high_score) / (fraction_low_score - fraction_high_score)

return round(score)


# Simple tests showing score for a challenge based on number of solves
if __name__ == '__main__':
MAX_SCORE = 1000
MIN_SCORE = 400
TOTAL_TEAMS = 28
print(f"\nECSC EU/EFTA with {TOTAL_TEAMS} teams\n"
f"Solves\tPoints\tPercent drop in score\n{'-' * 80}")
number_of_teams = TOTAL_TEAMS
for i in range(number_of_teams + 1):
score = dynamic_score(i, number_of_teams, MAX_SCORE, MIN_SCORE)
print(f"{i:2}\t\t{score:4}\t{1-(score/MAX_SCORE):>6.2%}")

TOTAL_TEAMS = 7
print(f"\nECSC Guests with {TOTAL_TEAMS} teams\n"
f"Solves\tPoints\tPercent drop in score\n{'-' * 80}")
number_of_teams = TOTAL_TEAMS
for i in range(number_of_teams + 1):
score = dynamic_score(i, number_of_teams, MAX_SCORE, MIN_SCORE)
print(f"{i:2}\t\t{score:4}\t{1-(score/MAX_SCORE):>6.2%}")

Attack/Defense

Tick score per service

Ts=(Ds+Ss)+a=1tAsaT_{s} = \left( D_{s} + S_{s} \right) + \sum_{a=1}^{t} A_{s_{a}}

where:

ss is the service id.

TsT_{s} is the teams' total score in a tick for a service.

DsD_{s} is the defense score in a tick for a service.

SsS_{s} is the SLA score in a tick for a service.

aa is the attack teams' id.

tt is the total number of teams.

AsaA_{s_{a}} is the score for a successful attack on team aa's service in a tick.

Attack

A=1+1f+1{1if rarv1Rif ra>rvand f>0A = 1 + \frac{1}{f} + 1 \cdot \left\{ \begin{array}{cl} 1 & if\ r_{a} \leq r_{v} \\ 1 - R & if\ r_{a} > r_{v} \\ \end{array} \right. and\ f > 0

where:

RR is the score reduction for attacking down, and RR is calculated with:

R=45(rvrat)2R = \frac{4}{5} \cdot \left ( \frac{r_{v}-r_{a}}{t} \right )^{2}

and:

AA is a the attack score for a successful attack on a vitcim in a tick.

ff is the number of teams that have captured a flag from the victimin a tick.

rar_{a} is the rank of attacker in tick flag was lost captured from victim.

rvr_{v} is the rank of the victim in tick flag was lost to attacker.

tt is the total number of teams.

Attack score is only calculated if a flag was captured.

SLA

S={3if service is up32if service is recovering0if service is downS = \left\{ \begin{array}{cl} 3 & if\ service\ is\ up \\ \frac{3}{2} & if\ service\ is\ recovering\\ 0 & if\ service\ is\ down \\ \end{array} \right.

where:

SS is a the SLA score for a keeping the service up in a tick.

A service is considered up if the service checker returns the state UP.

A service is considered recovering if the service checker returns the state RECOVERING.

A service is considered down if service cheke returns any other state.

Defense

D={2+1dif S>0 and d>00if S=0D = \left\{ \begin{array}{cl} 2 + \frac{1}{d} & if\ S > 0\ and\ d > 0 \\ 0 & if\ S = 0 \\ \end{array} \right.

where:

DD is a the defense score for successfully defending a service in a tick from all attackers.

dd is the number of teams that did not loose flags for service in tick.

Full scoring equations

The full form of scoring equations for A/D are documented in this document.

Overall ranking

The overall rank will be based on normalized scores from each day.

Jeopardy will count 50% of the overall score and Attack/Defense will count 50% of the overall score.

Day 1Day 2Day 3
CTF styleJeopardyA/DJeopardy
Weigth percentage25%50%25%
Weigthed normalized max points5000100005000

Calcultation of normalized day score

Calcultation of normalized score per team per day:

Dtd=StdHdNdD_{t_{d}} = \frac{S_{t_{d}}}{H_{d}} \cdot N_{d}

where:

tt is the team number.

dd is the day number.

DtdD_{t_{d}} is a teams normalized score for day dd.

StdS_{t_{d}} is the teams total score of the day dd.

HdH_{d} is the highest total score of the day dd.

NdN_{d} is the weighted normalized max score of the day dd.

Final ranking score

Final ranking score for a team is:

Ft=Dt1+Dt2+Dt3F_{t} = D_{t_{1}} + D_{t_{2}} + D_{t_{3}}

where:

FtF_{t} is the teams' final ranking score.

Dt1D_{t_{1}} is the teams' normalized score from day 1.

Dt2D_{t_{2}} is the teams' normalized score from day 2.

Dt3D_{t_{3}} is the teams' normalized score from day 3.

Tiebreaker

If two teams, xx and yy, end up with the same final ranking score, that is Fx=FyF_{x} = F_{y}, then the results from Attack/Defense on day 2 will be used as a tiebreaker.

The team with the highest score in Attack/Defense will be ranked highest of the teams with equal final ranking score.