Spaces:
Sleeping
Sleeping
| 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 --- | |
| 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) |