luac_voting/voting.lua
2025-03-06 17:38:48 +03:00

352 lines
16 KiB
Lua

-- The machine requires _two_ touch screens connected: one set to channel "vote", other set to "admin"
local categories = {"Creativity", "Appeal", "Execution", "Use of space"} -- used for buttons and current sort
local ENTRIES_PER_PAGE = 8 -- for ballots with large amount of entries
function show_vote_welcome()
-- window shown before player can see the ballot
-- by making them click the button, we can know their name
digiline_send("vote",
{
{command = "clear"},
{command = "set",
width = 6,
height = 2,
real_coordinates = true,
},
{command = "addlabel", label = "Welcome!", X = 0.5, Y = 1.0},
{command = "addbutton", name = "start", label = "Start Vote", X = 2.6, Y = 0.5, W = 3, H = 1},
}
)
end
function show_vote_error(voter, offender)
-- show this when owner of the ballot and clicker do not match
local msg = string.format("%s has tried to cast a vote instead of %s!\nThey also saw %s's votes, they're naughty!", offender, voter, voter)
digiline_send("vote",
{
{command = "clear"},
{command = "set",
width = 10.5,
height = 11,
real_coordinates = true,
},
{command = "addlabel", label = "ERROR!", X = 4.7, Y = 2.4},
{command = "addtextarea", name = "", label = "", default = msg, X = 1.3, Y = 3.2, W = 7.9, H = 4.1},
{command = "addbutton", name = "start", label = "Start", X = 3.8, Y = 9.6, W = 3, H = 0.8},
{command = "addlabel", label = "Try again:", X = 1.4, Y = 10}
}
)
end
function show_admin_welcome()
-- just a menu to select from 2 windows
digiline_send("admin",
{
{command = "clear"},
{command = "set",
width = 10.5,
height = 11,
real_coordinates = true,
locked = true,
},
{command = "addlabel", label = "Admin only!", X = 4.4, Y = 1.7},
{command = "addbutton", name = "edit_entries", label = "Edit entries", X = 3.7, Y = 3.3, W = 3, H = 0.8},
{command = "addbutton", name = "view_results", label = "View results", X = 3.7, Y = 4.7, W = 3, H = 0.8}
}
)
end
function show_admin_edit()
-- screen for adding/removing entries
local list_entries = {}
for i,e in ipairs(mem.entries) do
table.insert(list_entries, string.format("%s|%s", e.id, e.name))
end
local list_str = table.concat(list_entries, ",") -- TODO sanitize?
digiline_send("admin",
{
{command = "clear"},
{command = "set",
width = 16,
height = 12,
real_coordinates = true,
locked = true, -- does not prevent someone from looking at it :(
},
{command = "addtextlist", name = "entries", listelements = list_str, transparent = false, selected_id = mem.admin_entries_idx, X = 1.2, Y = 1.8, W = 10.5, H = 7.9},
{command = "addfield", name = "new_entry", label = "New entry", default = "", X = 1.2, Y = 10.4, W = 8.5, H = 0.8},
{command = "addbutton", name = "add_entry", label = "Add entry", X = 9.9, Y = 10.4, W = 1.7, H = 0.8},
{command = "addbutton", name = "delete_entry", label = "Delete selected", X = 12.2, Y = 8, W = 3, H = 0.8},
{command = "add", element = "field_close_on_enter", name = "new_entry", close_on_enter = false},
{command = "addbutton", name = "edit_back", label = "Back", X = 12.2, Y = 10.4, W = 1.7, H = 0.8},
}
)
end
function show_admin_results()
-- count and show results
-- count the votes that we actually cast
-- local total_scores = {}
-- for _,player_votes in pairs(mem.votes) do
-- for entry, cats in pairs(player_votes) do
-- local entry_scores = total_scores[entry] or {}
-- total_scores[entry] = entry_scores
-- for cat, value in pairs(cats) do
-- local cs = (entry_scores[cat] or 0) + value
-- entry_scores[cat] = cs
-- end
-- end
-- end
-- for all entries, check all known votes and add them up, using defaults if none
-- TODO can be made more efficient probably
local total_scores = {}
for i, e in ipairs(mem.entries) do
local id = e.id
local entry_scores = {}
total_scores[i] = entry_scores
entry_scores.id = id
for _,player_votes in pairs(mem.votes) do
local entry_votes = player_votes[id] or {} -- nil if didn't cast any votes for this entry
for i=1,4 do
local vote = (entry_votes[i] or 1) -- 1 is the default vote
entry_scores[i] = (entry_scores[i] or 0) + vote
end
end
end
-- sort by different fields
local sorted_by_str = "none"
if mem.sort_type == "sum" then
sorted_by_str = "Sorted by sum:"
table.sort(total_scores, function(a,b) return (a[1] + a[2] + a[3] + a[4]) > (b[1] + b[2] + b[3] + b[4]) end)
elseif type(mem.sort_type) == "number" then
sorted_by_str = string.format("Sorted by '%s':", categories[mem.sort_type])
local idx = mem.sort_type
table.sort(total_scores, function(a,b) return a[idx] > b[idx] end)
end
-- actually generate strings for the results table
local entry_list = {}
for _, s in ipairs(total_scores) do
local name = mem.entries_by_id[s.id].name
local sum = s[1] + s[2] + s[3] + s[4]
table.insert(entry_list, string.format("%03d||%03d|%03d|%03d|%03d -- %s", sum, s[1], s[2], s[3], s[4], name))
end
entry_list = table.concat(entry_list, ",")
digiline_send("admin",
{
{command = "clear"},
{command = "set",
width = 16,
height = 12,
real_coordinates = true,
locked = true, -- does not prevent someone from looking at it :(
},
{command = "addlabel", label = sorted_by_str, X = 0.9, Y = 0.6},
{command = "addbutton", name = "results_update", label = "Update", X = 8.7, Y = 0.3, W = 3, H = 1},
{command = "addbutton", name = "results_back", label = "Back", X = 12.7, Y = 0.3, W = 3, H = 1},
{command = "addtextlist", name = "", listelements = entry_list, transparent = false, selected_id = 1, X = 0.3, Y = 1.5, W = 12.3, H = 9.2},
{command = "addlabel", label = "Sort by:", X = 13.5, Y = 2.6},
{command = "addbutton", name = "sort_sum", label = "Sum", X = 12.7, Y = 3.1, W = 3, H = 1},
{command = "addbutton", name = "sort_1", label = categories[1], X = 12.7, Y = 4.2, W = 3, H = 1},
{command = "addbutton", name = "sort_2", label = categories[2], X = 12.7, Y = 5.3, W = 3, H = 1},
{command = "addbutton", name = "sort_3", label = categories[3], X = 12.7, Y = 6.4, W = 3, H = 1},
{command = "addbutton", name = "sort_4", label = categories[4], X = 12.7, Y = 7.5, W = 3, H = 1},
}
)
end
function show_vote_ballot(username)
local c =
{
{command = "clear"},
{command = "set",
width = 18,
height = 14,
real_coordinates = true,
},
{command = "addlabel", label = "Creativity", X = 10.5, Y = 0.8},
{command = "addlabel", label = "Aesthetic", X = 12.2, Y = 0.6},
{command = "addlabel", label = "Appeal", X = 12.4, Y = 1},
{command = "addlabel", label = "Detail &", X = 14.1, Y = 0.6},
{command = "addlabel", label = "Execution", X = 14, Y = 1.1},
{command = "addlabel", label = "Use of", X = 16.1, Y = 0.6},
{command = "addlabel", label = "space", X = 16.2, Y = 1},
}
local shift = 1.3 -- vertical spacing between entries
local votes = mem.votes[username] or {}
local page = mem.ballot_page
for i=1,ENTRIES_PER_PAGE do
local e = mem.entries[i + ENTRIES_PER_PAGE*page]
if not e then
break -- not enough entries to fill the page
end
local id = e.id
local e_v = votes[id] or {} -- previous votes of this player for this entry (if any)
table.insert(c, {command = "addimage", texture_name = "halo.png^[colorize:#222233", X = 0.5, Y = 0.4 + shift*i, W = 16.8, H = 1})
table.insert(c, {command = "addlabel", label = e.name, X = 0.8, Y = 0.9 + shift*i})
table.insert(c, {command = "adddropdown", name = string.format("v_%d_1", id), index_event = true, selected_id = e_v[1] or 1, choices = {[1] = "1", [2] = "2", [3] = "3"}, X = 10.8, Y = 0.5 + shift*i, W = 0.8, H = 0.8})
table.insert(c, {command = "adddropdown", name = string.format("v_%d_2", id), index_event = true, selected_id = e_v[2] or 1, choices = {[1] = "1", [2] = "2", [3] = "3"}, X = 12.6, Y = 0.5 + shift*i, W = 0.8, H = 0.8})
table.insert(c, {command = "adddropdown", name = string.format("v_%d_3", id), index_event = true, selected_id = e_v[3] or 1, choices = {[1] = "1", [2] = "2", [3] = "3"}, X = 14.4, Y = 0.5 + shift*i, W = 0.8, H = 0.8})
table.insert(c, {command = "adddropdown", name = string.format("v_%d_4", id), index_event = true, selected_id = e_v[4] or 1, choices = {[1] = "1", [2] = "2", [3] = "3"}, X = 16.2, Y = 0.5 + shift*i, W = 0.8, H = 0.8})
end
-- snow number of pages and page flipping buttons
local total_pages = math.ceil(#mem.entries / ENTRIES_PER_PAGE)
table.insert(c, {command = "addlabel", label = string.format("Page %s/%s", mem.ballot_page+1, total_pages), X = 0.8, Y = 12.5})
if mem.ballot_page > 0 then
table.insert(c, {command = "addbutton", name = "ballot_page_prev", label = "Prev page", X = 2.5, Y = 12.5, W = 3, H = 1})
end
if (mem.ballot_page + 1) < total_pages then
table.insert(c, {command = "addbutton", name = "ballot_page_next", label = "Next page", X = 5.5, Y = 12.5, W = 3, H = 1})
end
digiline_send("vote", c)
end
if event.type == "program" then
-- initialize defaults or use stored values
-- reset screens
show_vote_welcome()
show_admin_welcome()
mem.id_count = mem.id_count or 1 -- unique entry ID generator
mem.username = nil -- current voter's name
mem.entries_by_id = mem.entries_by_id or {} -- mapping from id to entry
mem.entries = mem.entries or {} -- ordered list of entries
mem.admin_entries_idx = nil -- stores list selection for "edit" screen
mem.votes = mem.votes or {} -- user votes
mem.state_admin = "welcome" -- which admin window is currently displayed
mem.sort_type = "sum" -- what sort to use ("sum" or 1,2,3,4)
mem.ballot_page = 0 -- current ballot page
elseif event.type == "digiline" then
if event.channel == "vote" then
if event.msg.start then
-- show user's ballot
mem.username = event.msg.clicker
mem.ballot_page = 0
show_vote_ballot(mem.username)
elseif event.msg.quit then
-- clear screen after user closed the window
show_vote_welcome()
elseif event.msg.ballot_page_next then
-- flip page
mem.ballot_page = mem.ballot_page + 1
show_vote_ballot(mem.username)
elseif event.msg.ballot_page_prev then
-- flip page
mem.ballot_page = mem.ballot_page - 1
show_vote_ballot(mem.username)
else
-- process vote selection
if mem.username ~= event.msg.clicker then
-- clicker is not the currently voting user! abort! :P
-- TODO show the offender's name on this screen
local name = mem.username
mem.username = nil
show_vote_error(name, event.msg.clicker)
end
local num_entries = #mem.entries
local votes = mem.votes[event.msg.clicker] or {}
mem.votes[event.msg.clicker] = votes
for k,v in pairs(event.msg) do
-- find and parse "v_<entry_idx>_<crit_idx>" key
if k:sub(1,2) == "v_" then
local vote = k:sub(3)
local off = vote:find("_", 1, true)
local e_id = tonumber(vote:sub(1,off-1)) or 0 -- entry ID
local c_id = tonumber(vote:sub(off+1)) or 0 -- category ID
local entry = votes[e_id] or {}
-- store vote
votes[e_id] = entry
entry[c_id] = tonumber(v) or 0 -- vote value for this category
end
end
--digiline_send("debug", mem.votes) -- TODO remove this
end
elseif event.channel == "admin" then
if mem.state_admin == "welcome" then
if event.msg.edit_entries then
-- show "edit" window
mem.state_admin = "edit"
show_admin_edit()
elseif event.msg.view_results then
-- show "results" window
mem.state_admin = "results"
show_admin_results()
end
elseif mem.state_admin == "edit" then
if event.msg.entries then
-- changed selection, store index
local e = event.msg.entries
if e:sub(1,4) == "CHG:" then
mem.admin_entries_idx = tonumber(e:sub(5))
end
show_admin_edit()
elseif event.msg.add_entry or (event.msg.key_enter_field == "new_entry") then
-- adding new entry
local new = event.msg.new_entry
if not new:find(",", 1, true) then -- can't have "," in names, do nothing if found
local new_id = mem.id_count
mem.id_count = mem.id_count + 1 -- update unique ID generator
if new ~= "" then -- add when not empty
local entry = {id = new_id, name = new}
table.insert(mem.entries, entry) -- store in ordered list
mem.entries_by_id[new_id] = entry -- store in ID -> entry map
end
show_admin_edit()
end
elseif event.msg.delete_entry then
-- removing selected entry
-- TODO either re-index existing votes, or even wipe all of them?!
local entry = mem.entries[mem.admin_entries_idx]
table.remove(mem.entries, mem.admin_entries_idx)
mem.entries_by_id[entry.id] = entry
-- try to keep stored index up-to-date with what user is seeing
mem.admin_entries_idx = math.min(mem.admin_entries_idx, #mem.entries)
show_admin_edit()
elseif event.msg.edit_back then
mem.state_admin = "welcome"
show_admin_welcome()
end
elseif mem.state_admin == "results" then
if event.msg.results_back then
mem.state_admin = "welcome"
show_admin_welcome()
elseif event.msg.results_update then
-- just re-generate window (will recalculate results)
show_admin_results()
elseif event.msg.sort_sum then
-- change sorting type and show updated window
mem.sort_type = "sum"
show_admin_results()
elseif event.msg.sort_1 then
mem.sort_type = 1
show_admin_results()
elseif event.msg.sort_2 then
mem.sort_type = 2
show_admin_results()
elseif event.msg.sort_3 then
mem.sort_type = 3
show_admin_results()
elseif event.msg.sort_4 then
mem.sort_type = 4
show_admin_results()
end
end
end
end