online_voting / app.py
tzurshubi's picture
Update app.py
c86cb9d verified
import dash
from dash import dcc, html, Input, Output, State, callback_context, ALL
import dash_bootstrap_components as dbc
import json
# --- CONFIGURATION & SCENARIOS ---
PREFERENCES = {
"Green": {"rank": 1, "reward": 50, "color": "success", "desc": "1st Priority"},
"Yellow": {"rank": 2, "reward": 25, "color": "warning", "desc": "2nd Priority"}, # Changed Grey to Yellow
"Red": {"rank": 3, "reward": 0, "color": "danger", "desc": "3rd Priority"},
}
SCENARIOS = [
{
"id": 1,
"desc": "Scenario 1: Your favorite is far behind. Your 2nd choice is in a close race.",
"votes": {"Red": 48, "Yellow": 46, "Green": 6},
"type": "compromise"
},
{
"id": 2,
"desc": "Scenario 2: Your favorite is winning comfortably.",
"votes": {"Green": 50, "Red": 30, "Yellow": 20},
"type": "winner"
},
{
"id": 3,
"desc": "Scenario 3: Your favorite is in a close race for the win.",
"votes": {"Red": 45, "Green": 42, "Yellow": 13},
"type": "runner_up"
},
{
"id": 4,
"desc": "Scenario 4: Your favorite is losing. Your 2nd choice is winning easily.",
"votes": {"Yellow": 55, "Red": 35, "Green": 10},
"type": "herding"
},
{
"id": 5,
"desc": "Scenario 5: It's a dead tie between your 2nd and 3rd choice. Your vote decides.",
"votes": {"Red": 49, "Yellow": 49, "Green": 2},
"type": "pivotal"
}
]
# --- APP INITIALIZATION ---
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.LUMEN], suppress_callback_exceptions=True)
server = app.server
# --- LAYOUT COMPONENTS ---
def get_header():
return dbc.NavbarSimple(
brand="Voting Behavior Experiment",
brand_href="#",
color="primary",
dark=True,
)
def get_intro_card():
return dbc.Card([
dbc.CardHeader("Welcome, Voter!"),
dbc.CardBody([
html.H4("Your Profile & Incentives", className="card-title"),
html.P(
"You are participating in a voting experiment with 3 candidates: Green, Yellow & Red. "
"You have specific preferences. If your candidate wins, you get the coins listed below. "
"If they lose, you get nothing for that candidate."
),
html.Hr(),
dbc.Row([
dbc.Col(dbc.Alert("1st: Green (50 ₪)", color="success"), width=4),
dbc.Col(dbc.Alert("2nd: Yellow (25 ₪)", color="warning"), width=4), # Changed to Yellow/Warning
dbc.Col(dbc.Alert("3rd: Red (0 ₪)", color="danger"), width=4),
]),
html.Br(),
dbc.Button("Start Experiment", id={"type": "nav-btn", "role": "start"}, color="primary", size="lg", class_name="w-100")
])
], class_name="mb-3 shadow")
def get_poll_card(scenario_idx, scenario_data):
total_votes = sum(scenario_data["votes"].values())
bars = []
candidates = ["Green", "Yellow", "Red"] # Updated order list
for cand in candidates:
votes = scenario_data["votes"].get(cand, 0)
percent = (votes / total_votes) * 100
color = PREFERENCES[cand]["color"]
# Determine text color for visibility (warning/yellow can be hard to read on white)
text_class = "mb-1 fw-bold"
bars.append(html.Div([
html.Div(f"{cand}: {votes} voters", className=text_class),
dbc.Progress(value=percent, color=color, label=f"{percent:.1f}%", className="mb-3", style={"height": "25px"})
]))
return dbc.Card([
dbc.CardHeader(f"Round {scenario_idx + 1} of {len(SCENARIOS)}"),
dbc.CardBody([
html.H5(scenario_data["desc"], className="mb-4 text-muted"),
html.Div(bars),
html.Hr(),
html.H4("Cast your vote:", className="text-center mb-3"),
dbc.Row([
dbc.Col(dbc.Button("Vote Green", id={"type": "vote-btn", "candidate": "Green"}, color="success", class_name="w-100 p-3")),
dbc.Col(dbc.Button("Vote Yellow", id={"type": "vote-btn", "candidate": "Yellow"}, color="warning", class_name="w-100 p-3")),
dbc.Col(dbc.Button("Vote Red", id={"type": "vote-btn", "candidate": "Red"}, color="danger", class_name="w-100 p-3")),
])
])
], class_name="shadow")
def get_results_card(total_reward, classification, vote_history):
class_color = "info"
if classification == "Truthful":
class_desc = "You consistently voted for your favorite candidate, regardless of their chances of winning."
class_color = "success"
elif classification == "Strategic-Pragmatic":
class_desc = "You adjusted your vote to maximize your expected reward (e.g., compromising for your 2nd choice when your favorite couldn't win)."
class_color = "warning"
else:
class_desc = "Your voting pattern included choices that guaranteed 0 reward (voting for your least favorite), which appears random or irrational."
class_color = "danger"
return dbc.Card([
dbc.CardHeader("Experiment Complete"),
dbc.CardBody([
html.H2(f"Total Winnings: {total_reward} ₪", className="text-center text-primary mb-4"),
dbc.Alert([
html.H4(f"Your Classification: {classification}"),
html.P(class_desc)
], color=class_color),
html.Hr(),
dbc.Button("Restart", id={"type": "nav-btn", "role": "restart"}, color="dark", outline=True, class_name="mt-2")
])
], class_name="shadow")
def determine_group(history):
votes = [h["vote"] for h in history]
if "Red" in votes:
return "Random"
if votes.count("Green") == 5:
return "Truthful"
return "Strategic-Pragmatic"
# --- MAIN LAYOUT ---
app.layout = dbc.Container([
get_header(),
html.Br(),
html.Div(id="page-content", children=get_intro_card()),
dcc.Store(id="game-store", data={"step": -1, "history": [], "total_reward": 0})
], fluid=True, style={"max-width": "800px"})
# --- CALLBACKS ---
@app.callback(
[Output("page-content", "children"),
Output("game-store", "data")],
[Input({"type": "nav-btn", "role": ALL}, "n_clicks"),
Input({"type": "vote-btn", "candidate": ALL}, "n_clicks")],
[State("game-store", "data")]
)
def update_game(nav_clicks, vote_clicks, data):
ctx = callback_context
if not ctx.triggered:
return dash.no_update, dash.no_update
triggered_str = ctx.triggered[0]["prop_id"].split(".")[0]
triggered_id = json.loads(triggered_str)
button_type = triggered_id.get("type")
if button_type == "nav-btn":
return get_poll_card(0, SCENARIOS[0]), {"step": 0, "history": [], "total_reward": 0}
if button_type == "vote-btn":
user_vote = triggered_id.get("candidate")
current_step = data["step"]
if current_step < 0 or current_step >= len(SCENARIOS):
return get_intro_card(), {"step": -1, "history": [], "total_reward": 0}
current_scenario = SCENARIOS[current_step]
round_votes = current_scenario["votes"].copy()
round_votes[user_vote] += 1
max_votes = max(round_votes.values())
winners = [cand for cand, v in round_votes.items() if v == max_votes]
winner = winners[0]
reward = PREFERENCES[winner]["reward"]
new_history = data["history"] + [{
"scenario": current_scenario["id"],
"vote": user_vote,
"winner": winner,
"reward": reward
}]
new_reward = data["total_reward"] + reward
new_step = current_step + 1
if new_step >= len(SCENARIOS):
classification = determine_group(new_history)
results_card = get_results_card(new_reward, classification, new_history)
return results_card, {"step": -1, "history": [], "total_reward": 0}
return get_poll_card(new_step, SCENARIOS[new_step]), {"step": new_step, "history": new_history, "total_reward": new_reward}
return dash.no_update, dash.no_update
if __name__ == "__main__":
import os
port = int(os.environ.get("PORT", "7860")) # HF uses 7860
app.run(host="0.0.0.0", port=port, debug=False)