| // The HTML and CSS remain the same until the ShyGuySimulator class | |
| 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(); | |
| } | |
| // Add autonomous movement | |
| startAutonomousMovement() { | |
| this.movementInterval = setInterval(async () => { | |
| if (!this.state.isProcessing && !this.state.hasSpokenToGirl) { | |
| await this.getNextMove(); | |
| } | |
| }, 5000); // Move every 5 seconds | |
| } | |
| stopAutonomousMovement() { | |
| if (this.movementInterval) { | |
| clearInterval(this.movementInterval); | |
| } | |
| } | |
| async getNextMove() { | |
| try { | |
| const response = await this.callMistralAPI(); | |
| let parsedResponse; | |
| try { | |
| // Try to parse the response as JSON | |
| parsedResponse = JSON.parse(response); | |
| } catch (e) { | |
| // If parsing fails, try to extract JSON from the text | |
| const jsonMatch = response.match(/\{[\s\S]*\}/); | |
| if (jsonMatch) { | |
| parsedResponse = JSON.parse(jsonMatch[0]); | |
| } else { | |
| throw new Error('Could not parse LLM response'); | |
| } | |
| } | |
| // Update emotional state | |
| this.state.lastEmotion = parsedResponse.emotion; | |
| // Add the dialogue | |
| this.addMessage(parsedResponse.dialogue, 'shyguy'); | |
| // Execute the movement | |
| if (parsedResponse.movement) { | |
| await this.movePlayer( | |
| parsedResponse.movement.x, | |
| parsedResponse.movement.y | |
| ); | |
| } | |
| } catch (error) { | |
| console.error('Error getting next move:', error); | |
| } | |
| } | |
| 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}, | |
| Position: (${this.state.playerPos.x},${this.state.playerPos.y}), | |
| Location: ${this.getLocationDescription()}`; | |
| this.context.push({ | |
| role: 'user', | |
| content: `${userInput}\n\n${currentState}` | |
| }); | |
| await this.getNextMove(); | |
| } catch (error) { | |
| this.removeLoadingMessage(); | |
| this.addMessage(`Error: ${error.message}`, 'error'); | |
| console.error('Error:', error); | |
| } finally { | |
| this.state.isProcessing = false; | |
| } | |
| } | |
| async movePlayer(dx, dy) { | |
| let newX = this.state.playerPos.x + dx * this.state.moveSpeed; | |
| let newY = this.state.playerPos.y + dy * this.state.moveSpeed; | |
| // Check boundaries | |
| newX = Math.max(0, Math.min(9, newX)); | |
| newY = Math.max(0, Math.min(9, newY)); | |
| // Check obstacles | |
| if (this.isLocation(newX, newY, 'obstacles')) { | |
| return; | |
| } | |
| // Random stumble when drunk | |
| 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)); | |
| } | |
| } | |
| // Update position | |
| this.state.playerPos = { x: newX, y: newY }; | |
| // Handle location interactions | |
| await this.handleLocationInteraction(newX, newY); | |
| // Advance time | |
| this.state.time = new Date(this.state.time.getTime() + 2 * 60000); | |
| this.updateStats(); | |
| this.initializeGrid(); | |
| } | |
| async handleLocationInteraction(x, y) { | |
| if (this.isLocation(x, y, '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); | |
| if (this.state.drinks > 5) { | |
| this.state.confidence = Math.max(0, this.state.confidence - 5); | |
| await this.handleInput("*Starting to feel really dizzy...*"); | |
| } | |
| } | |
| if (this.isLocation(x, y, 'sister')) { | |
| this.state.confidence = Math.min(100, this.state.confidence + 20); | |
| this.state.anxiety = Math.max(0, this.state.anxiety - 15); | |
| } | |
| if (this.isLocation(x, y, 'dj')) { | |
| this.state.confidence = Math.min(100, this.state.confidence + 10); | |
| this.state.anxiety = Math.max(0, this.state.anxiety - 5); | |
| } | |
| if (this.isLocation(x, y, 'girl')) { | |
| if (this.state.confidence >= 70 && this.state.anxiety <= 50) { | |
| this.gameWon(); | |
| } else { | |
| this.state.playerPos = { | |
| x: Math.max(0, x - 2), | |
| y: Math.max(0, y - 2) | |
| }; | |
| this.state.anxiety += 15; | |
| this.state.confidence = Math.max(0, this.state.confidence - 10); | |
| } | |
| } | |
| } | |
| gameWon() { | |
| this.stopAutonomousMovement(); | |
| // Rest of gameWon implementation remains the same | |
| } | |
| // Rest of the class implementation remains the same | |
| } | |
| // The rest of the code (event listeners, etc.) remains the same |