Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rogaine (calculate penalty, send scores online, print results and splits) #439

Merged
merged 1 commit into from
Sep 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions languages/ru_RU/LC_MESSAGES/sportorg.po
Original file line number Diff line number Diff line change
Expand Up @@ -636,8 +636,8 @@ msgstr "Режим присвоения"
msgid "by time"
msgstr "по времени"

msgid "by scores"
msgstr "по баллам"
msgid "by scores (rogaine)"
msgstr "по баллам (рогейн)"

msgid "ardf"
msgstr "ADRF"
Expand All @@ -651,6 +651,9 @@ msgstr "фиксированные баллы за КП"
msgid "minute penalty"
msgstr "Штраф за каждую просроченную минуту"

msgid "maximum overrun time"
msgstr "Макс. превышение контрольного времени"

msgid "no penalty"
msgstr "штраф не начисляется"

Expand Down Expand Up @@ -756,6 +759,12 @@ msgstr "Командные результаты"
msgid "Scores"
msgstr "Очки"

msgid "Points gained"
msgstr "Набрано очков"

msgid "Penalty for finishing late"
msgstr "Штраф за превышение"

msgid "Penalty calculation"
msgstr "Штраф"

Expand Down
44 changes: 40 additions & 4 deletions sportorg/gui/dialogs/timekeeping_properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,12 +109,19 @@ def init_ui(self):
# result processing tab
self.result_proc_tab = QWidget()
self.result_proc_layout = QFormLayout()
self.result_processing_group = QGroupBox(translate('Result processing'))
self.result_processing_layout = QFormLayout()
self.rp_time_radio = QRadioButton(translate('by time'))
self.result_proc_layout.addRow(self.rp_time_radio)
self.rp_time_radio.toggled.connect(self.rp_result_calculation_mode)
self.result_processing_layout.addRow(self.rp_time_radio)
self.rp_ardf_radio = QRadioButton(translate('ardf'))
self.result_proc_layout.addRow(self.rp_ardf_radio)
self.rp_scores_radio = QRadioButton(translate('by scores'))
self.result_proc_layout.addRow(self.rp_scores_radio)
self.rp_ardf_radio.toggled.connect(self.rp_result_calculation_mode)
self.result_processing_layout.addRow(self.rp_ardf_radio)
self.rp_scores_radio = QRadioButton(translate('by scores (rogaine)'))
self.rp_scores_radio.toggled.connect(self.rp_result_calculation_mode)
self.result_processing_layout.addRow(self.rp_scores_radio)
self.result_processing_group.setLayout(self.result_processing_layout)
self.result_proc_layout.addRow(self.result_processing_group)

self.rp_scores_group = QGroupBox()
self.rp_scores_layout = QFormLayout(self.rp_scores_group)
Expand All @@ -130,6 +137,15 @@ def init_ui(self):
self.rp_scores_layout.addRow(
self.rp_scores_minute_penalty_label, self.rp_scores_minute_penalty_edit
)
self.rp_scores_max_overrun_time_label = QLabel(
translate('maximum overrun time')
)
self.rp_scores_max_overrun_time = AdvTimeEdit(
max_width=80, display_format='HH:mm:ss'
)
self.rp_scores_layout.addRow(
self.rp_scores_max_overrun_time_label, self.rp_scores_max_overrun_time
)
self.rp_scores_allow_duplicates = QCheckBox(translate('allow duplicates'))
self.rp_scores_allow_duplicates.setToolTip(
translate(
Expand Down Expand Up @@ -335,6 +351,12 @@ def on_assignment_mode(self):
self.chip_reading_box.setDisabled(mode)
self.chip_duplicate_box.setDisabled(mode)

def rp_result_calculation_mode(self):
if self.rp_scores_radio.isChecked():
self.rp_scores_group.show()
else:
self.rp_scores_group.hide()

def penalty_calculation_mode(self):
if (
self.mr_lap_station_check.isChecked()
Expand Down Expand Up @@ -455,6 +477,12 @@ def set_values_from_model(self):
rp_scores_minute_penalty = obj.get_setting(
'result_processing_scores_minute_penalty', 1
)
rp_scores_max_overrun_time = OTime(
msec=obj.get_setting(
'result_processing_scores_max_overrun_time', 30 * 60 * 1000
)
)

rp_scores_allow_duplicates = obj.get_setting(
'result_processing_scores_allow_duplicates', False
)
Expand All @@ -473,6 +501,7 @@ def set_values_from_model(self):

self.rp_fixed_scores_edit.setValue(rp_fixed_scores_value)
self.rp_scores_minute_penalty_edit.setValue(rp_scores_minute_penalty)
self.rp_scores_max_overrun_time.setTime(rp_scores_max_overrun_time.to_time())
self.rp_scores_allow_duplicates.setChecked(rp_scores_allow_duplicates)

# penalty calculation
Expand Down Expand Up @@ -631,6 +660,10 @@ def apply_changes_impl(self):
rp_fixed_scores_value = self.rp_fixed_scores_edit.value()

rp_scores_minute_penalty = self.rp_scores_minute_penalty_edit.value()
rp_scores_max_overrun_time = (
self.rp_scores_max_overrun_time.getOTime().to_msec()
)

rp_scores_allow_duplicates = self.rp_scores_allow_duplicates.isChecked()

obj.set_setting('result_processing_mode', rp_mode)
Expand All @@ -639,6 +672,9 @@ def apply_changes_impl(self):
obj.set_setting(
'result_processing_scores_minute_penalty', rp_scores_minute_penalty
)
obj.set_setting(
'result_processing_scores_max_overrun_time', rp_scores_max_overrun_time
)
obj.set_setting(
'result_processing_scores_allow_duplicates', rp_scores_allow_duplicates
)
Expand Down
24 changes: 14 additions & 10 deletions sportorg/models/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,8 @@ def __init__(self):
self.penalty_laps = 0 # count of penalty legs (marked route)
self.place = 0
self.scores = 0
self.scores_rogain = 0
self.rogaine_score = 0
self.rogaine_penalty = 0
self.scores_ardf = 0
self.assigned_rank = Qualification.NOT_QUALIFIED
self.diff: Optional[OTime] = None # readonly
Expand Down Expand Up @@ -514,7 +515,7 @@ def __eq__(self, other) -> bool:
else:
return False
else: # process by score (rogain)
eq = eq and self.scores_rogain == other.scores_rogain
eq = eq and self.rogaine_score == other.rogaine_score
if eq and self.get_start_time() and other.get_start_time():
eq = eq and self.get_start_time() == other.get_start_time()
if eq and self.get_finish_time() and other.get_finish_time():
Expand Down Expand Up @@ -548,10 +549,10 @@ def __gt__(self, other) -> bool: # greater is worse
else:
return self.scores_ardf < other.scores_ardf
else: # process by score (rogain)
if self.scores_rogain == other.scores_rogain:
if self.rogaine_score == other.rogaine_score:
return self.get_result_otime() > other.get_result_otime()
else:
return self.scores_rogain < other.scores_rogain
return self.rogaine_score < other.rogaine_score

@property
@abstractmethod
Expand Down Expand Up @@ -581,7 +582,8 @@ def to_dict(self):
'card_number': self.card_number,
'speed': self.speed, # readonly
'scores': self.scores, # readonly
'scores_rogain': self.scores_rogain, # readonly
'rogaine_score': self.rogaine_score, # readonly
'rogaine_penalty': self.rogaine_penalty, # readonly
'scores_ardf': self.scores_ardf, # readonly
'created_at': self.created_at, # readonly
'result': self.get_result(), # readonly
Expand Down Expand Up @@ -610,8 +612,10 @@ def update_data(self, data):
self.scores = data['scores']
if 'scores_ardf' in data and data['scores_ardf'] is not None:
self.scores_ardf = data['scores_ardf']
if 'scores_rogain' in data and data['scores_rogain'] is not None:
self.scores_rogain = data['scores_rogain']
if 'rogaine_score' in data and data['rogaine_score'] is not None:
self.rogaine_score = data['rogaine_score']
if 'rogaine_penalty' in data and data['rogaine_penalty'] is not None:
self.rogaine_penalty = data['rogaine_penalty']
if str(data['place']).isdigit():
self.place = int(data['place'])
self.assigned_rank = Qualification.get_qual_by_code(data['assigned_rank'])
Expand Down Expand Up @@ -665,7 +669,7 @@ def get_result(self) -> str:
if race().get_setting('result_processing_mode', 'time') == 'ardf':
ret += f"{self.scores_ardf} {translate('points')} "
elif race().get_setting('result_processing_mode', 'time') == 'scores':
ret += f"{self.scores_rogain} {translate('points')} "
ret += f"{self.rogaine_score} {translate('points')} "

time_accuracy = race().get_setting('time_accuracy', 0)
ret += self.get_result_otime().to_str(time_accuracy)
Expand All @@ -684,7 +688,7 @@ def get_result_start_in_comment(self):
if race().get_setting('result_processing_mode', 'time') == 'ardf':
ret += f"{self.scores_ardf} {translate('points')} "
elif race().get_setting('result_processing_mode', 'time') == 'scores':
ret += f"{self.scores_rogain} {translate('points')} "
ret += f"{self.rogaine_score} {translate('points')} "

# time_accuracy = race().get_setting('time_accuracy', 0)
start = hhmmss_to_time(self.person.comment)
Expand Down Expand Up @@ -720,7 +724,7 @@ def get_result_relay(self) -> str:
if race().get_setting('result_processing_mode', 'time') == 'ardf':
ret += f"{self.scores_ardf} {translate('points')} "
elif race().get_setting('result_processing_mode', 'time') == 'scores':
ret += f"{self.scores_rogain} {translate('points')} "
ret += f"{self.rogaine_score} {translate('points')} "

time_accuracy = race().get_setting('time_accuracy', 0)
ret += self.get_result_otime_relay().to_str(time_accuracy)
Expand Down
10 changes: 7 additions & 3 deletions sportorg/models/result/result_calculation.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ def get_group_persons(self, group):
self._group_persons[group] = ret
return ret

@staticmethod
def set_places(array):
def set_places(self, array):
is_rogaine = self.race.get_setting('result_processing_mode', 'time') == 'scores'
is_ardf = self.race.get_setting('result_processing_mode', 'time') == 'ardf'
current_place = 1
last_place = 1
last_result = 0
Expand All @@ -86,7 +87,10 @@ def set_places(array):
if res.is_status_ok():
current_result = res.get_result_otime()
res.diff = current_result - array[0].get_result_otime()
res.diff_scores = array[0].scores - res.scores
if is_rogaine:
res.diff_scores = array[0].rogaine_score - res.rogaine_score
elif is_ardf:
res.diff_scores = array[0].scores_ardf - res.scores_ardf

# skip if out of competition
if res.person.is_out_of_competition:
Expand Down
88 changes: 69 additions & 19 deletions sportorg/models/result/result_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,18 @@ def check_result(self, result: ResultSportident):
result.scores_ardf = self.calculate_scores_ardf(result)
return True
elif race().get_setting('result_processing_mode', 'time') == 'scores':
# process by score (rogain)
result.scores_rogain = self.calculate_scores_rogain(result)
# process by score (rogaine)
allow_duplicates = race().get_setting(
'result_processing_scores_allow_duplicates', False
)
penalty_step = race().get_setting(
'result_processing_scores_minute_penalty', 1
)

score = self.calculate_rogaine_score(result, allow_duplicates)
penalty = self.calculate_rogaine_penalty(result, score, penalty_step)
result.rogaine_score = score - penalty
result.rogaine_penalty = penalty
return True

course = race().find_course(result)
Expand Down Expand Up @@ -73,10 +83,22 @@ def checking(cls, result):
result.status = ResultStatus.MISS_PENALTY_LAP

elif result.person.group and result.person.group.max_time.to_msec():
if result.get_result_otime() > result.person.group.max_time:
if race().get_setting('result_processing_mode', 'time') == 'time':
rp_mode = race().get_setting('result_processing_mode', 'time')
result_time = result.get_result_otime()
max_time = result.person.group.max_time
if rp_mode in ('time', 'ardf'):
if result_time > max_time:
result.status = ResultStatus.OVERTIME
elif race().get_setting('result_processing_mode', 'time') == 'ardf':
elif rp_mode == 'scores':
max_overrun_time = OTime(
msec=race().get_setting(
'result_processing_scores_max_overrun_time', 0
)
)
if (
max_overrun_time.to_msec() > 0
and result_time > max_time + max_overrun_time
):
result.status = ResultStatus.OVERTIME

result.status_comment = StatusComments().get_status_default_comment(
Expand Down Expand Up @@ -332,33 +354,61 @@ def get_control_score(code):
return int(code) // 10 # score = code / 10

@staticmethod
def calculate_scores_rogain(result):
user_array = []
ret = 0
def calculate_rogaine_score(result: Result, allow_duplicates: bool = False) -> int:
"""
Calculates the rogaine score for a given result.

allow_duplicates = race().get_setting(
'result_processing_scores_allow_duplicates', False
)
Parameters:
result (Result): The result for which the rogaine score needs to be calculated.
allow_duplicates (bool, optional): Whether to allow duplicate control points. Defaults to False.

Returns:
int: The calculated rogaine score.

If `allow_duplicates` flag is `True`, the function allows duplicate control points
to be included in the score calculation.
"""
user_array = []
score = 0

for cur_split in result.splits:
code = str(cur_split.code)
if code not in user_array or allow_duplicates:
user_array.append(code)
ret += ResultChecker.get_control_score(code)
score += ResultChecker.get_control_score(code)

return score

@staticmethod
def calculate_rogaine_penalty(
result: Result, score: int, penalty_step: int = 1
) -> int:
"""
Calculates the penalty for a given result based on the participant's excess of a race time.

Parameters:
result (Result): The result for which the penalty needs to be calculated.
score (int): The competitor's score.
penalty_step (int, optional): The penalty points for each minute late. Defaults to 1.

Returns:
int: The calculated penalty for the result.

"""
penalty = 0
if result.person and result.person.group:
user_time = result.get_result_otime()
max_time = result.person.group.max_time
if OTime() < max_time < user_time:
time_diff = user_time - max_time
seconds_diff = time_diff.to_sec()
minutes_diff = (seconds_diff + 59) // 60 # note, 1:01 = 2 minutes
penalty_step = race().get_setting(
'result_processing_scores_minute_penalty', 1.0
)
ret -= minutes_diff * penalty_step
if ret < 0:
ret = 0
return ret
penalty = minutes_diff * penalty_step

# result = score - penalty >= 0
penalty = min(penalty, score)

return penalty

@staticmethod
def calculate_scores_ardf(result):
Expand Down
4 changes: 4 additions & 0 deletions sportorg/modules/live/orgeo.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ def _get_person_obj(data, race_data, result=None):

if race_data['settings']['result_processing_mode'] == 'ardf':
obj['score'] = result['scores_ardf']
elif race_data['settings']['result_processing_mode'] == 'scores':
obj['score'] = result['rogaine_score']
if result['rogaine_penalty'] > 0:
obj['penalty'] = str(result['rogaine_penalty'])

obj['result_status'] = (
RESULT_STATUS[int(result['status'])]
Expand Down
16 changes: 9 additions & 7 deletions sportorg/modules/printing/printing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import time
from multiprocessing import Process, Queue

from PySide6.QtCore import QSizeF
from PySide6.QtGui import QTextDocument
from PySide6.QtCore import QMarginsF, QSizeF
from PySide6.QtGui import QPageLayout, QTextDocument
from PySide6.QtPrintSupport import QPrinter
from PySide6.QtWidgets import QApplication

Expand Down Expand Up @@ -63,11 +63,13 @@ def run(self):

printer.setFullPage(True)
printer.setPageMargins(
self.margin_left,
self.margin_top,
self.margin_right,
self.margin_bottom,
QPrinter.Millimeter,
QMarginsF(
self.margin_left,
self.margin_top,
self.margin_right,
self.margin_bottom,
),
QPageLayout.Unit.Millimeter,
)

page_size = QSizeF()
Expand Down
Loading
Loading