A Godot Engine app to help streamers organize Majority Judgment polls in their streams.
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.

226 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.
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():
# Instead, use a factory approach.
static func make(__title, __grading, __candidates):
var poll = load("res://addons/majority_judgment/MajorityJudgmentPoll.gd").new()
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()
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!")
for i in range(judgments.size()):
var existing_judgment = judgments[i]
if (
(existing_judgment.participant == judgment.participant)
(existing_judgment.candidate == judgment.candidate)
judgments[i] = 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)
(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():
return __participants_index
func rebuild_participants_index():
for participant in get_participants():
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
__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):
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.colors = get_grading().get_colors()
return merit