|
<!DOCTYPE html> |
|
<html lang="en"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Shy Guy Simulator - Complete Edition</title> |
|
<style> |
|
body { |
|
font-family: Arial, sans-serif; |
|
max-width: 1200px; |
|
margin: 20px auto; |
|
padding: 20px; |
|
background-color: #1a1a1a; |
|
color: #fff; |
|
} |
|
|
|
.game-layout { |
|
display: grid; |
|
grid-template-columns: 2fr 3fr; |
|
gap: 20px; |
|
} |
|
|
|
#game-container { |
|
background-color: #2a2a2a; |
|
padding: 20px; |
|
border-radius: 8px; |
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); |
|
} |
|
|
|
.chat-container { |
|
height: 300px; |
|
overflow-y: auto; |
|
margin: 20px 0; |
|
padding: 10px; |
|
background-color: #333; |
|
border-radius: 4px; |
|
} |
|
|
|
.message { |
|
margin: 10px 0; |
|
padding: 10px; |
|
border-radius: 4px; |
|
word-wrap: break-word; |
|
} |
|
|
|
.wingman { |
|
background-color: #2c5282; |
|
margin-right: 20%; |
|
} |
|
|
|
.shyguy { |
|
background-color: #4a5568; |
|
margin-left: 20%; |
|
} |
|
|
|
.error { |
|
background-color: #c53030; |
|
text-align: center; |
|
} |
|
|
|
.grid-container { |
|
display: grid; |
|
grid-template-columns: repeat(10, 1fr); |
|
gap: 2px; |
|
background-color: #1a1a1a; |
|
padding: 10px; |
|
margin: 20px 0; |
|
border-radius: 4px; |
|
aspect-ratio: 1; |
|
} |
|
|
|
.grid-cell { |
|
aspect-ratio: 1; |
|
background-color: #2d3748; |
|
border-radius: 2px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
position: relative; |
|
cursor: pointer; |
|
} |
|
|
|
#player { |
|
background-color: #4299e1; |
|
width: 80%; |
|
height: 80%; |
|
border-radius: 50%; |
|
position: absolute; |
|
transition: all 0.3s ease; |
|
} |
|
|
|
.bar { background-color: #744210 !important; } |
|
.dj { background-color: #2c5282 !important; } |
|
.girl { background-color: #d53f8c !important; } |
|
.sister { background-color: #805ad5 !important; } |
|
.obstacle { background-color: #4a5568 !important; } |
|
|
|
.legend { |
|
display: flex; |
|
gap: 10px; |
|
margin-top: 10px; |
|
flex-wrap: wrap; |
|
} |
|
|
|
.legend-item { |
|
display: flex; |
|
align-items: center; |
|
gap: 5px; |
|
} |
|
|
|
.legend-color { |
|
width: 20px; |
|
height: 20px; |
|
border-radius: 4px; |
|
} |
|
|
|
#input-container { |
|
display: flex; |
|
gap: 10px; |
|
margin-top: 20px; |
|
} |
|
|
|
#user-input { |
|
flex-grow: 1; |
|
padding: 10px; |
|
border: none; |
|
border-radius: 4px; |
|
background-color: #4a4a4a; |
|
color: white; |
|
} |
|
|
|
button { |
|
padding: 10px 20px; |
|
background-color: #4299e1; |
|
color: white; |
|
border: none; |
|
border-radius: 4px; |
|
cursor: pointer; |
|
} |
|
|
|
button:hover { |
|
background-color: #3182ce; |
|
} |
|
|
|
#stats { |
|
margin-top: 20px; |
|
padding: 10px; |
|
background-color: #333; |
|
border-radius: 4px; |
|
display: flex; |
|
justify-content: space-between; |
|
flex-wrap: wrap; |
|
gap: 10px; |
|
} |
|
|
|
.movement-controls { |
|
display: grid; |
|
grid-template-columns: repeat(3, 1fr); |
|
gap: 5px; |
|
max-width: 200px; |
|
margin: 20px auto; |
|
} |
|
|
|
.drunk-effect { |
|
animation: wobble 1s infinite; |
|
} |
|
|
|
@keyframes wobble { |
|
0% { transform: translate(0, 0) rotate(0deg); } |
|
15% { transform: translate(-5%, 0) rotate(-5deg); } |
|
30% { transform: translate(5%, 0) rotate(5deg); } |
|
45% { transform: translate(-5%, 0) rotate(-3deg); } |
|
60% { transform: translate(5%, 0) rotate(3deg); } |
|
75% { transform: translate(-5%, 0) rotate(-1deg); } |
|
100% { transform: translate(0, 0) rotate(0deg); } |
|
} |
|
|
|
#api-key-container { |
|
margin-bottom: 20px; |
|
} |
|
|
|
.tooltip { |
|
position: absolute; |
|
background: rgba(0, 0, 0, 0.8); |
|
padding: 5px; |
|
border-radius: 4px; |
|
font-size: 12px; |
|
pointer-events: none; |
|
z-index: 100; |
|
display: none; |
|
} |
|
|
|
.win-screen { |
|
position: fixed; |
|
top: 50%; |
|
left: 50%; |
|
transform: translate(-50%, -50%); |
|
background: rgba(0, 0, 0, 0.9); |
|
padding: 20px; |
|
border-radius: 10px; |
|
text-align: center; |
|
z-index: 1000; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="game-container"> |
|
<h1>Shy Guy Simulator - Complete Edition</h1> |
|
|
|
<div id="api-key-container"> |
|
<input type="password" id="api-key" placeholder="Enter your Mistral API key" style="width: 100%; padding: 10px; margin-bottom: 10px;"> |
|
<button onclick="initializeGame()" id="start-button">Start Game</button> |
|
</div> |
|
|
|
<div id="game-content" style="display: none;"> |
|
<div id="stats"> |
|
<span>Confidence: <span id="confidence">0</span>%</span> |
|
<span>Anxiety: <span id="anxiety">100</span>%</span> |
|
<span>Drinks: <span id="drinks">0</span></span> |
|
<span>Time: <span id="time">8:00 PM</span></span> |
|
</div> |
|
|
|
<div class="game-layout"> |
|
<div class="chat-side"> |
|
<div class="chat-container" id="chat-container"></div> |
|
<div id="input-container"> |
|
<input type="text" id="user-input" placeholder="Type your encouragement as wingman..."> |
|
<button onclick="handleUserInput()">Send</button> |
|
</div> |
|
</div> |
|
|
|
<div class="game-side"> |
|
<div class="grid-container" id="party-grid"></div> |
|
|
|
<div class="legend"> |
|
<div class="legend-item"> |
|
<div class="legend-color" style="background-color: #4299e1;"></div> |
|
<span>You</span> |
|
</div> |
|
<div class="legend-item"> |
|
<div class="legend-color" style="background-color: #744210;"></div> |
|
<span>Bar</span> |
|
</div> |
|
<div class="legend-item"> |
|
<div class="legend-color" style="background-color: #2c5282;"></div> |
|
<span>DJ</span> |
|
</div> |
|
<div class="legend-item"> |
|
<div class="legend-color" style="background-color: #d53f8c;"></div> |
|
<span>Girl</span> |
|
</div> |
|
<div class="legend-item"> |
|
<div class="legend-color" style="background-color: #805ad5;"></div> |
|
<span>Sister</span> |
|
</div> |
|
</div> |
|
|
|
<div class="movement-controls"> |
|
<button onclick="move(0, -1)">↑</button> |
|
<button onclick="move(-1, 0)">←</button> |
|
<button onclick="move(1, 0)">→</button> |
|
<button onclick="move(0, 1)">↓</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="tooltip" id="tooltip"></div> |
|
|
|
<script> |
|
class ShyGuySimulator { |
|
constructor(apiKey) { |
|
this.apiKey = apiKey; |
|
this.state = { |
|
confidence: 0, |
|
anxiety: 100, |
|
drinks: 0, |
|
time: new Date(2024, 0, 1, 20, 0), |
|
playerPos: { x: 0, y: 9 }, |
|
moveSpeed: 1, |
|
isProcessing: false, |
|
hasSpokenToGirl: false, |
|
lastEmotion: 'anxious', |
|
locations: { |
|
bar: [{ x: 8, y: 1 }, { x: 9, y: 1 }], |
|
dj: [{ x: 4, y: 0 }, { x: 5, y: 0 }], |
|
girl: [{ x: 9, y: 8 }], |
|
sister: [{ x: 2, y: 5 }], |
|
obstacles: [ |
|
{ x: 3, y: 3 }, { x: 4, y: 3 }, |
|
{ x: 6, y: 6 }, { x: 7, y: 6 } |
|
] |
|
} |
|
}; |
|
|
|
this.context = [{ |
|
role: 'system', |
|
content: `You are roleplaying as a shy guy at a party, providing both dialogue and movement decisions. |
|
The party is on a 10x10 grid. You start at (0,9), and the girl you like is at (9,8). |
|
ALWAYS structure your responses in this exact format: |
|
{ |
|
"dialogue": "Your spoken response here", |
|
"movement": { |
|
"x": number (-1, 0, or 1 for movement), |
|
"y": number (-1, 0, or 1 for movement) |
|
}, |
|
"emotion": "anxious|nervous|slightly_confident|confident" |
|
} |
|
|
|
Rules: |
|
1. When drinks > 2, be more likely to move toward the girl |
|
2. When confidence < 30, prefer to move away or stay still |
|
3. Keep dialogue natural and brief (1-2 sentences) |
|
4. Movement should reflect emotional state |
|
5. Account for obstacles at: [(3,3), (4,3), (6,6), (7,6)] |
|
6. Consider locations of: bar(8,1 & 9,1), DJ(4,0 & 5,0), sister(2,5) |
|
Current state: Confidence: ${this.state.confidence}%, Anxiety: ${this.state.anxiety}%` |
|
}]; |
|
|
|
this.initialize(); |
|
this.initializeGrid(); |
|
this.startAutonomousMovement(); |
|
} |
|
|
|
initialize() { |
|
this.addMessage("Hey! I'll be your wingman tonight. I see that girl you like over there - let's help you talk to her!", 'wingman'); |
|
this.addMessage("I... I don't know about this. Maybe I should just go home...", 'shyguy'); |
|
this.updateStats(); |
|
} |
|
|
|
startAutonomousMovement() { |
|
this.movementInterval = setInterval(async () => { |
|
if (!this.state.isProcessing && !this.state.hasSpokenToGirl) { |
|
await this.getNextMove(); |
|
} |
|
}, 5000); |
|
} |
|
|
|
stopAutonomousMovement() { |
|
if (this.movementInterval) { |
|
clearInterval(this.movementInterval); |
|
} |
|
} |
|
|
|
async getNextMove() { |
|
try { |
|
const response = await this.callMistralAPI(); |
|
let parsedResponse; |
|
|
|
try { |
|
parsedResponse = JSON.parse(response); |
|
} catch (e) { |
|
const jsonMatch = response.match(/\{[\s\S]*\}/); |
|
if (jsonMatch) { |
|
parsedResponse = JSON.parse(jsonMatch[0]); |
|
} else { |
|
throw new Error('Could not parse LLM response'); |
|
} |
|
} |
|
|
|
this.state.lastEmotion = parsedResponse.emotion; |
|
this.addMessage(parsedResponse.dialogue, 'shyguy'); |
|
|
|
if (parsedResponse.movement) { |
|
await this.movePlayer( |
|
parsedResponse.movement.x, |
|
parsedResponse.movement.y |
|
); |
|
} |
|
} catch (error) { |
|
console.error('Error getting next move:', error); |
|
} |
|
} |
|
|
|
initializeGrid() { |
|
const grid = document.getElementById('party-grid'); |
|
grid.innerHTML = ''; |
|
|
|
for (let y = 0; y < 10; y++) { |
|
for (let x = 0; x < 10; x++) { |
|
const cell = document.createElement('div'); |
|
cell.className = 'grid-cell'; |
|
cell.dataset.x = x; |
|
cell.dataset.y = y; |
|
|
|
if (this.isLocation(x, y, 'bar')) { |
|
cell.classList.add('bar'); |
|
cell.dataset.tooltip = "Bar - Get liquid courage"; |
|
} |
|
if (this.isLocation(x, y, 'dj')) { |
|
cell.classList.add('dj'); |
|
cell.dataset.tooltip = "DJ - Vibe to the music"; |
|
} |
|
if (this.isLocation(x, y, 'girl')) { |
|
cell.classList.add('girl'); |
|
cell.dataset.tooltip = "The girl you like"; |
|
} |
|
if (this.isLocation(x, y, 'sister')) { |
|
cell.classList.add('sister'); |
|
cell.dataset.tooltip = "Your sister - Get some encouragement"; |
|
} |
|
if (this.isLocation(x, y, 'obstacles')) { |
|
cell.classList.add('obstacle'); |
|
cell.dataset.tooltip = "Can't walk here"; |
|
} |
|
|
|
if (x === this.state.playerPos.x && y === this.state.playerPos.y) { |
|
const player = document.createElement('div'); |
|
player.id = 'player'; |
|
if (this.state.drinks >= 3) player.classList.add('drunk-effect'); |
|
cell.appendChild(player); |
|
} |
|
|
|
cell.addEventListener('mouseover', this.showTooltip); |
|
cell.addEventListener('mouseout', this.hideTooltip); |
|
|
|
grid.appendChild(cell); |
|
} |
|
} |
|
} |
|
|
|
showTooltip(e) { |
|
const tooltip = document.getElementById('tooltip'); |
|
const tooltipText = e.target.dataset.tooltip; |
|
|
|
if (tooltipText) { |
|
tooltip.textContent = tooltipText; |
|
tooltip.style.display = 'block'; |
|
tooltip.style.left = e.pageX + 10 + 'px'; |
|
tooltip.style.top = e.pageY + 10 + 'px'; |
|
} |
|
} |
|
|
|
hideTooltip() { |
|
const tooltip = document.getElementById('tooltip'); |
|
tooltip.style.display = 'none'; |
|
} |
|
|
|
isLocation(x, y, type) { |
|
return this.state.locations[type].some(pos => pos.x === x && pos.y === y); |
|
} |
|
|
|
async movePlayer(dx, dy) { |
|
if (this.state.isProcessing) return; |
|
|
|
let newX = this.state.playerPos.x + dx * this.state.moveSpeed; |
|
let newY = this.state.playerPos.y + dy * this.state.moveSpeed; |
|
|
|
|
|
newX = Math.max(0, Math.min(9, newX)); |
|
newY = Math.max(0, Math.min(9, newY)); |
|
|
|
|
|
if (this.isLocation(newX, newY, 'obstacles')) return; |
|
|
|
|
|
if (this.state.drinks >= 3) { |
|
const stumbleChance = (this.state.drinks - 2) * 0.1; |
|
if (Math.random() < stumbleChance) { |
|
const randomDir = Math.random() < 0.5 ? 1 : -1; |
|
if (Math.random() < 0.5) { |
|
newX += randomDir; |
|
} else { |
|
newY += randomDir; |
|
} |
|
newX = Math.max(0, Math.min(9, newX)); |
|
newY = Math.max(0, Math.min(9, newY)); |
|
} |
|
} |
|
|
|
this.state.playerPos = { x: newX, y: newY }; |
|
|
|
|
|
if (this.isLocation(newX, newY, 'bar')) { |
|
this.state.drinks++; |
|
this.state.confidence = Math.min(100, this.state.confidence + 15); |
|
this.state.anxiety = Math.max(0, this.state.anxiety - 10); |
|
this.state.moveSpeed = Math.min(2, 1 + this.state.drinks * 0.2); |
|
await this.handleInput("*Takes another drink from the bar*"); |
|
|
|
|
|
if (this.state.drinks > 5) { |
|
await this.handleInput("*Starting to feel really dizzy...*"); |
|
this.state.confidence = Math.max(0, this.state.confidence - 5); |
|
} |
|
} |
|
|
|
if (this.isLocation(newX, newY, 'sister')) { |
|
this.state.confidence = Math.min(100, this.state.confidence + 20); |
|
this.state.anxiety = Math.max(0, this.state.anxiety - 15); |
|
await this.handleInput("*Talks to sister for encouragement*"); |
|
} |
|
|
|
if (this.isLocation(newX, newY, 'dj')) { |
|
this.state.confidence = Math.min(100, this.state.confidence + 10); |
|
this.state.anxiety = Math.max(0, this.state.anxiety - 5); |
|
await this.handleInput("*Vibing to the music near the DJ*"); |
|
} |
|
|
|
if (this.isLocation(newX, newY, 'girl')) { |
|
if (this.state.confidence >= 70 && this.state.anxiety <= 50) { |
|
await this.handleInput("*Finally gathered the courage to talk to her!*"); |
|
this.gameWon(); |
|
} else { |
|
await this.handleInput("*Gets too nervous and quickly walks away*"); |
|
this.state.playerPos = { |
|
x: Math.max(0, newX - 2), |
|
y: Math.max(0, newY - 2) |
|
}; |
|
this.state.anxiety += 15; |
|
this.state.confidence = Math.max(0, this.state.confidence - 10); |
|
} |
|
} |
|
|
|
|
|
this.state.time = new Date(this.state.time.getTime() + 2 * 60000); |
|
|
|
this.updateStats(); |
|
this.initializeGrid(); |
|
} |
|
|
|
async handleInput(userInput) { |
|
if (this.state.isProcessing) return; |
|
this.state.isProcessing = true; |
|
|
|
try { |
|
if (!userInput.trim()) throw new Error("Please enter some text"); |
|
|
|
this.addMessage(userInput, 'wingman'); |
|
this.addLoadingMessage(); |
|
|
|
const currentState = `Current state: Confidence: ${this.state.confidence}%, Anxiety: ${this.state.anxiety}%, Drinks: ${this.state.drinks}, Location: ${this.getLocationDescription()}`; |
|
|
|
this.context.push({ |
|
role: 'user', |
|
content: `${userInput}\n\n${currentState}` |
|
}); |
|
|
|
const response = await this.callMistralAPI(); |
|
|
|
this.removeLoadingMessage(); |
|
this.addMessage(response, 'shyguy'); |
|
|
|
this.context.push({ |
|
role: 'assistant', |
|
content: response |
|
}); |
|
|
|
|
|
if (this.context.length > 10) { |
|
this.context = [ |
|
this.context[0], |
|
...this.context.slice(-4) |
|
]; |
|
} |
|
} catch (error) { |
|
this.removeLoadingMessage(); |
|
this.addMessage(`Error: ${error.message}`, 'error'); |
|
console.error('Error:', error); |
|
} finally { |
|
this.state.isProcessing = false; |
|
} |
|
} |
|
|
|
getLocationDescription() { |
|
const { x, y } = this.state.playerPos; |
|
if (this.isLocation(x, y, 'bar')) return 'at the bar'; |
|
if (this.isLocation(x, y, 'dj')) return 'near the DJ'; |
|
if (this.isLocation(x, y, 'sister')) return 'with sister'; |
|
if (this.isLocation(x, y, 'girl')) return 'near the girl'; |
|
return 'in the room'; |
|
} |
|
|
|
async callMistralAPI() { |
|
try { |
|
const response = await fetch('https://api.mistral.ai/v1/chat/completions', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
'Authorization': `Bearer ${this.apiKey}` |
|
}, |
|
body: JSON.stringify({ |
|
model: 'mistral-large-latest', |
|
messages: this.context, |
|
max_tokens: 150, |
|
temperature: 0.7 |
|
}) |
|
}); |
|
|
|
if (!response.ok) { |
|
const error = await response.json(); |
|
throw new Error(error.error?.message || 'API request failed'); |
|
} |
|
|
|
const data = await response.json(); |
|
return data.choices[0].message.content; |
|
} catch (error) { |
|
if (error.message.includes('API key')) { |
|
throw new Error('Invalid API key. Please check your API key and try again.'); |
|
} |
|
throw new Error('Failed to get response from AI. Please try again.'); |
|
} |
|
} |
|
|
|
gameWon() { |
|
this.addMessage("Congratulations! You successfully talked to her, and she seems interested! The party wasn't so scary after all.", 'wingman'); |
|
this.state.hasSpokenToGirl = true; |
|
|
|
const winScreen = document.createElement('div'); |
|
winScreen.className = 'win-screen'; |
|
winScreen.innerHTML = ` |
|
<h2>You did it!</h2> |
|
<p>Final Stats:</p> |
|
<p>Confidence: ${this.state.confidence}%</p> |
|
<p>Anxiety: ${this.state.anxiety}%</p> |
|
<p>Drinks: ${this.state.drinks}</p> |
|
<p>Time taken: ${this.getTimeDifference()}</p> |
|
<button onclick="location.reload()">Play Again</button> |
|
`; |
|
document.body.appendChild(winScreen); |
|
} |
|
|
|
getTimeDifference() { |
|
const startTime = new Date(2024, 0, 1, 20, 0); |
|
const timeDiff = this.state.time - startTime; |
|
const minutes = Math.floor(timeDiff / 60000); |
|
return `${minutes} minutes`; |
|
} |
|
|
|
addMessage(text, type) { |
|
const chat = document.getElementById('chat-container'); |
|
const messageDiv = document.createElement('div'); |
|
messageDiv.className = `message ${type}`; |
|
messageDiv.textContent = text; |
|
chat.appendChild(messageDiv); |
|
chat.scrollTop = chat.scrollHeight; |
|
} |
|
|
|
addLoadingMessage() { |
|
const chat = document.getElementById('chat-container'); |
|
const loadingDiv = document.createElement('div'); |
|
loadingDiv.className = 'message shyguy typing'; |
|
loadingDiv.id = 'loading-message'; |
|
loadingDiv.textContent = 'Thinking...'; |
|
chat.appendChild(loadingDiv); |
|
chat.scrollTop = chat.scrollHeight; |
|
} |
|
|
|
removeLoadingMessage() { |
|
const loadingMessage = document.getElementById('loading-message'); |
|
if (loadingMessage) { |
|
loadingMessage.remove(); |
|
} |
|
} |
|
|
|
updateStats() { |
|
document.getElementById('confidence').textContent = this.state.confidence; |
|
document.getElementById('anxiety').textContent = this.state.anxiety; |
|
document.getElementById('drinks').textContent = this.state.drinks; |
|
document.getElementById('time').textContent = |
|
this.state.time.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); |
|
} |
|
} |
|
|
|
let game; |
|
|
|
function initializeGame() { |
|
const apiKey = document.getElementById('api-key').value.trim(); |
|
if (!apiKey) { |
|
alert('Please enter your Mistral API key'); |
|
return; |
|
} |
|
|
|
document.getElementById('api-key-container').style.display = 'none'; |
|
document.getElementById('game-content').style.display = 'block'; |
|
|
|
game = new ShyGuySimulator(apiKey); |
|
} |
|
|
|
async function handleUserInput() { |
|
if (!game) return; |
|
|
|
const input = document.getElementById('user-input'); |
|
const text = input.value.trim(); |
|
if (text) { |
|
await game.handleInput(text); |
|
input.value = ''; |
|
} |
|
} |
|
|
|
async function move(dx, dy) { |
|
if (game) { |
|
await game.movePlayer(dx, dy); |
|
} |
|
} |
|
|
|
|
|
document.addEventListener('keydown', async (e) => { |
|
if (!game) return; |
|
|
|
switch(e.key) { |
|
case 'ArrowUp': |
|
await move(0, -1); |
|
break; |
|
case 'ArrowDown': |
|
await move(0, 1); |
|
break; |
|
case 'ArrowLeft': |
|
await move(-1, 0); |
|
break; |
|
case 'ArrowRight': |
|
await move(1, 0); |
|
break; |
|
} |
|
}); |
|
|
|
|
|
document.getElementById('user-input')?.addEventListener('keypress', function(e) { |
|
if (e.key === 'Enter') { |
|
handleUserInput(); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |