Social Change – Online Multiplayer + Bot
by GoldenCobra391371 lines62.9 KB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Social Change – Online Multiplayer + Bot</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Roboto, system-ui, sans-serif;
background: #0f111a;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 1.5rem;
color: #eaeef5;
}
.container {
max-width: 900px;
width: 100%;
background: #181c2b;
border-radius: 2.4rem;
padding: 2rem;
box-shadow: 0 30px 60px -16px rgba(0, 0, 0, 0.8), inset 0 1px 0 rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.05);
}
/* lobby */
.lobby {
display: flex;
flex-direction: column;
gap: 1.5rem;
align-items: center;
padding: 1.5rem 0;
}
.lobby h1 {
font-size: 2rem;
font-weight: 700;
color: #f0f3fa;
}
.lobby p {
color: #8e99c4;
max-width: 400px;
text-align: center;
}
.lobby .actions {
display: flex;
flex-wrap: wrap;
gap: 1rem;
justify-content: center;
}
.lobby .btn {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
color: #d0d8ee;
font-weight: 600;
font-size: 1rem;
padding: 0.8rem 2rem;
border-radius: 60px;
cursor: pointer;
transition: all 0.2s;
min-width: 160px;
}
.lobby .btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.12);
transform: scale(1.02);
}
.lobby .btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.lobby .btn.primary {
background: #facc15;
border-color: #facc15;
color: #121826;
box-shadow: 0 8px 24px -10px rgba(250, 204, 21, 0.25);
}
.lobby .btn.primary:hover:not(:disabled) {
background: #fde047;
}
.lobby .input-group {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
justify-content: center;
}
.lobby .input-group input {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 60px;
padding: 0.7rem 1.2rem;
font-size: 1rem;
color: #eaeef5;
outline: none;
width: 200px;
}
.lobby .input-group input:focus {
border-color: #facc15;
}
.lobby .input-group input::placeholder {
color: #5a6490;
}
.lobby .status {
color: #f87171;
font-size: 0.9rem;
min-height: 1.4rem;
}
/* game header */
.game-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.6rem;
margin-bottom: 1.2rem;
}
.game-header .title {
font-weight: 700;
font-size: 1.4rem;
color: #f0f3fa;
}
.game-header .room-code {
background: rgba(255, 255, 255, 0.06);
padding: 0.3rem 1rem;
border-radius: 40px;
font-size: 0.9rem;
color: #facc15;
border: 1px solid rgba(250, 204, 21, 0.15);
}
.game-header .room-code .copy-btn {
background: none;
border: none;
color: #8e99c4;
cursor: pointer;
margin-left: 0.5rem;
font-size: 0.8rem;
}
.game-header .room-code .copy-btn:hover {
color: #eaeef5;
}
.game-header .leave-btn {
background: rgba(248, 113, 113, 0.1);
border: 1px solid rgba(248, 113, 113, 0.15);
color: #fca5a5;
padding: 0.3rem 1.2rem;
border-radius: 40px;
cursor: pointer;
font-size: 0.8rem;
transition: 0.2s;
}
.game-header .leave-btn:hover {
background: rgba(248, 113, 113, 0.2);
}
.game-header .add-bot-btn {
background: rgba(74, 222, 128, 0.1);
border: 1px solid rgba(74, 222, 128, 0.15);
color: #86efac;
padding: 0.3rem 1.2rem;
border-radius: 40px;
cursor: pointer;
font-size: 0.8rem;
transition: 0.2s;
}
.game-header .add-bot-btn:hover {
background: rgba(74, 222, 128, 0.2);
}
.game-header .add-bot-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* system messages */
.system-msg {
background: rgba(255, 255, 255, 0.04);
border-radius: 0.8rem;
padding: 0.5rem 1rem;
margin-bottom: 1rem;
font-size: 0.85rem;
color: #8e99c4;
border-left: 3px solid rgba(250, 204, 21, 0.2);
min-height: 2.2rem;
word-break: break-word;
}
.system-msg.error {
border-left-color: #f87171;
color: #fca5a5;
}
.system-msg .highlight {
color: #facc15;
font-weight: 500;
}
/* player strip */
.player-strip {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
margin-bottom: 1.5rem;
padding: 0.2rem 0;
}
.player-pill {
background: rgba(255, 255, 255, 0.05);
padding: 0.4rem 1.2rem;
border-radius: 60px;
font-size: 0.85rem;
font-weight: 500;
color: #8e99c4;
border: 1px solid transparent;
transition: all 0.25s;
display: flex;
align-items: center;
gap: 0.4rem;
}
.player-pill.active {
background: rgba(250, 204, 21, 0.15);
border-color: #facc15;
color: #facc15;
}
.player-pill.answered {
background: rgba(74, 222, 128, 0.08);
border-color: rgba(74, 222, 128, 0.2);
color: #86efac;
}
.player-pill.bot {
border-color: rgba(74, 222, 128, 0.3);
color: #86efac;
}
/* card */
.card {
background: #f2f5fe;
background-image: radial-gradient(circle at 20% 25%, #ffffff, #e4e9f7);
border-radius: 2rem;
padding: 1.8rem 2rem;
margin-bottom: 1.2rem;
box-shadow: 0 16px 40px -12px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.card .q-number {
font-size: 0.75rem;
font-weight: 600;
color: #4b5580;
background: rgba(0, 0, 0, 0.04);
padding: 0.15rem 0.9rem;
border-radius: 40px;
display: inline-block;
margin-bottom: 0.8rem;
}
.card .q-text {
font-size: 1.35rem;
line-height: 1.5;
font-weight: 500;
color: #121826;
}
.card .q-text::before {
content: '\201C';
font-size: 2.4rem;
font-weight: 300;
color: #6b7ab0;
margin-right: 0.2rem;
opacity: 0.5;
}
.card .q-text::after {
content: '\201D';
font-size: 2.4rem;
font-weight: 300;
color: #6b7ab0;
margin-left: 0.2rem;
opacity: 0.5;
}
.card .q-meta {
margin-top: 1rem;
font-size: 0.75rem;
color: #4b5580;
border-top: 1px solid rgba(0, 0, 0, 0.05);
padding-top: 0.8rem;
display: flex;
justify-content: space-between;
}
/* answer section */
.answer-section {
background: rgba(255, 255, 255, 0.03);
border-radius: 1.4rem;
padding: 1.2rem 1.4rem;
margin-bottom: 1.2rem;
border: 1px solid rgba(255, 255, 255, 0.05);
}
.answer-section .label {
font-size: 0.8rem;
font-weight: 600;
color: #8e99c4;
margin-bottom: 0.6rem;
}
.answer-row {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
}
.answer-row input {
flex: 1;
min-width: 180px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 60px;
padding: 0.7rem 1.2rem;
font-size: 0.95rem;
color: #eaeef5;
outline: none;
}
.answer-row input:focus {
border-color: #facc15;
background: rgba(255, 255, 255, 0.08);
}
.answer-row input:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.answer-row .btn-submit {
background: #facc15;
border: none;
color: #121826;
font-weight: 700;
font-size: 0.9rem;
padding: 0.7rem 1.8rem;
border-radius: 60px;
cursor: pointer;
transition: 0.2s;
white-space: nowrap;
}
.answer-row .btn-submit:hover:not(:disabled) {
background: #fde047;
transform: scale(1.02);
}
.answer-row .btn-submit:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.answer-status {
font-size: 0.8rem;
color: #86efac;
margin-top: 0.5rem;
min-height: 1.4rem;
}
/* revealed answers */
.revealed-section {
margin-bottom: 1.2rem;
}
.revealed-section .section-title {
font-size: 0.8rem;
font-weight: 600;
color: #8e99c4;
margin-bottom: 0.6rem;
}
.revealed-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
max-height: 200px;
overflow-y: auto;
padding-right: 0.2rem;
}
.revealed-list::-webkit-scrollbar {
width: 4px;
}
.revealed-list::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.03);
border-radius: 10px;
}
.revealed-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.12);
border-radius: 10px;
}
.revealed-item {
background: rgba(255, 255, 255, 0.04);
border-radius: 0.8rem;
padding: 0.6rem 1rem;
border-left: 3px solid rgba(250, 204, 21, 0.3);
animation: fadeIn 0.3s ease;
}
.revealed-item .r-player {
font-weight: 600;
font-size: 0.8rem;
color: #facc15;
}
.revealed-item .r-answer {
font-size: 0.95rem;
color: #d0d8ee;
margin-top: 0.1rem;
word-break: break-word;
}
.revealed-item .r-question {
font-size: 0.7rem;
color: #6a74a0;
margin-top: 0.2rem;
opacity: 0.7;
font-style: italic;
}
.revealed-empty {
color: #4b5580;
font-size: 0.85rem;
padding: 0.4rem 0;
}
@keyframes fadeIn {
0% {
opacity: 0;
transform: translateY(8px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
/* controls */
.controls {
display: flex;
flex-wrap: wrap;
gap: 0.6rem;
justify-content: center;
margin-top: 0.2rem;
}
.controls .btn-action {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
color: #d0d8ee;
font-weight: 600;
font-size: 0.85rem;
padding: 0.6rem 1.6rem;
border-radius: 60px;
cursor: pointer;
transition: 0.2s;
flex: 1 1 auto;
min-width: 120px;
text-align: center;
}
.controls .btn-action:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.12);
transform: scale(1.02);
}
.controls .btn-action:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.controls .btn-action.primary {
background: #facc15;
border-color: #facc15;
color: #121826;
font-weight: 700;
box-shadow: 0 8px 24px -10px rgba(250, 204, 21, 0.25);
}
.controls .btn-action.primary:hover:not(:disabled) {
background: #fde047;
}
.controls .btn-action.danger {
border-color: rgba(248, 113, 113, 0.2);
color: #fca5a5;
}
.controls .btn-action.danger:hover:not(:disabled) {
background: rgba(248, 113, 113, 0.1);
}
/* responsive */
@media (max-width: 640px) {
.container {
padding: 1.2rem;
border-radius: 1.6rem;
}
.card {
padding: 1.2rem;
}
.card .q-text {
font-size: 1.05rem;
}
.card .q-text::before,
.card .q-text::after {
font-size: 1.8rem;
}
.lobby h1 {
font-size: 1.6rem;
}
.lobby .btn {
min-width: 120px;
font-size: 0.9rem;
padding: 0.6rem 1.2rem;
}
.game-header .add-bot-btn {
font-size: 0.7rem;
padding: 0.2rem 0.8rem;
}
}
</style>
</head>
<body>
<div class="container" id="app">
<!-- LOBBY -->
<div id="lobby" class="lobby">
<h1>Social Change</h1>
<p>Host a private room or join an existing game. All answers are shared live.</p>
<div class="actions">
<button class="btn primary" id="hostBtn">Host Game</button>
<div style="display:flex; flex-direction:column; gap:0.4rem; align-items:center;">
<div class="input-group">
<input type="text" id="joinCodeInput" placeholder="Room code" maxlength="6" style="text-transform:uppercase;">
<button class="btn" id="joinBtn">Join</button>
</div>
<div class="status" id="lobbyStatus"></div>
</div>
</div>
</div>
<!-- GAME UI -->
<div id="gameUI" style="display:none;">
<div class="game-header">
<span class="title">Social Change</span>
<div style="display:flex; gap:0.5rem; flex-wrap:wrap; align-items:center;">
<button class="add-bot-btn" id="addBotBtn" style="display:none;">Add Bot</button>
<span class="room-code" id="roomCodeDisplay">Code: <span id="codeSpan">------</span>
<button class="copy-btn" id="copyCodeBtn">Copy</button>
</span>
<button class="leave-btn" id="leaveGameBtn">Leave</button>
</div>
</div>
<div class="system-msg" id="systemMsg">Welcome! Draw a card to start.</div>
<div class="player-strip" id="playerStrip"></div>
<div class="card" id="cardDisplay">
<div class="q-number" id="qNumber">#00</div>
<div class="q-text" id="qText">Draw a card to begin</div>
<div class="q-meta">
<span id="qRemaining">deck: 40</span>
<span id="qTurnHint">waiting</span>
</div>
</div>
<div class="answer-section">
<div class="label" id="answerLabel">Your answer</div>
<div class="answer-row">
<input type="text" id="answerInput" placeholder="Type your answer..." maxlength="300" disabled>
<button class="btn-submit" id="submitAnswerBtn" disabled>Submit</button>
</div>
<div class="answer-status" id="answerStatus"></div>
</div>
<div class="revealed-section">
<div class="section-title">Revealed answers</div>
<div class="revealed-list" id="revealedList">
<div class="revealed-empty">No answers yet.</div>
</div>
</div>
<div class="controls">
<button class="btn-action primary" id="drawCardBtn">Draw card</button>
<button class="btn-action" id="nextTurnBtn">Next player</button>
<button class="btn-action danger" id="resetGameBtn">Reset Game</button>
</div>
</div>
</div>
<!-- PeerJS -->
<script src="https://unpkg.com/peerjs@1.5.2/dist/peerjs.min.js">
</script>
<script>
(function() {
"use strict";
const QUESTION_POOL = [
"When was the last time you changed your mind about something important, and what actually made you switch?",
"If your school suddenly had an extra hour of free time every day, what would you actually do with it—and would it make you happier or just lazier?",
"Be real: do you actually recycle, or do you just throw everything in the same bin and hope for the best?",
"What's a compliment you've received that stuck with you way longer than the person probably expected?",
"If you could trade lives with anyone in your grade for one week, who would it be and why?",
"Have you ever pretended to understand a social issue just so you wouldn't look clueless? What was it?",
"Think about the last time you saw someone being treated unfairly. Did you say something, walk away, or freeze? And how did you feel after?",
"If your phone died right now and you couldn't charge it for 24 hours, who in this room would you actually talk to first?",
"What's a word or phrase you overuse that you think makes you sound less smart than you actually are?",
"Is there a teacher or adult in your life who actually gets it? What makes them different from the rest?",
"If you had to give a five-minute speech to the whole school with no prep, what would you even talk about?",
"When you see a viral video of someone doing something kind for a stranger, does it inspire you or make you roll your eyes?",
"What's one thing your parents or guardians believe that you're pretty sure you'll never agree with?",
"If a friend told you a secret that made you think less of them, would you tell them honestly or just let the friendship fade?",
"Think about the last argument you witnessed online or in person. Did anyone actually listen, or was everyone just waiting for their turn to talk?",
"What's a 'small' thing someone did for you that actually changed your whole mood that day?",
"If your neighborhood had a giant billboard that could say one message to everyone, what would you put on it?",
"Have you ever been the only person in a room who disagreed with everyone else? What did you do?",
"What's something you secretly judge people for that you know you probably shouldn't?",
"If you woke up tomorrow and everyone in the world was exactly like you—same personality, same habits—would the world be better or worse?",
"What's one rule at this school (or in your house) that you secretly think is totally pointless—and what would you replace it with?",
"Be honest: when was the last time you actually read a news article instead of just the headline, and what made you click it?",
"If you had an extra $20 right now, are you buying yourself a treat, grabbing food for a friend, or dropping it in a donation box? No judgment, just facts.",
"Have you ever laughed at a joke that you knew was kind of messed up, just to fit in? What did you actually want to say?",
"If your Instagram/TikTok feed suddenly only showed posts from your local neighborhood (not celebrities), what do you think you'd see?",
"What's a 'tiny' annoyance in your daily life that you think actually points to a much bigger problem in the world?",
"If you could snap your fingers and make one group of people listen to another group of people for five minutes, who would they be?",
"Do you actually check where your clothes are made when you buy them, or do you just grab what looks cool? What would it take for you to care?",
"Think about your group chat. If one of you said something super offensive, would someone actually call it out, or would everyone just leave it on read?",
"What's a cause you care about but literally do nothing for? No shame—why do you think that is?",
"If you could change one thing about how your school handles conflict, what would it be and why?",
"What's a belief you held strongly a year ago that you've since softened on?",
"If your friends all decided to volunteer somewhere together, where would you want to go and what would you do?",
"What's the most recent thing you learned that actually changed how you see the world?",
"If you had to swap roles with a teacher for a day, what would you teach and how?",
"What's something you've never told anyone because you're afraid they'd judge you?",
"If you could make one rule that everyone in your school had to follow, what would it be?",
"What's a moment when you felt truly heard by someone? What made it different?",
"If you could send a one-sentence message to everyone in your generation, what would it say?",
"What's a question you wish more people would ask you?"
];
// ─── BOT ANSWERS ──────────────────────────────────────────────────────────
// Pre-defined answers for each question (parallel to QUESTION_POOL)
const BOT_ANSWERS_MAP = {
"When was the last time you changed your mind about something important, and what actually made you switch?": "I changed my mind about climate change after seeing the data on sea level rise. The science convinced me.",
"If your school suddenly had an extra hour of free time every day, what would you actually do with it—and would it make you happier or just lazier?": "I'd use it to read or learn a new skill. Definitely happier, not lazier.",
"Be real: do you actually recycle, or do you just throw everything in the same bin and hope for the best?": "I try to recycle, but honestly I sometimes just throw everything in one bin when I'm in a rush.",
"What's a compliment you've received that stuck with you way longer than the person probably expected?": "Someone told me I'm a good listener. That stuck with me because I value that trait.",
"If you could trade lives with anyone in your grade for one week, who would it be and why?": "I'd trade with the quiet kid who sits in the back. I want to see what they're thinking.",
"Have you ever pretended to understand a social issue just so you wouldn't look clueless? What was it?": "Yes, I pretended to understand cryptocurrency once. I still don't get it.",
"Think about the last time you saw someone being treated unfairly. Did you say something, walk away, or freeze? And how did you feel after?": "I froze last time. I felt guilty after and wished I'd spoken up.",
"If your phone died right now and you couldn't charge it for 24 hours, who in this room would you actually talk to first?": "I'd talk to the person closest to me, probably my best friend.",
"What's a word or phrase you overuse that you think makes you sound less smart than you actually are?": "I say 'literally' too much. It makes me sound like I'm exaggerating all the time.",
"Is there a teacher or adult in your life who actually gets it? What makes them different from the rest?": "My history teacher listens without judgment. That makes them different.",
"If you had to give a five-minute speech to the whole school with no prep, what would you even talk about?": "I'd talk about mental health and why it's okay to ask for help.",
"When you see a viral video of someone doing something kind for a stranger, does it inspire you or make you roll your eyes?": "It inspires me, but I also wonder if they're doing it for views.",
"What's one thing your parents or guardians believe that you're pretty sure you'll never agree with?": "They think success is all about money. I think it's about happiness.",
"If a friend told you a secret that made you think less of them, would you tell them honestly or just let the friendship fade?": "I'd tell them honestly, but gently. Friends deserve honesty.",
"Think about the last argument you witnessed online or in person. Did anyone actually listen, or was everyone just waiting for their turn to talk?": "Everyone was just waiting to talk. No one really listened.",
"What's a 'small' thing someone did for you that actually changed your whole mood that day?": "A stranger smiled at me on the bus. It made my whole day better.",
"If your neighborhood had a giant billboard that could say one message to everyone, what would you put on it?": "Be kind to each other.",
"Have you ever been the only person in a room who disagreed with everyone else? What did you do?": "Yes, I spoke up politely. It was uncomfortable but I stood my ground.",
"What's something you secretly judge people for that you know you probably shouldn't?": "I judge people for being late, even though I'm sometimes late too.",
"If you woke up tomorrow and everyone in the world was exactly like you—same personality, same habits—would the world be better or worse?": "Worse. The world needs different perspectives.",
"What's one rule at this school (or in your house) that you secretly think is totally pointless—and what would you replace it with?": "No hats in class. I'd replace it with 'wear what makes you comfortable'.",
"Be honest: when was the last time you actually read a news article instead of just the headline, and what made you click it?": "I read a climate article last week because the headline said 'record heat'.",
"If you had an extra $20 right now, are you buying yourself a treat, grabbing food for a friend, or dropping it in a donation box?": "I'd buy food for a friend. Sharing makes it better.",
"Have you ever laughed at a joke that you knew was kind of messed up, just to fit in? What did you actually want to say?": "Yes, I laughed at a racist joke once. I wanted to say it wasn't funny.",
"If your Instagram/TikTok feed suddenly only showed posts from your local neighborhood (not celebrities), what do you think you'd see?": "I'd see a lot of food pics and local events. It'd be more real.",
"What's a 'tiny' annoyance in your daily life that you think actually points to a much bigger problem in the world?": "People not picking up trash. It shows a lack of care for the environment.",
"If you could snap your fingers and make one group of people listen to another group of people for five minutes, who would they be?": "I'd make politicians listen to scientists.",
"Do you actually check where your clothes are made when you buy them, or do you just grab what looks cool? What would it take for you to care?": "I rarely check. I'd care more if I saw the workers' conditions.",
"Think about your group chat. If one of you said something super offensive, would someone actually call it out, or would everyone just leave it on read?": "Someone would call it out. We hold each other accountable.",
"What's a cause you care about but literally do nothing for? No shame—why do you think that is?": "I care about animal welfare but I don't volunteer. I think I'm just lazy.",
"If you could change one thing about how your school handles conflict, what would it be and why?": "I'd make mediation mandatory. It would help students resolve issues peacefully.",
"What's a belief you held strongly a year ago that you've since softened on?": "I used to think social media was all bad. Now I see it can be a tool for good.",
"If your friends all decided to volunteer somewhere together, where would you want to go and what would you do?": "I'd want to volunteer at an animal shelter and walk the dogs.",
"What's the most recent thing you learned that actually changed how you see the world?": "I learned about the circular economy. It changed how I view waste.",
"If you had to swap roles with a teacher for a day, what would you teach and how?": "I'd teach a class on emotional intelligence using role-play.",
"What's something you've never told anyone because you're afraid they'd judge you?": "I sometimes talk to myself. It helps me think.",
"If you could make one rule that everyone in your school had to follow, what would it be?": "No judgment. Everyone should feel safe to be themselves.",
"What's a moment when you felt truly heard by someone? What made it different?": "When my friend just listened without interrupting. That was different.",
"If you could send a one-sentence message to everyone in your generation, what would it say?": "You are enough, just as you are.",
"What's a question you wish more people would ask you?": "I wish people asked me about my dreams, not just my grades."
};
// ─── DOM refs ──────────────────────────────────────────────────────────
const lobby = document.getElementById('lobby');
const gameUI = document.getElementById('gameUI');
const hostBtn = document.getElementById('hostBtn');
const joinBtn = document.getElementById('joinBtn');
const joinCodeInput = document.getElementById('joinCodeInput');
const lobbyStatus = document.getElementById('lobbyStatus');
const codeSpan = document.getElementById('codeSpan');
const copyCodeBtn = document.getElementById('copyCodeBtn');
const leaveGameBtn = document.getElementById('leaveGameBtn');
const addBotBtn = document.getElementById('addBotBtn');
const playerStrip = document.getElementById('playerStrip');
const qNumber = document.getElementById('qNumber');
const qText = document.getElementById('qText');
const qRemaining = document.getElementById('qRemaining');
const qTurnHint = document.getElementById('qTurnHint');
const answerInput = document.getElementById('answerInput');
const submitAnswerBtn = document.getElementById('submitAnswerBtn');
const answerStatus = document.getElementById('answerStatus');
const answerLabel = document.getElementById('answerLabel');
const revealedList = document.getElementById('revealedList');
const drawCardBtn = document.getElementById('drawCardBtn');
const nextTurnBtn = document.getElementById('nextTurnBtn');
const resetGameBtn = document.getElementById('resetGameBtn');
const systemMsg = document.getElementById('systemMsg');
// ─── STATE ─────────────────────────────────────────────────────────────
let peer = null;
let isHost = false;
let roomCode = '';
let myPeerId = '';
let connections = {};
let gameState = null;
let myPlayerName = '';
let isConnecting = false;
let leaving = false;
let botAutoAnswerTimeout = null;
// ─── HELPERS ──────────────────────────────────────────────────────────
function generateRoomCode() {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let code = '';
for (let i = 0; i < 6; i++) code += chars.charAt(Math.floor(Math.random() * chars.length));
return code;
}
function shuffleArray(arr) {
for (let i = arr.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[arr[i], arr[j]] = [arr[j], arr[i]];
}
return arr;
}
function escapeHtml(text) {
const d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
function setSystemMsg(msg, isError = false) {
systemMsg.textContent = msg;
systemMsg.className = 'system-msg' + (isError ? ' error' : '');
}
function getBotAnswer(question) {
return BOT_ANSWERS_MAP[question] || "I'm not sure, but I think it's important to discuss.";
}
// ─── TURN MANAGEMENT ──────────────────────────────────────────────────
function advanceTurnInternal() {
if (!isHost || !gameState || gameState.players.length === 0) return;
gameState.currentPlayerIndex = (gameState.currentPlayerIndex + 1) % gameState.players.length;
broadcastState();
renderGame();
const cur = gameState.players[gameState.currentPlayerIndex];
setSystemMsg('Turn advanced to ' + (cur ? cur.name : 'waiting'));
}
// ─── RENDER ──────────────────────────────────────────────────────────
function renderGame() {
if (!gameState) return;
const state = gameState;
// Player strip
let stripHtml = '';
state.players.forEach((p, idx) => {
const isActive = (idx === state.currentPlayerIndex);
const hasAnswered = state.revealedAnswers.some(r => r.playerName === p.name && r.question === state
.currentQuestion);
let cls = 'player-pill';
if (isActive) cls += ' active';
if (hasAnswered) cls += ' answered';
if (p.isBot) cls += ' bot';
stripHtml +=
`<div class="${cls}">${escapeHtml(p.name)}${isActive ? ' ●' : ''}${hasAnswered ? ' ✓' : ''}${p.isBot ? ' 🤖' : ''}</div>`;
});
playerStrip.innerHTML = stripHtml || '<span style="color:#4b5580;font-size:0.85rem;">No players</span>';
// Card
if (state.currentQuestion) {
qNumber.textContent = '#' + String(state.currentQuestionIndex + 1).padStart(2, '0');
qText.textContent = state.currentQuestion;
qRemaining.textContent = 'deck: ' + state.deck.length + ' | used: ' + state.usedQuestions.length;
const cur = state.players[state.currentPlayerIndex];
qTurnHint.textContent = cur ? cur.name + '\'s turn' : 'waiting';
} else {
qNumber.textContent = '#00';
qText.textContent = 'Draw a card to begin';
qRemaining.textContent = 'deck: ' + state.deck.length;
qTurnHint.textContent = 'waiting';
}
// Revealed answers
if (state.revealedAnswers.length === 0) {
revealedList.innerHTML = '<div class="revealed-empty">No answers yet.</div>';
} else {
let html = '';
const reversed = [...state.revealedAnswers].reverse();
for (const r of reversed) {
html += `
<div class="revealed-item">
<div class="r-player">${escapeHtml(r.playerName)}</div>
<div class="r-answer">${escapeHtml(r.answer)}</div>
<div class="r-question">${escapeHtml(r.question)}</div>
</div>
`;
}
revealedList.innerHTML = html;
revealedList.scrollTop = 0;
}
// Answer section
const currentPlayer = state.players[state.currentPlayerIndex];
const isMyTurn = currentPlayer && currentPlayer.name === myPlayerName;
const alreadyAnswered = state.revealedAnswers.some(r => r.playerName === myPlayerName && r.question === state
.currentQuestion);
const allAnswered = state.players.length > 0 && state.players.every(p =>
state.revealedAnswers.some(r => r.playerName === p.name && r.question === state.currentQuestion)
);
if (!state.currentQuestion) {
answerInput.disabled = true;
submitAnswerBtn.disabled = true;
answerLabel.textContent = 'No question';
answerStatus.textContent = '';
} else if (alreadyAnswered) {
answerInput.disabled = true;
submitAnswerBtn.disabled = true;
answerLabel.textContent = myPlayerName + ' (answered)';
answerStatus.textContent = 'You have already answered this question.';
} else if (allAnswered) {
answerInput.disabled = true;
submitAnswerBtn.disabled = true;
answerLabel.textContent = 'All answered';
answerStatus.textContent = 'Everyone has answered. Draw a new card or go to next player.';
} else if (isMyTurn) {
answerInput.disabled = false;
submitAnswerBtn.disabled = false;
answerLabel.textContent = myPlayerName + '\'s answer';
answerStatus.textContent = '';
answerInput.focus();
} else {
answerInput.disabled = true;
submitAnswerBtn.disabled = true;
const turnPlayer = state.players[state.currentPlayerIndex];
answerLabel.textContent = (turnPlayer ? turnPlayer.name : 'Someone') + '\'s turn';
answerStatus.textContent = 'Wait for your turn.';
}
codeSpan.textContent = roomCode;
// Show/hide add bot button (only for host and if less than 4 players)
if (isHost) {
addBotBtn.style.display = 'inline-block';
addBotBtn.disabled = (state.players.length >= 4);
} else {
addBotBtn.style.display = 'none';
}
// Auto-answer bot if it's bot's turn and question exists and not all answered
if (isHost && state.currentQuestion && !allAnswered) {
const cur = state.players[state.currentPlayerIndex];
if (cur && cur.isBot) {
const botAlreadyAnswered = state.revealedAnswers.some(r => r.playerName === cur.name && r.question ===
state.currentQuestion);
if (!botAlreadyAnswered) {
// Schedule bot answer after 1.5 seconds
if (botAutoAnswerTimeout) clearTimeout(botAutoAnswerTimeout);
botAutoAnswerTimeout = setTimeout(() => {
submitBotAnswer(cur.name);
}, 1500);
}
}
}
}
// ─── BOT LOGIC ──────────────────────────────────────────────────────────
function submitBotAnswer(botName) {
if (!isHost || !gameState) return;
const botPlayer = gameState.players.find(p => p.name === botName && p.isBot);
if (!botPlayer) return;
const question = gameState.currentQuestion;
if (!question) return;
// Check if already answered
if (gameState.revealedAnswers.some(r => r.playerName === botName && r.question === question)) return;
const answer = getBotAnswer(question);
gameState.revealedAnswers.push({
playerName: botName,
question: question,
answer: answer
});
// Advance turn after bot answers
broadcastState();
renderGame();
setSystemMsg('Bot answered: ' + answer.substring(0, 40) + '...');
// Advance turn
advanceTurnInternal();
}
// ─── NETWORK ──────────────────────────────────────────────────────────
function hostGame() {
if (isConnecting) return;
isConnecting = true;
hostBtn.disabled = true;
lobbyStatus.textContent = 'Creating room...';
const code = generateRoomCode();
roomCode = code;
isHost = true;
if (peer) {
peer.destroy();
peer = null;
}
peer = new Peer(code, { debug: 2 });
peer.on('open', (id) => {
myPeerId = id;
isConnecting = false;
hostBtn.disabled = false;
const name = prompt('Enter your name (as host):', 'Host');
myPlayerName = (name && name.trim()) ? name.trim() : 'Host';
gameState = {
players: [{ name: myPlayerName, id: myPeerId, isBot: false }],
currentPlayerIndex: 0,
currentQuestion: null,
currentQuestionIndex: -1,
deck: shuffleArray([...QUESTION_POOL]),
usedQuestions: [],
revealedAnswers: []
};
showGameUI();
renderGame();
setSystemMsg('Room created! Share the code: ' + roomCode + ' (add a bot to play solo)');
lobbyStatus.textContent = '';
});
peer.on('connection', (conn) => {
connections[conn.peer] = conn;
conn.on('data', (data) => handleMessage(data, conn.peer));
conn.on('close', () => {
if (gameState) {
const idx = gameState.players.findIndex(p => p.id === conn.peer);
if (idx !== -1) {
const removed = gameState.players[idx];
gameState.players.splice(idx, 1);
if (gameState.currentPlayerIndex >= gameState.players.length) {
gameState.currentPlayerIndex = 0;
}
broadcastState();
renderGame();
setSystemMsg(removed.name + ' left the game.');
}
}
delete connections[conn.peer];
});
});
peer.on('error', (err) => {
isConnecting = false;
hostBtn.disabled = false;
if (err.type === 'unavailable-id') {
lobbyStatus.textContent = 'Room code taken, generating a new one...';
setTimeout(() => {
peer.destroy();
hostGame();
}, 500);
} else {
lobbyStatus.textContent = 'Error: ' + err.message;
console.error('Peer error:', err);
isHost = false;
roomCode = '';
peer = null;
}
});
}
function joinGame(code) {
if (isConnecting) return;
isConnecting = true;
joinBtn.disabled = true;
lobbyStatus.textContent = 'Joining...';
roomCode = code.toUpperCase();
isHost = false;
const name = prompt('Enter your name:', 'Player');
if (!name || !name.trim()) {
lobbyStatus.textContent = 'Name is required.';
isConnecting = false;
joinBtn.disabled = false;
return;
}
myPlayerName = name.trim();
const randomId = 'player-' + Math.random().toString(36).substr(2, 6);
if (peer) {
peer.destroy();
peer = null;
}
peer = new Peer(randomId, { debug: 2 });
peer.on('open', (id) => {
myPeerId = id;
isConnecting = false;
joinBtn.disabled = false;
const conn = peer.connect(roomCode, { reliable: true });
conn.on('open', () => {
connections[roomCode] = conn;
showGameUI();
lobbyStatus.textContent = '';
setSystemMsg('Connected to room ' + roomCode + '. Waiting for host...');
conn.send({ type: 'join', playerName: myPlayerName, id: myPeerId });
conn.on('data', (data) => handleMessage(data, roomCode));
conn.on('close', () => {
if (!leaving) {
lobbyStatus.textContent = 'Disconnected from host.';
setSystemMsg('Disconnected from host.', true);
delete connections[roomCode];
setTimeout(() => {
if (!gameState) showLobby();
else {
setTimeout(() => showLobby(), 2000);
}
}, 1500);
} else {
delete connections[roomCode];
}
});
conn.on('error', (err) => {
lobbyStatus.textContent = 'Connection error: ' + err.message;
isConnecting = false;
joinBtn.disabled = false;
console.error('Connection error:', err);
});
});
conn.on('error', (err) => {
lobbyStatus.textContent = 'Connection error: ' + err.message;
isConnecting = false;
joinBtn.disabled = false;
console.error('Connection error:', err);
});
});
peer.on('error', (err) => {
isConnecting = false;
joinBtn.disabled = false;
if (err.type === 'peer-unavailable') {
lobbyStatus.textContent = 'Room not found. Check the code.';
} else {
lobbyStatus.textContent = 'Error: ' + err.message;
}
console.error('Peer error:', err);
});
}
function handleMessage(data, senderId) {
if (data.type === 'initState') {
gameState = data.state;
renderGame();
setSystemMsg('You joined the game. ' + gameState.players.length + ' players.');
return;
}
if (data.type === 'join' && isHost) {
if (gameState.players.some(p => p.id === data.id)) return;
gameState.players.push({ name: data.playerName, id: data.id, isBot: false });
broadcastState();
renderGame();
setSystemMsg(data.playerName + ' joined the game.');
return;
}
if (data.type === 'hostLeft' && !isHost) {
setSystemMsg('Host has left the game. Game ended.', true);
lobbyStatus.textContent = 'Host disconnected.';
setTimeout(() => showLobby(), 3000);
return;
}
if (isHost && gameState) {
switch (data.type) {
case 'drawCard':
drawCardInternal();
break;
case 'submitAnswer': {
const p = gameState.players.find(p => p.name === data.playerName);
if (p) {
gameState.revealedAnswers.push({
playerName: data.playerName,
question: gameState.currentQuestion,
answer: data.answer
});
broadcastState();
renderGame();
setSystemMsg(data.playerName + ' answered the question.');
// Automatically advance turn after answer
advanceTurnInternal();
}
break;
}
case 'nextTurn':
advanceTurnInternal();
break;
case 'resetRequest':
resetGameInternal();
break;
default:
break;
}
} else if (!isHost && senderId === roomCode) {
if (data.type === 'stateUpdate') {
gameState = data.state;
renderGame();
}
}
}
function broadcastState() {
if (!isHost) return;
for (const pid in connections) {
const conn = connections[pid];
if (conn && conn.open) {
conn.send({ type: 'stateUpdate', state: gameState });
}
}
renderGame();
}
function drawCardInternal() {
if (!isHost || !gameState) return;
if (gameState.deck.length === 0) {
gameState.usedQuestions = [];
gameState.deck = shuffleArray([...QUESTION_POOL]);
}
const idx = Math.floor(Math.random() * gameState.deck.length);
const q = gameState.deck.splice(idx, 1)[0];
gameState.usedQuestions.push(q);
gameState.currentQuestion = q;
gameState.currentQuestionIndex = gameState.usedQuestions.length - 1;
// Set turn to first player (or keep current? We'll set to first player after drawing a new question)
gameState.currentPlayerIndex = 0;
broadcastState();
setSystemMsg('New card drawn (random): ' + q.substring(0, 40) + '...');
}
function resetGameInternal() {
if (!isHost || !gameState) return;
gameState.currentPlayerIndex = 0;
gameState.currentQuestion = null;
gameState.currentQuestionIndex = -1;
gameState.deck = shuffleArray([...QUESTION_POOL]);
gameState.usedQuestions = [];
gameState.revealedAnswers = [];
answerInput.value = '';
answerStatus.textContent = '';
broadcastState();
renderGame();
setSystemMsg('Game has been reset by the host.');
}
// ─── ADD BOT ──────────────────────────────────────────────────────────
function addBot() {
if (!isHost || !gameState) return;
if (gameState.players.length >= 4) {
setSystemMsg('Cannot add more bots. Max 4 players.', true);
return;
}
const botNames = ['Bot', 'Bot2', 'Bot3', 'Bot4'];
let botName = 'Bot';
let i = 1;
while (gameState.players.some(p => p.name === botName)) {
botName = 'Bot' + (i++);
}
gameState.players.push({ name: botName, id: 'bot-' + Date.now(), isBot: true });
broadcastState();
renderGame();
setSystemMsg('Added ' + botName + ' to the game.');
}
// ─── UI HELPERS ──────────────────────────────────────────────────────────
function showGameUI() {
lobby.style.display = 'none';
gameUI.style.display = 'block';
}
function showLobby() {
lobby.style.display = 'flex';
gameUI.style.display = 'none';
if (peer) {
if (isHost && gameState) {
for (const pid in connections) {
const conn = connections[pid];
if (conn && conn.open) {
conn.send({ type: 'hostLeft' });
}
}
}
peer.destroy();
peer = null;
}
connections = {};
gameState = null;
isHost = false;
roomCode = '';
lobbyStatus.textContent = '';
isConnecting = false;
hostBtn.disabled = false;
joinBtn.disabled = false;
leaving = false;
if (botAutoAnswerTimeout) clearTimeout(botAutoAnswerTimeout);
setSystemMsg('');
}
// ─── EVENT LISTENERS ──────────────────────────────────────────────────
hostBtn.addEventListener('click', () => {
if (isConnecting) return;
hostGame();
});
joinBtn.addEventListener('click', () => {
const code = joinCodeInput.value.trim().toUpperCase();
if (!code || code.length < 4) {
lobbyStatus.textContent = 'Please enter a valid room code (4+ characters).';
return;
}
if (isConnecting) return;
joinGame(code);
});
joinCodeInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') joinBtn.click();
});
copyCodeBtn.addEventListener('click', () => {
const text = roomCode;
if (navigator.clipboard) {
navigator.clipboard.writeText(text).catch(() => {});
} else {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
document.body.removeChild(ta);
}
copyCodeBtn.textContent = 'Copied!';
setTimeout(() => copyCodeBtn.textContent = 'Copy', 1500);
});
leaveGameBtn.addEventListener('click', () => {
if (confirm('Leave the game?')) {
leaving = true;
if (isHost && gameState) {
for (const pid in connections) {
const conn = connections[pid];
if (conn && conn.open) {
conn.send({ type: 'hostLeft' });
}
}
}
for (const pid in connections) {
if (connections[pid] && connections[pid].open) {
connections[pid].close();
}
}
showLobby();
}
});
addBotBtn.addEventListener('click', addBot);
drawCardBtn.addEventListener('click', () => {
if (isHost) {
drawCardInternal();
} else {
if (connections[roomCode] && connections[roomCode].open) {
connections[roomCode].send({ type: 'drawCard' });
} else {
alert('Not connected to host.');
}
}
});
submitAnswerBtn.addEventListener('click', () => {
const answer = answerInput.value.trim();
if (!answer) {
answerStatus.textContent = 'Please type an answer.';
return;
}
if (!gameState || !gameState.currentQuestion) return;
if (gameState.revealedAnswers.some(r => r.playerName === myPlayerName && r.question === gameState
.currentQuestion)) {
answerStatus.textContent = 'You already answered this.';
return;
}
if (isHost) {
gameState.revealedAnswers.push({
playerName: myPlayerName,
question: gameState.currentQuestion,
answer: answer
});
broadcastState();
answerInput.value = '';
answerStatus.textContent = 'Answer submitted!';
setSystemMsg('You answered the question.');
// Advance turn
advanceTurnInternal();
} else {
if (connections[roomCode] && connections[roomCode].open) {
connections[roomCode].send({
type: 'submitAnswer',
playerName: myPlayerName,
answer: answer
});
answerInput.value = '';
answerStatus.textContent = 'Answer sent!';
} else {
answerStatus.textContent = 'Not connected to host.';
}
}
});
answerInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !submitAnswerBtn.disabled) {
e.preventDefault();
submitAnswerBtn.click();
}
});
nextTurnBtn.addEventListener('click', () => {
if (isHost) {
advanceTurnInternal();
} else {
if (connections[roomCode] && connections[roomCode].open) {
connections[roomCode].send({ type: 'nextTurn' });
}
}
});
resetGameBtn.addEventListener('click', () => {
if (!gameState) return;
if (isHost) {
if (confirm('Reset the game? All answers and the current question will be cleared.')) {
resetGameInternal();
}
} else {
if (connections[roomCode] && connections[roomCode].open) {
connections[roomCode].send({ type: 'resetRequest' });
setSystemMsg('Reset requested. Waiting for host...');
} else {
alert('Not connected to host.');
}
}
});
// ─── INIT ──────────────────────────────────────────────────────────────
showLobby();
if (typeof Peer === 'undefined') {
lobbyStatus.textContent = 'PeerJS library failed to load. Check internet connection.';
}
})();
</script>
</body>
</html>Game Source: Social Change – Online Multiplayer + Bot
Creator: GoldenCobra39
Libraries: none
Complexity: complex (1371 lines, 62.9 KB)
The full source code is displayed above on this page.
Remix Instructions
To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: social-change-online-multiplayer-bot-goldencobra39" to link back to the original. Then publish at arcadelab.ai/publish.