|
<!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 - Grid 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: 1fr 400px; |
|
gap: 20px; |
|
} |
|
|
|
#game-container { |
|
background-color: #2a2a2a; |
|
padding: 20px; |
|
border-radius: 8px; |
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5); |
|
} |
|
|
|
.grid-container { |
|
display: grid; |
|
grid-template-columns: repeat(8, 1fr); |
|
gap: 2px; |
|
background-color: #333; |
|
padding: 10px; |
|
border-radius: 4px; |
|
margin-bottom: 20px; |
|
} |
|
|
|
.grid-cell { |
|
aspect-ratio: 1; |
|
background-color: #4a4a4a; |
|
border-radius: 2px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
font-size: 20px; |
|
} |
|
|
|
.chat-container { |
|
height: 400px; |
|
overflow-y: auto; |
|
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; } |
|
|
|
#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; |
|
} |
|
|
|
#stats { |
|
display: grid; |
|
grid-template-columns: repeat(3, 1fr); |
|
gap: 10px; |
|
margin-bottom: 20px; |
|
} |
|
|
|
.stat-box { |
|
background-color: #333; |
|
padding: 10px; |
|
border-radius: 4px; |
|
text-align: center; |
|
} |
|
|
|
#api-key-container { |
|
margin-bottom: 20px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="game-container"> |
|
<h1>Shy Guy Simulator - Grid 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"> |
|
<div class="stat-box">Confidence: <span id="confidence">0</span>%</div> |
|
<div class="stat-box">Drinks: <span id="drinks">0</span></div> |
|
<div class="stat-box">Time: <span id="time">8:00 PM</span></div> |
|
</div> |
|
|
|
<div class="game-layout"> |
|
<div class="grid-container" id="grid"></div> |
|
<div class="chat-section"> |
|
<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> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
class ShyGuySimulator { |
|
constructor(apiKey) { |
|
this.apiKey = apiKey; |
|
this.state = { |
|
confidence: 0, |
|
drinks: 0, |
|
time: new Date(2024, 0, 1, 20, 0), |
|
position: { x: 0, y: 0 }, |
|
isProcessing: false |
|
}; |
|
|
|
this.targetPosition = { x: 7, y: 7 }; |
|
this.gridSize = 8; |
|
|
|
|
|
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 an 8x8 grid. You start at (0,0), and the girl you like is at (7,7). |
|
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` |
|
}]; |
|
|
|
this.initialize(); |
|
} |
|
|
|
initialize() { |
|
this.setupGrid(); |
|
this.addMessage("Let's help you talk to her! I'll be your wingman tonight.", 'wingman'); |
|
this.addMessage("I... I don't know about this. Maybe I should just go home...", 'shyguy'); |
|
this.updateStats(); |
|
this.updateGrid(); |
|
} |
|
|
|
setupGrid() { |
|
const grid = document.getElementById('grid'); |
|
grid.innerHTML = ''; |
|
for (let y = 0; y < this.gridSize; y++) { |
|
for (let x = 0; x < this.gridSize; x++) { |
|
const cell = document.createElement('div'); |
|
cell.className = 'grid-cell'; |
|
cell.id = `cell-${x}-${y}`; |
|
grid.appendChild(cell); |
|
} |
|
} |
|
} |
|
|
|
updateGrid() { |
|
|
|
document.querySelectorAll('.grid-cell').forEach(cell => { |
|
cell.textContent = ''; |
|
}); |
|
|
|
|
|
const shyGuyCell = document.getElementById(`cell-${this.state.position.x}-${this.state.position.y}`); |
|
if (shyGuyCell) shyGuyCell.textContent = '😳'; |
|
|
|
|
|
const girlCell = document.getElementById(`cell-${this.targetPosition.x}-${this.targetPosition.y}`); |
|
if (girlCell) girlCell.textContent = '👧'; |
|
} |
|
|
|
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}% |
|
- Drinks: ${this.state.drinks} |
|
- Position: (${this.state.position.x}, ${this.state.position.y}) |
|
- Distance to girl: ${this.calculateDistance()}`; |
|
|
|
this.context.push({ |
|
role: 'user', |
|
content: `${userInput}\n\n${currentState}` |
|
}); |
|
|
|
const response = await this.callMistralAPI(); |
|
|
|
this.removeLoadingMessage(); |
|
this.processAIResponse(response); |
|
|
|
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; |
|
} |
|
} |
|
|
|
calculateDistance() { |
|
const dx = this.targetPosition.x - this.state.position.x; |
|
const dy = this.targetPosition.y - this.state.position.y; |
|
return Math.sqrt(dx * dx + dy * dy); |
|
} |
|
|
|
processAIResponse(responseText) { |
|
try { |
|
|
|
const jsonMatch = responseText.match(/\{[\s\S]*\}/); |
|
const responseData = jsonMatch ? JSON.parse(jsonMatch[0]) : JSON.parse(responseText); |
|
|
|
|
|
this.addMessage(responseData.dialogue, 'shyguy'); |
|
|
|
|
|
const newX = Math.max(0, Math.min(7, this.state.position.x + responseData.movement.x)); |
|
const newY = Math.max(0, Math.min(7, this.state.position.y + responseData.movement.y)); |
|
this.state.position = { x: newX, y: newY }; |
|
|
|
|
|
const emotionConfidenceMap = { |
|
'anxious': -5, |
|
'nervous': 0, |
|
'slightly_confident': 5, |
|
'confident': 10 |
|
}; |
|
this.state.confidence = Math.max(0, Math.min(100, |
|
this.state.confidence + (emotionConfidenceMap[responseData.emotion] || 0) |
|
)); |
|
|
|
|
|
this.updateGrid(); |
|
this.updateStats(); |
|
|
|
|
|
if (this.state.position.x === this.targetPosition.x && |
|
this.state.position.y === this.targetPosition.y) { |
|
this.handleWin(); |
|
} |
|
} catch (error) { |
|
console.error('Error processing AI response:', error); |
|
this.addMessage("I... uh... *mumbles something incoherent*", 'shyguy'); |
|
} |
|
} |
|
|
|
handleWin() { |
|
this.addMessage("Oh my god, I actually made it! Hi... I've been wanting to talk to you...", 'shyguy'); |
|
this.addMessage("Congratulations! You've helped Shy Guy reach his goal!", 'wingman'); |
|
|
|
document.getElementById('user-input').disabled = true; |
|
} |
|
|
|
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.'); |
|
} |
|
} |
|
|
|
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('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 = ''; |
|
} |
|
} |
|
|
|
document.getElementById('user-input')?.addEventListener('keypress', function(e) { |
|
if (e.key === 'Enter') { |
|
handleUserInput(); |
|
} |
|
}); |
|
</script> |
|
</body> |
|
</html> |