empathy-game / index.html
C50BARZ's picture
Add 2 files
c43b17d verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Empathy Connection Game</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/emoji-picker-element@1"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap');
body {
font-family: 'Poppins', sans-serif;
background-color: #f3f4f6;
height: 100vh;
overflow: hidden;
}
.message-container {
scrollbar-width: thin;
scrollbar-color: #9CA3AF #E5E7EB;
}
.message-container::-webkit-scrollbar {
width: 6px;
}
.message-container::-webkit-scrollbar-track {
background: #E5E7EB;
}
.message-container::-webkit-scrollbar-thumb {
background-color: #9CA3AF;
border-radius: 3px;
}
.typing-indicator::after {
content: "...";
animation: typing 1.5s infinite;
display: inline-block;
width: 0;
}
@keyframes typing {
0% { content: "."; }
33% { content: ".."; }
66% { content: "..."; }
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
}
.emoji-picker {
position: absolute;
bottom: 60px;
right: 20px;
z-index: 10;
}
.confetti {
position: absolute;
width: 10px;
height: 10px;
background-color: #f00;
opacity: 0;
}
</style>
</head>
<body class="flex flex-col h-screen">
<!-- Header -->
<header class="bg-indigo-600 text-white p-4 shadow-md">
<div class="container mx-auto flex justify-between items-center">
<div class="flex items-center space-x-2">
<i class="fas fa-heart text-2xl"></i>
<h1 class="text-xl font-bold">Empathy Connection</h1>
</div>
<div id="user-count" class="flex items-center space-x-1">
<i class="fas fa-users"></i>
<span>1</span>
</div>
</div>
</header>
<!-- Game Area -->
<main class="flex-1 flex flex-col md:flex-row overflow-hidden">
<!-- Sidebar -->
<aside class="w-full md:w-64 bg-white p-4 border-r border-gray-200 flex flex-col">
<div class="mb-6">
<h2 class="font-bold text-lg text-indigo-700 mb-2">Game Rules</h2>
<ul class="text-sm space-y-2 text-gray-600">
<li class="flex items-start">
<i class="fas fa-check-circle text-green-500 mt-1 mr-2"></i>
<span>Share personal experiences when prompted</span>
</li>
<li class="flex items-start">
<i class="fas fa-check-circle text-green-500 mt-1 mr-2"></i>
<span>Respond with empathy and understanding</span>
</li>
<li class="flex items-start">
<i class="fas fa-check-circle text-green-500 mt-1 mr-2"></i>
<span>Be respectful and kind to others</span>
</li>
</ul>
</div>
<div class="mb-6">
<h2 class="font-bold text-lg text-indigo-700 mb-2">Players</h2>
<div id="players-list" class="space-y-2">
<div class="flex items-center space-x-2">
<div class="w-8 h-8 rounded-full bg-indigo-100 flex items-center justify-center text-indigo-600">
<i class="fas fa-user"></i>
</div>
<span class="text-sm">You</span>
</div>
</div>
</div>
<div class="mt-auto">
<div id="game-status" class="p-3 rounded-lg bg-indigo-50 text-indigo-700 text-sm">
<p>Waiting for more players to join...</p>
</div>
</div>
</aside>
<!-- Main Chat Area -->
<section class="flex-1 flex flex-col bg-gray-50">
<!-- Prompt Area -->
<div id="prompt-area" class="p-4 bg-white border-b border-gray-200">
<div class="max-w-3xl mx-auto">
<div class="bg-indigo-50 p-4 rounded-lg">
<h3 class="font-bold text-indigo-700 mb-2">Today's Theme: Overcoming Challenges</h3>
<p class="text-gray-700">Share a time when you faced a difficult situation and how you handled it. Then respond to others with empathy and understanding.</p>
</div>
</div>
</div>
<!-- Messages Container -->
<div id="messages" class="message-container flex-1 overflow-y-auto p-4 space-y-4">
<div class="max-w-3xl mx-auto">
<div class="text-center py-8">
<div class="inline-block p-4 bg-indigo-100 rounded-full">
<i class="fas fa-heart text-indigo-500 text-3xl"></i>
</div>
<h3 class="text-xl font-bold text-indigo-700 mt-4">Welcome to Empathy Connection</h3>
<p class="text-gray-600 mt-2">Share your story and connect with others through empathy</p>
</div>
</div>
</div>
<!-- Input Area -->
<div class="p-4 bg-white border-t border-gray-200">
<div class="max-w-3xl mx-auto relative">
<div id="typing-indicator" class="text-xs text-gray-500 mb-1 hidden">
<span class="typing-indicator">Someone is typing</span>
</div>
<div class="flex space-x-2">
<button id="emoji-button" class="w-10 h-10 rounded-full bg-gray-100 flex items-center justify-center text-gray-500 hover:bg-gray-200">
<i class="far fa-smile"></i>
</button>
<div class="flex-1 relative">
<textarea id="message-input" rows="1" class="w-full p-3 pr-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 resize-none" placeholder="Share your experience..."></textarea>
<button id="send-button" class="absolute right-2 bottom-2 w-8 h-8 rounded-full bg-indigo-100 text-indigo-600 flex items-center justify-center hover:bg-indigo-200 disabled:opacity-50" disabled>
<i class="fas fa-paper-plane"></i>
</button>
</div>
</div>
<emoji-picker id="emoji-picker" class="emoji-picker hidden"></emoji-picker>
</div>
</div>
</section>
</main>
<!-- Connection Modal -->
<div id="connection-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4">
<div class="text-center">
<div class="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-4">
<i class="fas fa-plug text-indigo-500 text-2xl"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-2">Connecting to Game</h3>
<p class="text-gray-600 mb-6">Please wait while we connect you to other players...</p>
<div class="w-full bg-gray-200 rounded-full h-2">
<div id="connection-progress" class="bg-indigo-600 h-2 rounded-full w-0"></div>
</div>
</div>
</div>
</div>
<!-- Welcome Modal -->
<div id="welcome-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4">
<div class="text-center">
<div class="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mx-auto mb-4">
<i class="fas fa-heart text-indigo-500 text-2xl"></i>
</div>
<h3 class="text-xl font-bold text-gray-800 mb-2">Welcome to Empathy Connection</h3>
<p class="text-gray-600 mb-4">Practice empathy by sharing experiences and responding to others with understanding.</p>
<div class="mb-4">
<label for="username" class="block text-left text-sm font-medium text-gray-700 mb-1">Your Name</label>
<input type="text" id="username" class="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Enter your name">
</div>
<button id="start-game" class="w-full bg-indigo-600 text-white py-2 px-4 rounded-md hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2">
Join the Game
</button>
</div>
</div>
</div>
<script>
// Game state
const gameState = {
players: {},
currentUser: null,
socket: null,
isTyping: false,
typingTimeout: null
};
// DOM elements
const elements = {
connectionModal: document.getElementById('connection-modal'),
welcomeModal: document.getElementById('welcome-modal'),
usernameInput: document.getElementById('username'),
startGameButton: document.getElementById('start-game'),
messageInput: document.getElementById('message-input'),
sendButton: document.getElementById('send-button'),
messagesContainer: document.getElementById('messages'),
playersList: document.getElementById('players-list'),
userCount: document.getElementById('user-count'),
typingIndicator: document.getElementById('typing-indicator'),
emojiButton: document.getElementById('emoji-button'),
emojiPicker: document.getElementById('emoji-picker'),
gameStatus: document.getElementById('game-status'),
connectionProgress: document.getElementById('connection-progress')
};
// Initialize the game
function initGame() {
// Show welcome modal
setTimeout(() => {
elements.connectionModal.classList.add('hidden');
elements.welcomeModal.classList.remove('hidden');
}, 2000);
// Simulate connection progress
let progress = 0;
const interval = setInterval(() => {
progress += 5;
elements.connectionProgress.style.width = `${progress}%`;
if (progress >= 100) {
clearInterval(interval);
}
}, 100);
// Set up event listeners
setupEventListeners();
}
// Set up all event listeners
function setupEventListeners() {
// Start game button
elements.startGameButton.addEventListener('click', () => {
const username = elements.usernameInput.value.trim();
if (username) {
joinGame(username);
}
});
// Message input events
elements.messageInput.addEventListener('input', handleMessageInput);
elements.messageInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
// Send button
elements.sendButton.addEventListener('click', sendMessage);
// Emoji picker
elements.emojiButton.addEventListener('click', toggleEmojiPicker);
elements.emojiPicker.addEventListener('emoji-click', (event) => {
const emoji = event.detail.unicode;
const input = elements.messageInput;
const startPos = input.selectionStart;
const endPos = input.selectionEnd;
input.value = input.value.substring(0, startPos) + emoji + input.value.substring(endPos);
input.focus();
input.selectionStart = startPos + emoji.length;
input.selectionEnd = startPos + emoji.length;
// Trigger input event to update send button state
input.dispatchEvent(new Event('input'));
});
// Close emoji picker when clicking outside
document.addEventListener('click', (e) => {
if (!elements.emojiButton.contains(e.target) && !elements.emojiPicker.contains(e.target)) {
elements.emojiPicker.classList.add('hidden');
}
});
}
// Toggle emoji picker visibility
function toggleEmojiPicker() {
elements.emojiPicker.classList.toggle('hidden');
}
// Handle message input changes
function handleMessageInput() {
const message = elements.messageInput.value.trim();
elements.sendButton.disabled = message === '';
// Update typing status
if (message && !gameState.isTyping) {
gameState.isTyping = true;
sendTypingStatus(true);
} else if (!message && gameState.isTyping) {
gameState.isTyping = false;
sendTypingStatus(false);
}
// Reset typing timeout
if (gameState.typingTimeout) {
clearTimeout(gameState.typingTimeout);
}
if (message) {
gameState.typingTimeout = setTimeout(() => {
gameState.isTyping = false;
sendTypingStatus(false);
}, 2000);
}
// Auto-resize textarea
autoResizeTextarea();
}
// Auto-resize textarea based on content
function autoResizeTextarea() {
const textarea = elements.messageInput;
textarea.style.height = 'auto';
textarea.style.height = `${Math.min(textarea.scrollHeight, 120)}px`;
}
// Send typing status to server
function sendTypingStatus(isTyping) {
if (gameState.socket) {
gameState.socket.send(JSON.stringify({
type: 'typing',
userId: gameState.currentUser.id,
isTyping: isTyping
}));
}
}
// Join the game
function joinGame(username) {
elements.welcomeModal.classList.add('hidden');
// Create user
gameState.currentUser = {
id: generateId(),
name: username,
color: getRandomColor(),
avatar: getRandomAvatar()
};
// Simulate WebSocket connection
setTimeout(() => {
connectWebSocket();
}, 500);
// Add user to players list
addPlayer(gameState.currentUser);
// Show welcome message
addSystemMessage(`Welcome, ${username}! Share your experience when you're ready.`);
}
// Connect to WebSocket (simulated in this demo)
function connectWebSocket() {
// In a real app, this would connect to your WebSocket server
console.log("Connecting to WebSocket server...");
gameState.socket = {
send: function(data) {
// Simulate receiving messages with a delay
setTimeout(() => {
const message = JSON.parse(data);
handleIncomingMessage(message);
}, 300);
}
};
// Simulate other players joining
simulateOtherPlayers();
}
// Simulate other players joining the game
function simulateOtherPlayers() {
const names = ["Alex", "Jordan", "Taylor", "Morgan", "Casey"];
const avatars = ["user", "user-tie", "user-graduate", "user-nurse", "user-astronaut"];
// Add 3-5 simulated players
const numPlayers = Math.floor(Math.random() * 3) + 3;
for (let i = 0; i < numPlayers; i++) {
setTimeout(() => {
const player = {
id: generateId(),
name: names[i],
color: getRandomColor(),
avatar: `fa-${avatars[i]}`
};
gameState.players[player.id] = player;
addPlayer(player);
addSystemMessage(`${player.name} has joined the game`);
// Update user count
updateUserCount();
// Simulate some messages from other players
if (i === 0) {
setTimeout(() => {
simulatePlayerMessage(player, "Hi everyone! I'm excited to share and listen today.");
}, 1500);
}
if (i === 1) {
setTimeout(() => {
simulatePlayerMessage(player, "This is my first time here. Looking forward to connecting!");
}, 3000);
}
if (i === 2) {
setTimeout(() => {
simulatePlayerTyping(player);
}, 4500);
}
}, i * 1000);
}
// Update game status
setTimeout(() => {
elements.gameStatus.innerHTML = `
<p class="font-medium">Game in progress</p>
<p class="text-xs mt-1">Share your story and respond to others</p>
`;
}, numPlayers * 1000);
}
// Simulate a player typing
function simulatePlayerTyping(player) {
// Show typing indicator
const typingEvent = {
type: 'typing',
userId: player.id,
isTyping: true
};
handleIncomingMessage(typingEvent);
// Send message after delay
setTimeout(() => {
simulatePlayerMessage(player, "I once faced a big challenge when I had to speak in front of a large audience. I was so nervous but I practiced a lot and it went better than I expected!");
// Hide typing indicator
const stopTypingEvent = {
type: 'typing',
userId: player.id,
isTyping: false
};
handleIncomingMessage(stopTypingEvent);
}, 2000);
}
// Simulate a message from another player
function simulatePlayerMessage(player, text) {
const message = {
type: 'message',
userId: player.id,
text: text,
timestamp: new Date().toISOString()
};
handleIncomingMessage(message);
}
// Handle incoming messages from WebSocket
function handleIncomingMessage(message) {
switch (message.type) {
case 'message':
addPlayerMessage(message.userId, message.text, message.timestamp);
break;
case 'typing':
handleTypingIndicator(message.userId, message.isTyping);
break;
case 'user-joined':
addPlayer(message.user);
addSystemMessage(`${message.user.name} has joined the game`);
updateUserCount();
break;
case 'user-left':
removePlayer(message.userId);
addSystemMessage(`${gameState.players[message.userId]?.name || 'A player'} has left the game`);
updateUserCount();
break;
}
}
// Send a message
function sendMessage() {
const text = elements.messageInput.value.trim();
if (!text || !gameState.socket) return;
// Create message
const message = {
type: 'message',
userId: gameState.currentUser.id,
text: text,
timestamp: new Date().toISOString()
};
// Add to chat
addPlayerMessage(gameState.currentUser.id, text, message.timestamp);
// Clear input
elements.messageInput.value = '';
elements.sendButton.disabled = true;
autoResizeTextarea();
// Send to server
gameState.socket.send(JSON.stringify(message));
// Stop typing
if (gameState.isTyping) {
gameState.isTyping = false;
sendTypingStatus(false);
}
// Simulate responses from other players
simulateEmpatheticResponses(text);
}
// Simulate empathetic responses from other players
function simulateEmpatheticResponses(text) {
const players = Object.values(gameState.players);
if (players.length === 0) return;
// Randomly select 1-2 players to respond
const numResponses = Math.floor(Math.random() * 2) + 1;
const responders = [];
for (let i = 0; i < numResponses && i < players.length; i++) {
let randomPlayer;
do {
randomPlayer = players[Math.floor(Math.random() * players.length)];
} while (randomPlayer.id === gameState.currentUser.id || responders.includes(randomPlayer));
responders.push(randomPlayer);
// Generate response based on message content
let response;
if (text.toLowerCase().includes("happy") || text.toLowerCase().includes("excited")) {
response = "That's wonderful to hear! I'm so happy for you!";
} else if (text.toLowerCase().includes("sad") || text.toLowerCase().includes("difficult")) {
response = "I'm really sorry to hear that. That must have been really hard.";
} else if (text.toLowerCase().includes("nervous") || text.toLowerCase().includes("anxious")) {
response = "I can relate to that feeling. It takes courage to face those situations.";
} else {
const responses = [
"Thank you for sharing that. I appreciate your openness.",
"That's really interesting. I hadn't thought about it that way before.",
"I can understand how that would feel. Thanks for helping me see your perspective.",
"That sounds like a meaningful experience. I appreciate you sharing it with us."
];
response = responses[Math.floor(Math.random() * responses.length)];
}
// Delay the response
setTimeout(() => {
simulatePlayerTyping(randomPlayer);
setTimeout(() => {
simulatePlayerMessage(randomPlayer, response);
// Occasionally trigger confetti for positive messages
if (text.toLowerCase().includes("happy") || text.toLowerCase().includes("excited")) {
if (Math.random() > 0.7) {
triggerConfetti();
}
}
}, 1500);
}, Math.random() * 3000 + 1000);
}
}
// Add a player to the list
function addPlayer(player) {
gameState.players[player.id] = player;
const playerElement = document.createElement('div');
playerElement.className = 'flex items-center space-x-2';
playerElement.id = `player-${player.id}`;
playerElement.innerHTML = `
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white" style="background-color: ${player.color}">
<i class="fas ${player.avatar}"></i>
</div>
<span class="text-sm">${player.name}</span>
`;
elements.playersList.appendChild(playerElement);
updateUserCount();
}
// Remove a player from the list
function removePlayer(playerId) {
const playerElement = document.getElementById(`player-${playerId}`);
if (playerElement) {
playerElement.remove();
}
delete gameState.players[playerId];
updateUserCount();
}
// Update the user count display
function updateUserCount() {
const count = Object.keys(gameState.players).length + 1; // +1 for current user
elements.userCount.querySelector('span').textContent = count;
}
// Add a system message to the chat
function addSystemMessage(text) {
const messageElement = document.createElement('div');
messageElement.className = 'text-center my-4';
messageElement.innerHTML = `
<span class="inline-block px-3 py-1 bg-gray-100 text-gray-600 rounded-full text-sm">
${text}
</span>
`;
elements.messagesContainer.appendChild(messageElement);
scrollToBottom();
}
// Add a player message to the chat
function addPlayerMessage(userId, text, timestamp) {
const isCurrentUser = userId === gameState.currentUser?.id;
const player = isCurrentUser ? gameState.currentUser : gameState.players[userId];
if (!player) return;
const time = new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const messageElement = document.createElement('div');
messageElement.className = `flex ${isCurrentUser ? 'justify-end' : 'justify-start'} mb-4`;
if (isCurrentUser) {
messageElement.innerHTML = `
<div class="max-w-xs md:max-w-md lg:max-w-lg">
<div class="bg-indigo-100 text-gray-800 p-3 rounded-lg rounded-tr-none">
<p>${text}</p>
<div class="text-right mt-1">
<span class="text-xs text-gray-500">${time}</span>
</div>
</div>
</div>
`;
} else {
messageElement.innerHTML = `
<div class="flex space-x-2 max-w-xs md:max-w-md lg:max-w-lg">
<div class="w-8 h-8 rounded-full flex items-center justify-center text-white" style="background-color: ${player.color}">
<i class="fas ${player.avatar}"></i>
</div>
<div>
<div class="font-medium text-xs text-gray-600 mb-1">${player.name}</div>
<div class="bg-white border border-gray-200 text-gray-800 p-3 rounded-lg rounded-tl-none">
<p>${text}</p>
<div class="text-right mt-1">
<span class="text-xs text-gray-500">${time}</span>
</div>
</div>
</div>
</div>
`;
}
elements.messagesContainer.appendChild(messageElement);
scrollToBottom();
}
// Handle typing indicator
function handleTypingIndicator(userId, isTyping) {
if (userId === gameState.currentUser?.id) return;
const player = gameState.players[userId];
if (!player) return;
if (isTyping) {
elements.typingIndicator.innerHTML = `
<span class="typing-indicator">${player.name} is typing</span>
`;
elements.typingIndicator.classList.remove('hidden');
} else {
elements.typingIndicator.classList.add('hidden');
}
}
// Scroll chat to bottom
function scrollToBottom() {
elements.messagesContainer.scrollTop = elements.messagesContainer.scrollHeight;
}
// Generate random ID
function generateId() {
return Math.random().toString(36).substr(2, 9);
}
// Get random color for player avatar
function getRandomColor() {
const colors = [
'#EF4444', '#F59E0B', '#10B981', '#3B82F6', '#6366F1', '#8B5CF6', '#EC4899'
];
return colors[Math.floor(Math.random() * colors.length)];
}
// Get random avatar icon
function getRandomAvatar() {
const avatars = [
'fa-user', 'fa-user-tie', 'fa-user-graduate', 'fa-user-nurse',
'fa-user-astronaut', 'fa-user-secret', 'fa-user-md', 'fa-user-ninja'
];
return avatars[Math.floor(Math.random() * avatars.length)];
}
// Trigger confetti effect
function triggerConfetti() {
const colors = ['#EF4444', '#F59E0B', '#10B981', '#3B82F6', '#6366F1', '#8B5CF6', '#EC4899'];
for (let i = 0; i < 50; i++) {
const confetti = document.createElement('div');
confetti.className = 'confetti';
confetti.style.backgroundColor = colors[Math.floor(Math.random() * colors.length)];
confetti.style.left = `${Math.random() * 100}%`;
confetti.style.transform = `rotate(${Math.random() * 360}deg)`;
// Random size
const size = Math.random() * 10 + 5;
confetti.style.width = `${size}px`;
confetti.style.height = `${size}px`;
document.body.appendChild(confetti);
// Animate
const animationDuration = Math.random() * 3 + 2;
const animationDelay = Math.random() * 0.5;
confetti.style.animation = `
confetti-fall ${animationDuration}s ease-in ${animationDelay}s forwards
`;
// Remove after animation
setTimeout(() => {
confetti.remove();
}, (animationDuration + animationDelay) * 1000);
}
// Add confetti animation styles
const style = document.createElement('style');
style.innerHTML = `
@keyframes confetti-fall {
0% {
opacity: 1;
transform: translateY(-100vh) rotate(0deg);
}
100% {
opacity: 0;
transform: translateY(100vh) rotate(360deg);
}
}
`;
document.head.appendChild(style);
}
// Initialize the game when the page loads
window.addEventListener('DOMContentLoaded', initGame);
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=C50BARZ/empathy-game" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>