$ cd ../
$ cat /backups/brain/
0036
CTFTime automatic points calculation

Automatically calculate CTFTime Rating points manually (scorebard not ready yet) or automatically from a CTFTime entry (for re-calculation or export).

Possible improvements:

  • Proper HTML better parsing or use an API if available
  • Support CTFd scoreboards

Usage:

1
2
3
ctf-points CTFTIME_URL            # Calculate it automatically from CTFTime (LosFuzzys)
ctf-points CTFTIME_URL TEAM_ID    # Calculate it automatically from CTFTime
ctf-points                        # Calculate it manually

Sample output:

1
2
3
4
5
6
7
8
[ecomaikgolf@laptop ../.local/bin/]$ ctf-points https://ctftime.org/event/2273 8323
[+] Team points: 5206.0
[+] Best points: 20412.0
[+] Team place: 7
[+] Weight: 24.5
[+] Final rating: 9.749
     - Part of points:    6.249
     - Part of placement: 3.500

Source (so vulnerable to malicious team names it ain’t even funny lol):

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
#   "requests"
# ]
# ///

import sys
import requests

def lookup(s, start, end):
  w_s = s.find(start) + len(start)
  w_e = s[w_s:].find(end) + w_s
  return s[w_s:w_e]

def calculate(team_points, best_points, team_place, weight):
  # Calculate coefficients
  points_coef = team_points / best_points
  place_coef = 1 / team_place

  # Calculate and display rating
  if points_coef > 0:
    e_rating = (points_coef + place_coef) * weight
    print(f"[+] Final rating: {e_rating:.3f}")
    print(f"     - Part of points:    {points_coef * weight:.3f}")
    print(f"     - Part of placement: {place_coef  * weight:.3f}")
  else:
    print("[!] No points")

def manual():
  # Get user inputs
  team_points = float(input("[>] Team points: "))
  best_points = float(input("[>] Best points: "))
  team_place = float(input("[>] Team place: "))
  weight = float(input("[>] Weight: "))

  calculate(team_points, best_points, team_place, weight)

def automatic(url):
  response = requests.get(url)

  if response.status_code != 200:
      print("[!] Error fetching CTFTime event page")
      exit(1)

  raw = response.text
  
  weight = float(lookup(raw, "Rating weight:", " ").strip())

  table = lookup(raw, "<table class=\"table table-striped past_event_rating\">", "</table>").split("<tr>")[2:]

  bt = None
  lf = None
  teams = [None] * len(table)
  i = 0
  for t in table:

    if(t.strip() == ""):
       continue

    t = t.replace("</td><td class=\"place leader\">", "")
    t = t.replace("</td><td class=\"place\">", "")
    place  = int(lookup(t, "<td class=\"place_ico\">", "</td>"))
    points = float(lookup(t, "<td class=\"points\">", "</td>"))
    rating = float(lookup(t, "</td><td>", "</td>"))
    id     = int(lookup(t, "<a href=\"/team/", "\">"))
    name   = lookup(t, f"<a href=\"/team/{id}\">", "</a>")

    assert teams[i] is None

    teams[i] = {
      "place": place,
      "points": points,
      "rating": rating,
      "id": id,
      "name": name,
    }

    if id == ID :
      lf = teams[i]
    if place == 1:
      bt = teams[i]
    i += 1

  if lf is None:
    print(f"[!] Couldn't find requested Team ID: {ID}")
    exit(1)

  print(f"[+] Team points: {lf["points"]}")
  print(f"[+] Best points: {bt["points"]}")
  print(f"[+] Team place: {lf["place"]}")
  print(f"[+] Weight: {weight}")
  calculate(lf["points"], bt["points"], lf["place"], weight)

ID  = 8323 # LosFuzzys
URL = None
if len(sys.argv) >= 2:
  URL = sys.argv[1]
if len(sys.argv) >= 3:
  ID  = int(sys.argv[2])

if URL is None:
  manual()
else:
  automatic(URL)

# vim: filetype=python
$ cd ../