You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

227 lines
5.8 KiB

extends Resource
class_name MajorityJudgmentPoll
"""
A poll with multiple candidates, where each participant
gives a single grade to each of the candidates.
The candidates are then sorted by their median grade.
https://en.wikipedia.org/wiki/Majority_Judgment
The constitutive details of the poll are up to you, of course,
the poll can be imperative or informative.
"""
const NOT_OPENED := -1
const NOT_CLOSED := -1
# Minimum length: 4 unicode characters (? TBD)
# Maximum length: 256 unicode characters (? TBD)
export(String) var title:String setget set_title, get_title
# > How do we localize this?
# Something like this, perhaps?
# title_of_locale = {
# 'en_US': "President of the United States of America in 2020",
# 'fr_FR': "Président des États-Unis d'Amérique en 2020",
# …
# }
#export(Dictionary) var localized_titles:Dictionary
# or perhaps
# https://docs.godotengine.org/en/stable/tutorials/i18n/internationalizing_games.html
export(Resource) var grading #:MajorityJudgmentGrading
# Array of MajorityJudgmentCandidate
export(Array, Resource) var candidates:Array setget set_candidates, get_candidates
# Array of MajorityJudgmentJudgments
# If you mutate this property directly and not through add_judgment(),
# remember to update the memoization cache as well with update_participants_index()
export(Array, Resource) var judgments:Array setget set_judgments, get_judgments
# Seconds since UNIX EPOCH (01-01-1970)
export var opened_at:int = NOT_OPENED
export var closed_at:int = NOT_CLOSED
# Don't pass parameters in _init().
# ResourceLoader.load() won't like it.
func _init():
pass
# Instead, use a factory approach.
static func make(__title, __grading, __candidates):
var poll = load("res://addons/majority_judgment/MajorityJudgmentPoll.gd").new()
poll.set_title(__title)
poll.set_grading(__grading)
poll.set_candidates(__candidates)
return poll
func set_title(__title:String) -> void:
title = __title
func get_title() -> String:
if null == title:
return ''
return title
func set_grading(__grading:MajorityJudgmentGrading) -> void:
grading = __grading
func get_grading() -> MajorityJudgmentGrading:
assert(grading, "Poll has no grading.")
return grading
func set_candidates(__candidates:Array) -> void:
assert(__candidates, "Poll requires at least one candidate.")
candidates = __candidates
func add_candidate(candidate:MajorityJudgmentCandidate) -> void:
if not candidates:
candidates = Array()
candidates.append(candidate)
func get_candidates() -> Array:
assert(candidates, "Poll has no candidates.")
return candidates
func count_candidates() -> int:
if not candidates:
return 0
return candidates.size()
func has_judgments() -> bool:
if not judgments:
return false
return 0 < judgments.size()
func set_judgments(__judgments:Array) -> void:
judgments = __judgments
func add_judgment(judgment:MajorityJudgmentJudgment) -> void:
if not judgments:
judgments = Array()
if not judgment.candidate in self.candidates:
printerr("Judgment Candidate is not in the Poll!")
return
for i in range(judgments.size()):
var existing_judgment = judgments[i]
if (
(existing_judgment.participant == judgment.participant)
and
(existing_judgment.candidate == judgment.candidate)
):
judgments[i] = judgment
return
update_participants_index(judgment.participant)
judgments.append(judgment)
func get_judgments() -> Array:
# assert(judgments, "Poll has no judgments.")
return judgments
func is_open() -> bool:
var now = App.get_now()
return (
(NOT_OPENED != opened_at and opened_at <= now)
and
(NOT_CLOSED == closed_at or closed_at >= now)
)
func is_closed() -> bool:
return not is_open()
func open() -> void:
assert(NOT_OPENED == opened_at, "Cannot open() an already opened poll.")
opened_at = App.get_now()
func tally() -> MajorityJudgmentPollTally:
if not has_judgments():
return null
# Pick relevant tallier from settings later on
var tallier = MajorityJudgmentEasyTallier.new()
# var tallier = MajorityJudgmentLiquidTallier.new()
return tallier.tally(self)
func get_or_create_participant(identifier:String) -> MajorityJudgmentParticipant:
identifier = MajorityJudgmentParticipant.sanitize_name(identifier)
var known_participants = get_participants_index()
if not known_participants.has(identifier):
known_participants[identifier] = MajorityJudgmentParticipant.make(identifier)
return known_participants[identifier]
# Memoization of expensive computation
var __participants_index := Dictionary() # id => Participant
func get_participants_index():
if __participants_index.empty():
rebuild_participants_index()
return __participants_index
func rebuild_participants_index():
for participant in get_participants():
assert(
not __participants_index.has(participant.name),
"Participants index should be consistent: names should be unique."
)
__participants_index[participant.name] = participant
func update_participants_index(participant:MajorityJudgmentParticipant):
# Zealous sanitization, since Participants also handle it on their names
var identifier = MajorityJudgmentParticipant.sanitize_name(participant.name)
if not __participants_index.has(identifier):
__participants_index[identifier] = participant
assert(
__participants_index[identifier] == participant,
"Participants index should be consistent."
)
func get_participants() -> Array:
var participants := Array()
for judgment in judgments:
var participant : MajorityJudgmentParticipant = judgment.participant
if not participants.has(participant):
participants.append(participant)
return participants
func count_participants() -> int:
return get_participants().size()
func get_dummy_merit_profile(candidate):
var merit = MajorityJudgmentCandidateMeritProfile.new()
merit.grades = Array()
for i in range(get_grading().grades.size()):
merit.grades.append(1)
merit.colors = get_grading().get_colors()
return merit