|
| 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.'); |
0 commit comments