Skip to content

Commit b239c2c

Browse files
authored
Add project: Election Campaign Simulator (#1159)
Submitted by @Girish0902
1 parent 4b8740b commit b239c2c

6 files changed

Lines changed: 311 additions & 0 deletions

File tree

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Election Campaign Simulator
2+
3+
Manage a 30-day political campaign with a $1,000,000 budget. Take actions, deliver speeches, and track voter sentiment across demographic groups to win the election.
4+
5+
## Features
6+
- **Budget Management** — Spend on ads, rallies, canvassing, and fundraising
7+
- **Speech Writer** — Write and deliver speeches on key topics
8+
- **Voter Sentiment** — Track approval across youth, suburban, rural, urban, and senior demographics
9+
- **Random Events** — Endorsements, scandals, and surprises affect your campaign
10+
11+
Open `index.html` in any browser to play.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<meta name="description" content="Election Campaign Simulator — Manage your campaign budget, deliver speeches, and track voter sentiment.">
7+
<title>Election Campaign Simulator</title>
8+
<link rel="stylesheet" href="style.css">
9+
</head>
10+
<body>
11+
<div class="app">
12+
<header class="topbar">
13+
<h1><span aria-hidden="true">&#x1F3DB;</span> Election Campaign Simulator</h1>
14+
<p class="subtitle">Budget &middot; Speeches &middot; Voter Sentiment</p>
15+
</header>
16+
17+
<div class="dashboard">
18+
<div class="stats-row">
19+
<div class="stat-card"><span class="stat-label">Budget</span><span class="stat-value gold" id="budgetVal">$1,000,000</span></div>
20+
<div class="stat-card"><span class="stat-label">Approval</span><span class="stat-value" id="approvalVal">50%</span></div>
21+
<div class="stat-card"><span class="stat-label">Day</span><span class="stat-value" id="dayVal">1 / 30</span></div>
22+
<div class="stat-card"><span class="stat-label">Votes</span><span class="stat-value" id="votesVal">0</span></div>
23+
</div>
24+
25+
<div class="main-grid">
26+
<div class="panel">
27+
<h2>Campaign Actions</h2>
28+
<div class="actions" id="actionsList"></div>
29+
</div>
30+
31+
<div class="panel">
32+
<h2>Speech Writer</h2>
33+
<div class="speech-area">
34+
<select id="speechTopic" aria-label="Speech topic">
35+
<option value="economy">Economy & Jobs</option>
36+
<option value="healthcare">Healthcare</option>
37+
<option value="education">Education</option>
38+
<option value="security">National Security</option>
39+
<option value="environment">Environment</option>
40+
</select>
41+
<textarea id="speechText" rows="4" placeholder="Write your campaign speech..." aria-label="Speech content"></textarea>
42+
<button class="btn" id="deliverSpeech">Deliver Speech</button>
43+
</div>
44+
<div class="speech-log" id="speechLog"><p class="empty-state">No speeches delivered yet.</p></div>
45+
</div>
46+
47+
<div class="panel">
48+
<h2>Voter Sentiment</h2>
49+
<div class="sentiment-gauge" id="sentimentGauge">
50+
<div class="gauge-bar"><div class="gauge-fill" id="sentimentFill" style="width:50%"></div></div>
51+
<div class="gauge-labels"><span>Hostile</span><span id="sentimentLabel">Neutral</span><span>Adoring</span></div>
52+
</div>
53+
<div class="demographics" id="demographics"></div>
54+
</div>
55+
</div>
56+
57+
<div class="event-log" id="eventLog">
58+
<h3>Campaign Trail Log</h3>
59+
<div class="log-entries" id="logEntries"><p class="empty-state">Your campaign begins today. Make every decision count.</p></div>
60+
</div>
61+
</div>
62+
</div>
63+
<script src="script.js"></script>
64+
</body>
65+
</html>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"title": "Election Campaign Simulator",
3+
"description": "Manage your campaign budget, deliver speeches, and track voter sentiment to win the election.",
4+
"entry": "index.html",
5+
"tags": ["html", "css", "javascript", "simulation"],
6+
"author": {
7+
"name": "Girish Madarkar",
8+
"github": "Girish0902"
9+
}
10+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
const state = {
2+
budget: 1000000,
3+
approval: 50,
4+
day: 1,
5+
maxDays: 30,
6+
votes: 0,
7+
log: [],
8+
sentiment: {
9+
overall: 50,
10+
youth: 50, suburban: 50, rural: 50, urban: 50,
11+
seniors: 50, undecided: 50,
12+
},
13+
};
14+
15+
const ACTIONS = [
16+
{ name: "TV Ad Campaign", cost: 80000, approval: 3, votes: 5000, desc: "Broadcast ads across key networks" },
17+
{ name: "Rally in City", cost: 30000, approval: 2, votes: 3000, desc: "Hold a major rally in an urban center" },
18+
{ name: "Door-to-Door Canvassing", cost: 15000, approval: 1, votes: 2000, desc: "Volunteers engage voters personally" },
19+
{ name: "Social Media Blitz", cost: 10000, approval: 1, votes: 1500, desc: "Targeted digital campaign" },
20+
{ name: "Town Hall Meeting", cost: 20000, approval: 4, votes: 1000, desc: "Answer questions from undecided voters" },
21+
{ name: "Radio Interview", cost: 5000, approval: 1, votes: 800, desc: "Speak on popular talk radio" },
22+
{ name: "Fundraising Dinner", cost: -40000, approval: 0, votes: 0, desc: "Raise funds from donors (gain budget)" },
23+
{ name: "Opposition Research", cost: 25000, approval: 0, votes: 6000, desc: "Uncover weakness in opponent's record" },
24+
{ name: "Get Out The Vote", cost: 40000, approval: 0, votes: 8000, desc: "Mobilize supporters on election day" },
25+
];
26+
27+
const SPEECHES = {
28+
economy: { impact: { youth: 2, suburban: 3, rural: 2, seniors: 3, urban: 2 }, approval: 2 },
29+
healthcare: { impact: { youth: 3, suburban: 3, rural: 2, seniors: 4, urban: 3 }, approval: 3 },
30+
education: { impact: { youth: 4, suburban: 3, rural: 1, seniors: 1, urban: 3 }, approval: 2 },
31+
security: { impact: { youth: 1, suburban: 2, rural: 3, seniors: 4, urban: 1 }, approval: 2 },
32+
environment: { impact: { youth: 4, suburban: 3, rural: 1, seniors: 1, urban: 2 }, approval: 1 },
33+
};
34+
35+
const NAMES = ["Urban voters","Suburban families","Rural communities","Young voters","Senior citizens","Undecided voters"];
36+
37+
const $=id=>document.getElementById(id);
38+
39+
function updateUI(){
40+
$('budgetVal').textContent='$'+state.budget.toLocaleString();
41+
$('approvalVal').textContent=state.approval+'%';
42+
$('approvalVal').style.color=state.approval>=60?'var(--green)':state.approval>=40?'var(--gold)':'var(--red)';
43+
$('dayVal').textContent=state.day+' / '+state.maxDays;
44+
$('votesVal').textContent=state.votes.toLocaleString();
45+
const fill=$('sentimentFill');
46+
fill.style.width=state.sentiment.overall+'%';
47+
const labels=['Hostile','Unfavorable','Neutral','Favorable','Adoring'];
48+
const idx=state.sentiment.overall<20?0:state.sentiment.overall<40?1:state.sentiment.overall<60?2:state.sentiment.overall<80?3:4;
49+
$('sentimentLabel').textContent=labels[idx];
50+
51+
const demo=$('demographics');
52+
const groups=['youth','suburban','rural','urban','seniors','undecided'];
53+
demo.innerHTML=groups.map(g=>{
54+
const val=state.sentiment[g];
55+
const color=val>=60?'var(--green)':val>=40?'var(--gold)':'var(--red)';
56+
const label=g==='youth'?'Youth (18-29)':g==='suburban'?'Suburban':g==='rural'?'Rural':g==='urban'?'Urban':g==='seniors'?'Seniors (65+)':'Undecided';
57+
return `<div class="demo-row"><span class="label">${label}</span><span class="val" style="color:${color}">${val}%</span></div>`;
58+
}).join('');
59+
60+
const acts=$('actionsList');
61+
acts.innerHTML=ACTIONS.map((a,i)=>{
62+
const affordable=a.cost<=0||a.cost<=state.budget;
63+
const canAct=state.day<state.maxDays;
64+
return `<button class="action-btn" data-idx="${i}" ${(!affordable||!canAct)?'disabled':''}>
65+
${a.name} <span class="cost">${a.cost<0?'+$'+Math.abs(a.cost).toLocaleString():'$'+a.cost.toLocaleString()} · ${a.votes.toLocaleString()} votes · +${a.approval}% app.</span>
66+
</button>`;
67+
}).join('');
68+
acts.querySelectorAll('.action-btn').forEach(b=>b.addEventListener('click',()=>performAction(parseInt(b.dataset.idx))));
69+
}
70+
71+
function performAction(idx){
72+
const a=ACTIONS[idx];
73+
if(a.cost>0&&a.cost>state.budget) return;
74+
if(state.day>=state.maxDays) return;
75+
state.budget-=a.cost;
76+
state.approval=Math.min(100,Math.max(0,state.approval+a.approval+Math.floor(Math.random()*3-1)));
77+
state.votes+=Math.floor(a.votes*(0.8+Math.random()*0.4));
78+
state.sentiment.overall=Math.min(100,Math.max(0,state.sentiment.overall+Math.floor(Math.random()*4-1)));
79+
state.day++;
80+
addLog(`Day ${state.day-1}: ${a.name}`, a.desc);
81+
randomEvent();
82+
updateUI();
83+
if(state.day>=state.maxDays) endCampaign();
84+
}
85+
86+
function deliverSpeech(){
87+
const topic=$('speechTopic').value;
88+
const text=$('speechText').value.trim();
89+
if(!text){ addLog('Speech draft empty','Write something before delivering.'); return; }
90+
if(state.day>=state.maxDays) return;
91+
92+
const sp=SPEECHES[topic];
93+
Object.keys(sp.impact).forEach(k=>{
94+
state.sentiment[k]=Math.min(100,Math.max(0,state.sentiment[k]+sp.impact[k]));
95+
});
96+
const avg=Object.values(state.sentiment).slice(0,5).reduce((a,b)=>a+b,0)/5;
97+
state.sentiment.overall=Math.round(avg);
98+
state.approval=Math.min(100,Math.max(0,state.approval+sp.approval+Math.floor(Math.random()*2)));
99+
state.day++;
100+
addLog(`Speech on ${topic}`, text.length>80?text.slice(0,80)+'...':text);
101+
$('speechText').value='';
102+
103+
const log=$('speechLog');
104+
const entry=document.createElement('div');
105+
entry.className='speech-entry';
106+
entry.innerHTML=`<span class="topic">[${topic}]</span> "${text.length>60?text.slice(0,60)+'...':text}" <span class="impact">+${sp.approval}% approval</span>`;
107+
log.prepend(entry);
108+
if(log.children.length>5) log.removeChild(log.lastChild);
109+
110+
randomEvent();
111+
updateUI();
112+
if(state.day>=state.maxDays) endCampaign();
113+
}
114+
115+
function randomEvent(){
116+
const events=[
117+
{ msg:"Endorsement from a local newspaper!", approval:2, votes:2000 },
118+
{ msg:"Your opponent released a negative ad.", approval:-2, votes:-1000 },
119+
{ msg:"A gaffe at a public event hurts your campaign.", approval:-3, votes:-1500 },
120+
{ msg:"A celebrity endorses you!", approval:2, votes:3000 },
121+
{ msg:"Debate performance praised by pundits.", approval:3, votes:2500 },
122+
{ msg:"Bad weather reduces rally turnout.", approval:-1, votes:-500 },
123+
{ msg:"A policy proposal gains national attention!", approval:3, votes:4000 },
124+
{ msg:"Campaign staff error causes scheduling chaos.", approval:-1, votes:-500 },
125+
];
126+
if(Math.random()<0.35){
127+
const e=events[Math.floor(Math.random()*events.length)];
128+
state.approval=Math.min(100,Math.max(0,state.approval+e.approval));
129+
state.votes=Math.max(0,state.votes+e.votes);
130+
addLog('Event: '+e.msg,'');
131+
}
132+
}
133+
134+
function addLog(title,desc){
135+
const el=$('logEntries');
136+
const entry=document.createElement('div');
137+
entry.className='log-entry';
138+
entry.innerHTML=`<span class="day">D${state.day}</span> <span><strong>${title}</strong>${desc?' — '+desc:''}</span>`;
139+
el.prepend(entry);
140+
if(el.children.length>20) el.removeChild(el.lastChild);
141+
}
142+
143+
function endCampaign(){
144+
const win=state.votes>=50000;
145+
const msg=win?'Congratulations! You won the election with '+state.votes.toLocaleString()+' votes!' : 'You lost the election with '+state.votes.toLocaleString()+' votes. Try a different strategy next time.';
146+
addLog('Election Day!',msg);
147+
$('actionsList').innerHTML=`<div class="empty-state" style="padding:30px">Campaign Over — ${msg}<br><br><button class="btn" onclick="location.reload()">Start New Campaign</button></div>`;
148+
}
149+
150+
$('deliverSpeech').addEventListener('click',deliverSpeech);
151+
updateUI();
152+
addLog('Campaign Kickoff','Your campaign for office has begun. You have 30 days and $1,000,000.');
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
2+
:root{--bg:#0b1120;--surface:#111827;--card:rgba(30,41,59,0.6);--border:#334155;--gold:#d4a017;--gold-glow:rgba(212,160,23,0.12);--text:#f8fafc;--text2:#cbd5e1;--text3:#64748b;--accent:#93c5fd;--green:#22c55e;--red:#ef4444;--blue:#3b82f6;--radius:10px;--trans:all 0.3s cubic-bezier(0.16,1,0.3,1)}
3+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text2);min-height:100vh}
4+
.app{max-width:1200px;margin:0 auto;padding:20px}
5+
.topbar{text-align:center;padding:20px 0;border-bottom:1px solid var(--border);margin-bottom:20px}
6+
.topbar h1{font-size:22px;font-weight:700;color:var(--gold);letter-spacing:1px}
7+
.topbar .subtitle{font-size:13px;color:var(--text3);margin-top:4px}
8+
.stats-row{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:20px}
9+
.stat-card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:16px;text-align:center;backdrop-filter:blur(8px)}
10+
.stat-label{font-size:11px;color:var(--text3);text-transform:uppercase;letter-spacing:0.8px;display:block}
11+
.stat-value{font-family:ui-monospace,Consolas,monospace;font-size:22px;font-weight:700;color:var(--text);margin-top:4px;display:block}
12+
.stat-value.gold{color:var(--gold)}
13+
.main-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px}
14+
@media(max-width:800px){.main-grid{grid-template-columns:1fr}.stats-row{grid-template-columns:repeat(2,1fr)}}
15+
.panel{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:20px;backdrop-filter:blur(8px)}
16+
.panel h2{font-size:13px;font-weight:600;color:var(--accent);text-transform:uppercase;letter-spacing:1px;margin-bottom:14px;padding-bottom:8px;border-bottom:1px solid var(--border)}
17+
.actions{display:grid;grid-template-columns:1fr;gap:8px}
18+
.action-btn{padding:12px 16px;background:var(--surface);border:1px solid var(--border);border-radius:8px;color:var(--text2);font-size:13px;cursor:pointer;transition:var(--trans);text-align:left;font-family:inherit}
19+
.action-btn:hover{border-color:var(--gold);background:var(--gold-glow)}
20+
.action-btn .cost{font-family:ui-monospace,Consolas,monospace;font-size:11px;color:var(--gold);display:block;margin-top:3px}
21+
.action-btn:disabled{opacity:0.3;cursor:not-allowed}
22+
.speech-area select,.speech-area textarea{width:100%;padding:10px 12px;background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:8px;font-size:13px;margin-bottom:8px;font-family:inherit;outline:none}
23+
.speech-area select:focus,.speech-area textarea:focus{border-color:var(--gold)}
24+
.speech-area textarea{resize:vertical;min-height:80px}
25+
.btn{padding:10px 24px;background:var(--gold-glow);border:1px solid var(--gold);border-radius:8px;color:var(--gold);font-size:13px;font-weight:500;cursor:pointer;transition:var(--trans);font-family:inherit;width:100%}
26+
.btn:hover{box-shadow:0 0 20px var(--gold-glow)}
27+
.btn:disabled{opacity:0.3;cursor:not-allowed}
28+
.speech-log{margin-top:12px;max-height:200px;overflow-y:auto}
29+
.speech-entry{padding:10px;background:var(--surface);border-radius:6px;margin-bottom:6px;font-size:12px;line-height:1.5}
30+
.speech-entry .topic{color:var(--gold);font-weight:600}
31+
.speech-entry .impact{color:var(--green)}
32+
.sentiment-gauge{margin-bottom:16px}
33+
.gauge-bar{height:12px;background:var(--surface);border-radius:6px;overflow:hidden;border:1px solid var(--border)}
34+
.gauge-fill{height:100%;background:linear-gradient(90deg,var(--red),var(--gold),var(--green));border-radius:6px;transition:width 0.6s cubic-bezier(0.16,1,0.3,1)}
35+
.gauge-labels{display:flex;justify-content:space-between;font-size:10px;color:var(--text3);margin-top:4px}
36+
.gauge-labels span:nth-child(2){color:var(--gold);font-weight:600}
37+
.demographics{display:grid;gap:8px}
38+
.demo-row{display:flex;justify-content:space-between;align-items:center;font-size:12px;padding:4px 0}
39+
.demo-row .label{color:var(--text2)}
40+
.demo-row .val{font-family:ui-monospace,Consolas,monospace;font-size:11px}
41+
.event-log{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);padding:20px;backdrop-filter:blur(8px)}
42+
.event-log h3{font-size:12px;font-weight:600;color:var(--accent);text-transform:uppercase;letter-spacing:1px;margin-bottom:10px}
43+
.log-entries{max-height:180px;overflow-y:auto}
44+
.log-entry{padding:8px 0;border-bottom:1px solid rgba(51,65,85,0.4);font-size:12px;color:var(--text2);line-height:1.5;display:flex;gap:8px}
45+
.log-entry .day{font-family:ui-monospace,Consolas,monospace;color:var(--gold);white-space:nowrap}
46+
.empty-state{text-align:center;padding:20px;color:var(--text3);font-size:12px}
Lines changed: 27 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)