zshashz commited on
Commit
8bbb94c
·
1 Parent(s): 57d720e
Files changed (1) hide show
  1. index.html +525 -438
index.html CHANGED
@@ -43,7 +43,7 @@
43
  word-wrap: break-word;
44
  }
45
 
46
- .wingman {
47
  background-color: #2c5282;
48
  margin-right: 20%;
49
  }
@@ -58,42 +58,54 @@
58
  text-align: center;
59
  }
60
 
61
- .grid-container {
62
- display: grid;
63
- grid-template-columns: repeat(10, 1fr);
64
- gap: 2px;
65
- background-color: #1a1a1a;
66
- padding: 10px;
67
- margin: 20px 0;
68
  border-radius: 4px;
69
- aspect-ratio: 1;
70
  }
71
 
72
- .grid-cell {
73
- aspect-ratio: 1;
74
- background-color: #2d3748;
75
- border-radius: 2px;
76
- display: flex;
77
- align-items: center;
78
- justify-content: center;
79
  position: relative;
80
- cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
81
  }
82
 
83
  #player {
84
  background-color: #4299e1;
85
- width: 80%;
86
- height: 80%;
87
- border-radius: 50%;
 
 
 
 
88
  position: absolute;
89
- transition: all 0.3s ease;
 
 
90
  }
91
 
92
- .bar { background-color: #744210 !important; }
93
- .dj { background-color: #2c5282 !important; }
94
- .girl { background-color: #d53f8c !important; }
95
- .sister { background-color: #805ad5 !important; }
96
- .obstacle { background-color: #4a5568 !important; }
97
 
98
  .legend {
99
  display: flex;
@@ -172,7 +184,7 @@
172
  }
173
 
174
  .tooltip {
175
- position: absolute;
176
  background: rgba(0, 0, 0, 0.8);
177
  padding: 5px;
178
  border-radius: 4px;
@@ -193,6 +205,13 @@
193
  text-align: center;
194
  z-index: 1000;
195
  }
 
 
 
 
 
 
 
196
  </style>
197
  </head>
198
  <body>
@@ -219,15 +238,25 @@
219
  <input type="text" id="user-input" placeholder="Talk to your shy friend...">
220
  <button onclick="handleUserInput()">Send</button>
221
  </div>
 
 
 
 
222
  </div>
223
 
224
  <div class="game-side">
225
- <div class="grid-container" id="party-grid"></div>
 
 
226
 
227
  <div class="legend">
228
  <div class="legend-item">
229
  <div class="legend-color" style="background-color: #4299e1;"></div>
230
- <span>You</span>
 
 
 
 
231
  </div>
232
  <div class="legend-item">
233
  <div class="legend-color" style="background-color: #744210;"></div>
@@ -253,441 +282,499 @@
253
 
254
  <div class="tooltip" id="tooltip"></div>
255
 
256
- <script>
257
- class ShyGuySimulator {
258
- constructor(apiKey) {
259
- this.apiKey = apiKey;
260
- this.state = {
261
- confidence: 0,
262
- anxiety: 100,
263
- drinks: 0,
264
- time: new Date(2024, 0, 1, 20, 0),
265
- playerPos: { x: 0, y: 9 },
266
- isProcessing: false,
267
- hasSpokenToGirl: false,
268
- emotion: 'anxious',
269
- locations: {
270
- bar: [{ x: 8, y: 1 }, { x: 9, y: 1 }],
271
- dj: [{ x: 4, y: 0 }, { x: 5, y: 0 }],
272
- girl: [{ x: 9, y: 8 }],
273
- sister: [{ x: 2, y: 5 }],
274
- obstacles: [
275
- { x: 3, y: 3 }, { x: 4, y: 3 },
276
- { x: 6, y: 6 }, { x: 7, y: 6 }
277
- ]
278
- }
279
- };
280
-
281
- this.context = [{
282
- role: 'system',
283
- content: `You are roleplaying as a shy guy at a party, providing both dialogue and movement decisions.
284
- The party is on a 10x10 grid. You start at (0,9), and the girl you like is at (9,8).
285
- ALWAYS structure your responses in this exact format:
286
- {
287
- "dialogue": "Your spoken response here",
288
- "movement": {
289
- "x": number (-1, 0, or 1 for movement),
290
- "y": number (-1, 0, or 1 for movement)
291
- },
292
- "emotion": "anxious|nervous|slightly_confident|confident"
293
- }
294
-
295
- Rules:
296
- 1. When drinks > 2, be more likely to move toward the girl
297
- 2. When confidence < 30, prefer to move away or stay still
298
- 3. Keep dialogue natural and brief (1-2 sentences)
299
- 4. Movement should reflect emotional state
300
- 5. Consider current position and avoid obstacles
301
- 6. React to encouragement from the wingman
302
- 7. If near the girl and confidence/drinks are low, move away`
303
- }];
304
-
305
- this.initialize();
306
- this.initializeGrid();
307
- }
308
-
309
- initialize() {
310
- 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');
311
- this.addMessage("I... I don't know about this. Maybe I should just go home...", 'shyguy');
312
- this.updateStats();
313
- }
314
-
315
- async processLLMResponse(response) {
316
- try {
317
- // Extract the JSON from the response
318
- const jsonMatch = response.match(/\{[\s\S]*\}/);
319
- if (!jsonMatch) {
320
- throw new Error("Invalid response format");
321
- }
322
-
323
- const parsedResponse = JSON.parse(jsonMatch[0]);
324
-
325
- // Validate the response format
326
- if (!parsedResponse.dialogue || !parsedResponse.movement || !parsedResponse.emotion) {
327
- throw new Error("Missing required fields in response");
328
- }
329
-
330
- // Store the emotion state
331
- this.state.emotion = parsedResponse.emotion;
332
-
333
- // Apply the movement
334
- await this.movePlayer(
335
- parsedResponse.movement.x,
336
- parsedResponse.movement.y
337
- );
338
-
339
- // Update player state based on emotion
340
- this.updateEmotionalState(parsedResponse.emotion);
341
-
342
- return parsedResponse.dialogue;
343
- } catch (error) {
344
- console.error("Error processing LLM response:", error);
345
- return "I'm not sure what to do right now...";
346
- }
347
- }
348
-
349
- updateEmotionalState(emotion) {
350
- const emotionEffects = {
351
- 'anxious': { confidence: -5, anxiety: 5 },
352
- 'nervous': { confidence: -2, anxiety: 2 },
353
- 'slightly_confident': { confidence: 5, anxiety: -5 },
354
- 'confident': { confidence: 10, anxiety: -10 }
355
- };
356
-
357
- const effect = emotionEffects[emotion] || { confidence: 0, anxiety: 0 };
358
-
359
- this.state.confidence = Math.max(0, Math.min(100, this.state.confidence + effect.confidence));
360
- this.state.anxiety = Math.max(0, Math.min(100, this.state.anxiety + effect.anxiety));
361
- }
362
-
363
- async handleInput(userInput) {
364
- if (this.state.isProcessing) return;
365
- this.state.isProcessing = true;
366
-
367
- try {
368
- if (!userInput.trim()) throw new Error("Please enter some text");
369
-
370
- this.addMessage(userInput, 'wingman');
371
- this.addLoadingMessage();
372
-
373
- const currentState = `Current state:
374
- Confidence: ${this.state.confidence}%,
375
- Anxiety: ${this.state.anxiety}%,
376
- Drinks: ${this.state.drinks},
377
- Location: ${this.getLocationDescription()},
378
- Position: (${this.state.playerPos.x},${this.state.playerPos.y}),
379
- Emotion: ${this.state.emotion}`;
380
-
381
- this.context.push({
382
- role: 'user',
383
- content: `${userInput}\n\n${currentState}`
384
- });
385
-
386
- const llmResponse = await this.callMistralAPI();
387
- const dialogue = await this.processLLMResponse(llmResponse);
388
-
389
- this.removeLoadingMessage();
390
- this.addMessage(dialogue, 'shyguy');
391
-
392
- this.context.push({
393
- role: 'assistant',
394
- content: llmResponse
395
- });
396
-
397
- if (this.context.length > 10) {
398
- this.context = [
399
- this.context[0],
400
- ...this.context.slice(-4)
401
- ];
402
- }
403
- } catch (error) {
404
- this.removeLoadingMessage();
405
- this.addMessage(`Error: ${error.message}`, 'error');
406
- console.error('Error:', error);
407
- } finally {
408
- this.state.isProcessing = false;
409
- }
410
- }
411
-
412
- async movePlayer(dx, dy) {
413
- let newX = this.state.playerPos.x + dx;
414
- let newY = this.state.playerPos.y + dy;
415
-
416
- // Check boundaries
417
- newX = Math.max(0, Math.min(9, newX));
418
- newY = Math.max(0, Math.min(9, newY));
419
-
420
- // Check obstacles
421
- if (this.isLocation(newX, newY, 'obstacles')) return;
422
-
423
- // Random stumble when drunk
424
- if (this.state.drinks >= 3) {
425
- const stumbleChance = (this.state.drinks - 2) * 0.1;
426
- if (Math.random() < stumbleChance) {
427
- const randomDir = Math.random() < 0.5 ? 1 : -1;
428
- if (Math.random() < 0.5) {
429
- newX += randomDir;
430
- } else {
431
- newY += randomDir;
432
- }
433
- newX = Math.max(0, Math.min(9, newX));
434
- newY = Math.max(0, Math.min(9, newY));
435
  }
436
- }
437
 
438
- // Update position
439
- this.state.playerPos = { x: newX, y: newY };
440
-
441
- // Handle location interactions
442
- await this.handleLocationInteraction(newX, newY);
443
 
444
- // Advance time
445
- this.state.time = new Date(this.state.time.getTime() + 2 * 60000); // 2 minutes per move
446
-
447
- this.updateStats();
448
- this.initializeGrid();
449
- }
450
-
451
- async handleLocationInteraction(x, y) {
452
- if (this.isLocation(x, y, 'bar')) {
453
- this.state.drinks++;
454
- this.state.confidence = Math.min(100, this.state.confidence + 15);
455
- this.state.anxiety = Math.max(0, this.state.anxiety - 10);
456
- await this.addMessage("*Takes another drink from the bar*", 'shyguy');
457
-
458
- if (this.state.drinks > 5) {
459
- this.state.confidence = Math.max(0, this.state.confidence - 5);
460
- await this.addMessage("*Starting to feel really dizzy...*", 'shyguy');
 
 
 
 
 
 
 
461
  }
462
- }
463
-
464
- if (this.isLocation(x, y, 'sister')) {
465
- this.state.confidence = Math.min(100, this.state.confidence + 20);
466
- this.state.anxiety = Math.max(0, this.state.anxiety - 15);
467
- await this.addMessage("*Gets some encouragement from sister*", 'shyguy');
468
- }
469
-
470
- if (this.isLocation(x, y, 'dj')) {
471
- this.state.confidence = Math.min(100, this.state.confidence + 10);
472
- this.state.anxiety = Math.max(0, this.state.anxiety - 5);
473
- await this.addMessage("*Vibing to the music*", 'shyguy');
474
- }
475
-
476
- if (this.isLocation(x, y, 'girl')) {
477
- if (this.state.confidence >= 70 && this.state.anxiety <= 50) {
478
- await this.addMessage("*Finally gathered the courage to talk to her!*", 'shyguy');
479
- this.gameWon();
480
- } else {
481
- await this.addMessage("*Gets too nervous and quickly walks away*", 'shyguy');
482
- this.state.playerPos = {
483
- x: Math.max(0, x - 2),
484
- y: Math.max(0, y - 2)
485
- };
486
- this.state.anxiety += 15;
487
- this.state.confidence = Math.max(0, this.state.confidence - 10);
 
 
488
  }
489
- }
490
- }
491
-
492
- async callMistralAPI() {
493
- try {
494
- const response = await fetch('https://api.mistral.ai/v1/chat/completions', {
495
- method: 'POST',
496
- headers: {
497
- 'Content-Type': 'application/json',
498
- 'Authorization': `Bearer ${this.apiKey}`
499
- },
500
- body: JSON.stringify({
501
- model: 'mistral-large-latest',
502
- messages: this.context,
503
- max_tokens: 150,
504
- temperature: 0.7
505
- })
506
- });
507
-
508
- if (!response.ok) {
509
- const error = await response.json();
510
- throw new Error(error.error?.message || 'API request failed');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  }
512
 
513
- const data = await response.json();
514
- return data.choices[0].message.content;
515
- } catch (error) {
516
- if (error.message.includes('API key')) {
517
- throw new Error('Invalid API key. Please check your API key and try again.');
518
  }
519
- throw new Error('Failed to get response from AI. Please try again.');
520
- }
521
- }
522
 
523
- initializeGrid() {
524
- const grid = document.getElementById('party-grid');
525
- grid.innerHTML = '';
526
-
527
- for (let y = 0; y < 10; y++) {
528
- for (let x = 0; x < 10; x++) {
529
- const cell = document.createElement('div');
530
- cell.className = 'grid-cell';
531
- cell.dataset.x = x;
532
- cell.dataset.y = y;
533
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  if (this.isLocation(x, y, 'bar')) {
535
- cell.classList.add('bar');
536
- cell.dataset.tooltip = "Bar - Get liquid courage";
 
 
 
 
 
 
 
537
  }
 
 
 
 
 
 
 
538
  if (this.isLocation(x, y, 'dj')) {
539
- cell.classList.add('dj');
540
- cell.dataset.tooltip = "DJ - Vibe to the music";
 
541
  }
 
542
  if (this.isLocation(x, y, 'girl')) {
543
- cell.classList.add('girl');
544
- cell.dataset.tooltip = "The girl you like";
 
 
 
 
 
 
 
 
 
 
545
  }
546
- if (this.isLocation(x, y, 'sister')) {
547
- cell.classList.add('sister');
548
- cell.dataset.tooltip = "Your sister - Get some encouragement";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
549
  }
550
- if (this.isLocation(x, y, 'obstacles')) {
551
- cell.classList.add('obstacle');
552
- cell.dataset.tooltip = "Can't walk here";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
553
  }
 
 
 
 
 
 
 
 
 
 
 
554
 
555
- if (x === this.state.playerPos.x && y === this.state.playerPos.y) {
556
- const player = document.createElement('div');
557
- player.id = 'player';
558
- if (this.state.drinks >= 3) player.classList.add('drunk-effect');
559
- cell.appendChild(player);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
560
  }
 
 
 
 
 
561
 
562
- cell.addEventListener('mouseover', this.showTooltip);
563
- cell.addEventListener('mouseout', this.hideTooltip);
 
 
 
 
 
 
 
 
564
 
565
- grid.appendChild(cell);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
566
  }
567
  }
568
- }
569
 
570
- showTooltip(e) {
571
- const tooltip = document.getElementById('tooltip');
572
- const tooltipText = e.target.dataset.tooltip;
573
-
574
- if (tooltipText) {
575
- tooltip.textContent = tooltipText;
576
- tooltip.style.display = 'block';
577
- tooltip.style.left = e.pageX + 10 + 'px';
578
- tooltip.style.top = e.pageY + 10 + 'px';
 
 
 
 
579
  }
580
- }
581
-
582
- hideTooltip() {
583
- const tooltip = document.getElementById('tooltip');
584
- tooltip.style.display = 'none';
585
- }
586
-
587
- isLocation(x, y, type) {
588
- return this.state.locations[type].some(pos => pos.x === x && pos.y === y);
589
- }
590
-
591
- getLocationDescription() {
592
- const { x, y } = this.state.playerPos;
593
- if (this.isLocation(x, y, 'bar')) return 'at the bar';
594
- if (this.isLocation(x, y, 'dj')) return 'near the DJ';
595
- if (this.isLocation(x, y, 'sister')) return 'with sister';
596
- if (this.isLocation(x, y, 'girl')) return 'near the girl';
597
- return 'in the room';
598
- }
599
-
600
- gameWon() {
601
- this.addMessage("Congratulations! You successfully talked to her, and she seems interested! The party wasn't so scary after all.", 'wingman');
602
- this.state.hasSpokenToGirl = true;
603
-
604
- const winScreen = document.createElement('div');
605
- winScreen.className = 'win-screen';
606
- winScreen.innerHTML = `
607
- <h2>You did it!</h2>
608
- <p>Final Stats:</p>
609
- <p>Confidence: ${this.state.confidence}%</p>
610
- <p>Anxiety: ${this.state.anxiety}%</p>
611
- <p>Drinks: ${this.state.drinks}</p>
612
- <p>Time taken: ${this.getTimeDifference()}</p>
613
- <button onclick="location.reload()">Play Again</button>
614
- `;
615
- document.body.appendChild(winScreen);
616
- }
617
-
618
- getTimeDifference() {
619
- const startTime = new Date(2024, 0, 1, 20, 0);
620
- const timeDiff = this.state.time - startTime;
621
- const minutes = Math.floor(timeDiff / 60000);
622
- return `${minutes} minutes`;
623
- }
624
-
625
- addMessage(text, type) {
626
- const chat = document.getElementById('chat-container');
627
- const messageDiv = document.createElement('div');
628
- messageDiv.className = `message ${type}`;
629
- messageDiv.textContent = text;
630
- chat.appendChild(messageDiv);
631
- chat.scrollTop = chat.scrollHeight;
632
- }
633
-
634
- addLoadingMessage() {
635
- const chat = document.getElementById('chat-container');
636
- const loadingDiv = document.createElement('div');
637
- loadingDiv.className = 'message shyguy typing';
638
- loadingDiv.id = 'loading-message';
639
- loadingDiv.textContent = 'Thinking...';
640
- chat.appendChild(loadingDiv);
641
- chat.scrollTop = chat.scrollHeight;
642
- }
643
-
644
- removeLoadingMessage() {
645
- const loadingMessage = document.getElementById('loading-message');
646
- if (loadingMessage) {
647
- loadingMessage.remove();
648
  }
649
- }
650
-
651
- updateStats() {
652
- document.getElementById('confidence').textContent = this.state.confidence;
653
- document.getElementById('anxiety').textContent = this.state.anxiety;
654
- document.getElementById('drinks').textContent = this.state.drinks;
655
- document.getElementById('time').textContent =
656
- this.state.time.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
657
- }
658
- }
659
-
660
- let game;
661
-
662
- function initializeGame() {
663
- const apiKey = document.getElementById('api-key').value.trim();
664
- if (!apiKey) {
665
- alert('Please enter your Mistral API key');
666
- return;
667
- }
668
-
669
- document.getElementById('api-key-container').style.display = 'none';
670
- document.getElementById('game-content').style.display = 'block';
671
-
672
- game = new ShyGuySimulator(apiKey);
673
- }
674
-
675
- async function handleUserInput() {
676
- if (!game) return;
677
-
678
- const input = document.getElementById('user-input');
679
- const text = input.value.trim();
680
- if (text) {
681
- await game.handleInput(text);
682
- input.value = '';
683
- }
684
- }
685
-
686
- document.getElementById('user-input')?.addEventListener('keypress', function(e) {
687
- if (e.key === 'Enter') {
688
- handleUserInput();
689
- }
690
- });
691
- </script>
692
  </body>
693
  </html>
 
43
  word-wrap: break-word;
44
  }
45
 
46
+ .wingman-msg {
47
  background-color: #2c5282;
48
  margin-right: 20%;
49
  }
 
58
  text-align: center;
59
  }
60
 
61
+ .viewport {
62
+ width: 800px;
63
+ height: 800px;
64
+ overflow: hidden;
65
+ position: relative;
66
+ border: 2px solid #4a5568;
 
67
  border-radius: 4px;
 
68
  }
69
 
70
+ .grid-container {
71
+ width: 10240px; /* 512 * 20px */
72
+ height: 10240px;
 
 
 
 
73
  position: relative;
74
+ background-color: #1a1a1a;
75
+ background-image: linear-gradient(#2d3748 1px, transparent 1px),
76
+ linear-gradient(90deg, #2d3748 1px, transparent 1px);
77
+ background-size: 20px 20px;
78
+ }
79
+
80
+ #player, #wingman {
81
+ width: 16px;
82
+ height: 16px;
83
+ border-radius: 50%;
84
+ position: absolute;
85
+ transition: all 0.1s ease;
86
+ z-index: 2;
87
  }
88
 
89
  #player {
90
  background-color: #4299e1;
91
+ }
92
+
93
+ #wingman {
94
+ background-color: #48bb78;
95
+ }
96
+
97
+ .location-marker {
98
  position: absolute;
99
+ width: 20px;
100
+ height: 20px;
101
+ border-radius: 4px;
102
  }
103
 
104
+ .bar { background-color: #744210; }
105
+ .dj { background-color: #2c5282; }
106
+ .girl { background-color: #d53f8c; }
107
+ .sister { background-color: #805ad5; }
108
+ .obstacle { background-color: #4a5568; }
109
 
110
  .legend {
111
  display: flex;
 
184
  }
185
 
186
  .tooltip {
187
+ position: fixed;
188
  background: rgba(0, 0, 0, 0.8);
189
  padding: 5px;
190
  border-radius: 4px;
 
205
  text-align: center;
206
  z-index: 1000;
207
  }
208
+
209
+ .controls-info {
210
+ margin-top: 10px;
211
+ padding: 10px;
212
+ background-color: #333;
213
+ border-radius: 4px;
214
+ }
215
  </style>
216
  </head>
217
  <body>
 
238
  <input type="text" id="user-input" placeholder="Talk to your shy friend...">
239
  <button onclick="handleUserInput()">Send</button>
240
  </div>
241
+ <div class="controls-info">
242
+ Use arrow keys to move the wingman (green dot).<br>
243
+ Get close to push the shy guy (blue dot) in the right direction!
244
+ </div>
245
  </div>
246
 
247
  <div class="game-side">
248
+ <div class="viewport">
249
+ <div class="grid-container" id="party-grid"></div>
250
+ </div>
251
 
252
  <div class="legend">
253
  <div class="legend-item">
254
  <div class="legend-color" style="background-color: #4299e1;"></div>
255
+ <span>Shy Guy</span>
256
+ </div>
257
+ <div class="legend-item">
258
+ <div class="legend-color" style="background-color: #48bb78;"></div>
259
+ <span>Wingman (You)</span>
260
  </div>
261
  <div class="legend-item">
262
  <div class="legend-color" style="background-color: #744210;"></div>
 
282
 
283
  <div class="tooltip" id="tooltip"></div>
284
 
285
+ <script>
286
+ class ShyGuySimulator {
287
+ constructor(apiKey) {
288
+ this.apiKey = apiKey;
289
+ this.state = {
290
+ confidence: 0,
291
+ anxiety: 100,
292
+ drinks: 0,
293
+ time: new Date(2024, 0, 1, 20, 0),
294
+ playerPos: { x: 50, y: 450 },
295
+ wingmanPos: { x: 40, y: 450 },
296
+ isProcessing: false,
297
+ hasSpokenToGirl: false,
298
+ emotion: 'anxious',
299
+ locations: {
300
+ bar: [{ x: 450, y: 50 }, { x: 451, y: 50 }],
301
+ dj: [{ x: 250, y: 20 }, { x: 251, y: 20 }],
302
+ girl: [{ x: 450, y: 450 }],
303
+ sister: [{ x: 150, y: 250 }],
304
+ obstacles: [
305
+ { x: 200, y: 200 }, { x: 201, y: 200 },
306
+ { x: 300, y: 300 }, { x: 301, y: 300 }
307
+ ]
308
+ }
309
+ };
310
+
311
+ this.context = [{
312
+ role: 'system',
313
+ content: `You are roleplaying as a shy guy at a party, providing both dialogue and movement decisions.
314
+ The party is on a 512x512 grid. Movement values should be between -30 and 30 units per move.
315
+ ALWAYS structure your responses in this exact format:
316
+ {
317
+ "dialogue": "Your spoken response here",
318
+ "movement": {
319
+ "x": number (-30 to 30),
320
+ "y": number (-30 to 30)
321
+ },
322
+ "emotion": "anxious|nervous|slightly_confident|confident"
323
+ }
324
+
325
+ Rules:
326
+ 1. When drinks > 2, be more likely to move toward the girl
327
+ 2. When confidence < 30, prefer to move away or stay still
328
+ 3. Keep dialogue natural and brief (1-2 sentences)
329
+ 4. Movement should reflect emotional state
330
+ 5. Consider current position and avoid obstacles
331
+ 6. React to encouragement from the wingman
332
+ 7. If near the girl and confidence/drinks are low, move away`
333
+ }];
334
+
335
+ this.initialize();
336
+ this.initializeGrid();
337
+ this.setupKeyboardControls();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
  }
 
339
 
340
+ initialize() {
341
+ 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-msg');
342
+ this.addMessage("I... I don't know about this. Maybe I should just go home...", 'shyguy');
343
+ this.updateStats();
344
+ }
345
 
346
+ setupKeyboardControls() {
347
+ document.addEventListener('keydown', (e) => {
348
+ const moveDistance = 5;
349
+ let dx = 0, dy = 0;
350
+
351
+ switch(e.key) {
352
+ case 'ArrowLeft':
353
+ dx = -moveDistance;
354
+ break;
355
+ case 'ArrowRight':
356
+ dx = moveDistance;
357
+ break;
358
+ case 'ArrowUp':
359
+ dy = -moveDistance;
360
+ break;
361
+ case 'ArrowDown':
362
+ dy = moveDistance;
363
+ break;
364
+ }
365
+
366
+ if (dx !== 0 || dy !== 0) {
367
+ this.moveWingman(dx, dy);
368
+ }
369
+ });
370
  }
371
+
372
+ moveWingman(dx, dy) {
373
+ let newX = this.state.wingmanPos.x + dx;
374
+ let newY = this.state.wingmanPos.y + dy;
375
+
376
+ // Boundary checks
377
+ newX = Math.max(0, Math.min(511, newX));
378
+ newY = Math.max(0, Math.min(511, newY));
379
+
380
+ // Check for collision with shy guy
381
+ const distanceToPlayer = Math.hypot(
382
+ newX - this.state.playerPos.x,
383
+ newY - this.state.playerPos.y
384
+ );
385
+
386
+ if (distanceToPlayer < 20) { // Push range
387
+ // Calculate push direction
388
+ const pushForce = 10;
389
+ const pushDx = (this.state.playerPos.x - newX) / distanceToPlayer * pushForce;
390
+ const pushDy = (this.state.playerPos.y - newY) / distanceToPlayer * pushForce;
391
+
392
+ // Move shy guy
393
+ this.movePlayer(pushDx, pushDy, true);
394
+ }
395
+
396
+ // Update wingman position
397
+ this.state.wingmanPos = { x: newX, y: newY };
398
+ this.updateCharacterPositions();
399
  }
400
+
401
+ async movePlayer(dx, dy, isPush = false) {
402
+ let newX = this.state.playerPos.x + dx;
403
+ let newY = this.state.playerPos.y + dy;
404
+
405
+ // Boundary checks
406
+ newX = Math.max(0, Math.min(511, newX));
407
+ newY = Math.max(0, Math.min(511, newY));
408
+
409
+ // Check obstacles
410
+ if (this.isLocationNearby(newX, newY, 'obstacles', 15)) {
411
+ return;
412
+ }
413
+
414
+ // Random stumble when drunk
415
+ if (this.state.drinks >= 3 && !isPush) {
416
+ const stumbleChance = (this.state.drinks - 2) * 0.1;
417
+ if (Math.random() < stumbleChance) {
418
+ const randomDir = Math.random() < 0.5 ? 1 : -1;
419
+ if (Math.random() < 0.5) {
420
+ newX += randomDir * 10;
421
+ } else {
422
+ newY += randomDir * 10;
423
+ }
424
+ newX = Math.max(0, Math.min(511, newX));
425
+ newY = Math.max(0, Math.min(511, newY));
426
+ }
427
+ }
428
+
429
+ // Update position
430
+ this.state.playerPos = { x: newX, y: newY };
431
+
432
+ if (!isPush) {
433
+ // Only handle location interactions and time updates for normal movement
434
+ await this.handleLocationInteraction(newX, newY);
435
+ this.state.time = new Date(this.state.time.getTime() + 2 * 60000);
436
+ }
437
+
438
+ this.updateStats();
439
+ this.updateCharacterPositions();
440
  }
441
 
442
+ isLocationNearby(x, y, type, radius = 10) {
443
+ return this.state.locations[type].some(pos =>
444
+ Math.hypot(x - pos.x * 20, y - pos.y * 20) < radius
445
+ );
 
446
  }
 
 
 
447
 
448
+ updateCharacterPositions() {
449
+ const player = document.getElementById('player');
450
+ const wingman = document.getElementById('wingman');
 
 
 
 
 
 
 
451
 
452
+ if (player) {
453
+ player.style.left = `${this.state.playerPos.x}px`;
454
+ player.style.top = `${this.state.playerPos.y}px`;
455
+ if (this.state.drinks >= 3) {
456
+ player.classList.add('drunk-effect');
457
+ } else {
458
+ player.classList.remove('drunk-effect');
459
+ }
460
+ }
461
+
462
+ if (wingman) {
463
+ wingman.style.left = `${this.state.wingmanPos.x}px`;
464
+ wingman.style.top = `${this.state.wingmanPos.y}px`;
465
+ }
466
+
467
+ // Center viewport on shy guy
468
+ const viewport = document.querySelector('.viewport');
469
+ if (viewport) {
470
+ const scrollX = this.state.playerPos.x - viewport.clientWidth / 2;
471
+ const scrollY = this.state.playerPos.y - viewport.clientHeight / 2;
472
+ viewport.scrollTo(scrollX, scrollY);
473
+ }
474
+ }
475
+
476
+ initializeGrid() {
477
+ const grid = document.getElementById('party-grid');
478
+ grid.innerHTML = '';
479
+
480
+ // Add locations as markers
481
+ Object.entries(this.state.locations).forEach(([type, positions]) => {
482
+ positions.forEach(pos => {
483
+ const marker = document.createElement('div');
484
+ marker.className = `location-marker ${type}`;
485
+ marker.style.left = `${pos.x * 20}px`;
486
+ marker.style.top = `${pos.y * 20}px`;
487
+ grid.appendChild(marker);
488
+ });
489
+ });
490
+
491
+ // Add characters
492
+ const player = document.createElement('div');
493
+ player.id = 'player';
494
+ if (this.state.drinks >= 3) player.classList.add('drunk-effect');
495
+ grid.appendChild(player);
496
+
497
+ const wingman = document.createElement('div');
498
+ wingman.id = 'wingman';
499
+ grid.appendChild(wingman);
500
+
501
+ // Update positions
502
+ this.updateCharacterPositions();
503
+ }
504
+
505
+ async handleLocationInteraction(x, y) {
506
+ x = Math.floor(x / 20);
507
+ y = Math.floor(y / 20);
508
+
509
  if (this.isLocation(x, y, 'bar')) {
510
+ this.state.drinks++;
511
+ this.state.confidence = Math.min(100, this.state.confidence + 15);
512
+ this.state.anxiety = Math.max(0, this.state.anxiety - 10);
513
+ await this.addMessage("*Takes another drink from the bar*", 'shyguy');
514
+
515
+ if (this.state.drinks > 5) {
516
+ this.state.confidence = Math.max(0, this.state.confidence - 5);
517
+ await this.addMessage("*Starting to feel really dizzy...*", 'shyguy');
518
+ }
519
  }
520
+
521
+ if (this.isLocation(x, y, 'sister')) {
522
+ this.state.confidence = Math.min(100, this.state.confidence + 20);
523
+ this.state.anxiety = Math.max(0, this.state.anxiety - 15);
524
+ await this.addMessage("*Gets some encouragement from sister*", 'shyguy');
525
+ }
526
+
527
  if (this.isLocation(x, y, 'dj')) {
528
+ this.state.confidence = Math.min(100, this.state.confidence + 10);
529
+ this.state.anxiety = Math.max(0, this.state.anxiety - 5);
530
+ await this.addMessage("*Vibing to the music*", 'shyguy');
531
  }
532
+
533
  if (this.isLocation(x, y, 'girl')) {
534
+ if (this.state.confidence >= 70 && this.state.anxiety <= 50) {
535
+ await this.addMessage("*Finally gathered the courage to talk to her!*", 'shyguy');
536
+ this.gameWon();
537
+ } else {
538
+ await this.addMessage("*Gets too nervous and quickly walks away*", 'shyguy');
539
+ this.state.playerPos = {
540
+ x: Math.max(0, x - 2) * 20,
541
+ y: Math.max(0, y - 2) * 20
542
+ };
543
+ this.state.anxiety += 15;
544
+ this.state.confidence = Math.max(0, this.state.confidence - 10);
545
+ }
546
  }
547
+ }
548
+
549
+ isLocation(x, y, type) {
550
+ return this.state.locations[type].some(pos => pos.x === x && pos.y === y);
551
+ }
552
+
553
+ async handleInput(userInput) {
554
+ if (this.state.isProcessing) return;
555
+ this.state.isProcessing = true;
556
+
557
+ try {
558
+ if (!userInput.trim()) throw new Error("Please enter some text");
559
+
560
+ this.addMessage(userInput, 'wingman-msg');
561
+ this.addLoadingMessage();
562
+
563
+ const currentState = `Current state:
564
+ Confidence: ${this.state.confidence}%,
565
+ Anxiety: ${this.state.anxiety}%,
566
+ Drinks: ${this.state.drinks},
567
+ Location: ${this.getLocationDescription()},
568
+ Position: (${Math.floor(this.state.playerPos.x / 20)},${Math.floor(this.state.playerPos.y / 20)}),
569
+ Emotion: ${this.state.emotion}`;
570
+
571
+ this.context.push({
572
+ role: 'user',
573
+ content: `${userInput}\n\n${currentState}`
574
+ });
575
+
576
+ const llmResponse = await this.callMistralAPI();
577
+ const dialogue = await this.processLLMResponse(llmResponse);
578
+
579
+ this.removeLoadingMessage();
580
+ this.addMessage(dialogue, 'shyguy');
581
+
582
+ this.context.push({
583
+ role: 'assistant',
584
+ content: llmResponse
585
+ });
586
+
587
+ if (this.context.length > 10) {
588
+ this.context = [
589
+ this.context[0],
590
+ ...this.context.slice(-4)
591
+ ];
592
+ }
593
+ } catch (error) {
594
+ this.removeLoadingMessage();
595
+ this.addMessage(`Error: ${error.message}`, 'error');
596
+ console.error('Error:', error);
597
+ } finally {
598
+ this.state.isProcessing = false;
599
  }
600
+ }
601
+
602
+ async processLLMResponse(response) {
603
+ try {
604
+ const jsonMatch = response.match(/\{[\s\S]*\}/);
605
+ if (!jsonMatch) {
606
+ throw new Error("Invalid response format");
607
+ }
608
+
609
+ const parsedResponse = JSON.parse(jsonMatch[0]);
610
+
611
+ if (!parsedResponse.dialogue || !parsedResponse.movement || !parsedResponse.emotion) {
612
+ throw new Error("Missing required fields in response");
613
+ }
614
+
615
+ this.state.emotion = parsedResponse.emotion;
616
+
617
+ await this.movePlayer(
618
+ parsedResponse.movement.x,
619
+ parsedResponse.movement.y
620
+ );
621
+
622
+ this.updateEmotionalState(parsedResponse.emotion);
623
+
624
+ return parsedResponse.dialogue;
625
+ } catch (error) {
626
+ console.error("Error processing LLM response:", error);
627
+ return "I'm not sure what to do right now...";
628
  }
629
+ }
630
+
631
+ updateEmotionalState(emotion) {
632
+ const emotionEffects = {
633
+ 'anxious': { confidence: -5, anxiety: 5 },
634
+ 'nervous': { confidence: -2, anxiety: 2 },
635
+ 'slightly_confident': { confidence: 5, anxiety: -5 },
636
+ 'confident': { confidence: 10, anxiety: -10 }
637
+ };
638
+
639
+ const effect = emotionEffects[emotion] || { confidence: 0, anxiety: 0 };
640
 
641
+ this.state.confidence = Math.max(0, Math.min(100, this.state.confidence + effect.confidence));
642
+ this.state.anxiety = Math.max(0, Math.min(100, this.state.anxiety + effect.anxiety));
643
+ }
644
+
645
+ async callMistralAPI() {
646
+ try {
647
+ const response = await fetch('https://api.mistral.ai/v1/chat/completions', {
648
+ method: 'POST',
649
+ headers: {
650
+ 'Content-Type': 'application/json',
651
+ 'Authorization': `Bearer ${this.apiKey}`
652
+ },
653
+ body: JSON.stringify({
654
+ model: 'mistral-large-latest',
655
+ messages: this.context,
656
+ max_tokens: 150,
657
+ temperature: 0.7
658
+ })
659
+ });
660
+
661
+ if (!response.ok) {
662
+ const error = await response.json();
663
+ throw new Error(error.error?.message || 'API request failed');
664
+ }
665
+
666
+ const data = await response.json();
667
+ return data.choices[0].message.content;
668
+ } catch (error) {
669
+ if (error.message.includes('API key')) {
670
+ throw new Error('Invalid API key. Please check your API key and try again.');
671
+ }
672
+ throw new Error('Failed to get response from AI. Please try again.');
673
  }
674
+ }
675
+
676
+ getLocationDescription() {
677
+ const x = Math.floor(this.state.playerPos.x / 20);
678
+ const y = Math.floor(this.state.playerPos.y / 20);
679
 
680
+ if (this.isLocation(x, y, 'bar')) return 'at the bar';
681
+ if (this.isLocation(x, y, 'dj')) return 'near the DJ';
682
+ if (this.isLocation(x, y, 'sister')) return 'with sister';
683
+ if (this.isLocation(x, y, 'girl')) return 'near the girl';
684
+ return 'in the room';
685
+ }
686
+
687
+ gameWon() {
688
+ this.addMessage("Congratulations! You successfully talked to her, and she seems interested! The party wasn't so scary after all.", 'wingman-msg');
689
+ this.state.hasSpokenToGirl = true;
690
 
691
+ const winScreen = document.createElement('div');
692
+ winScreen.className = 'win-screen';
693
+ winScreen.innerHTML = `
694
+ <h2>You did it!</h2>
695
+ <p>Final Stats:</p>
696
+ <p>Confidence: ${this.state.confidence}%</p>
697
+ <p>Anxiety: ${this.state.anxiety}%</p>
698
+ <p>Drinks: ${this.state.drinks}</p>
699
+ <p>Time taken: ${this.getTimeDifference()}</p>
700
+ <button onclick="location.reload()">Play Again</button>
701
+ `;
702
+ document.body.appendChild(winScreen);
703
+ }
704
+
705
+ getTimeDifference() {
706
+ const startTime = new Date(2024, 0, 1, 20, 0);
707
+ const timeDiff = this.state.time - startTime;
708
+ const minutes = Math.floor(timeDiff / 60000);
709
+ return `${minutes} minutes`;
710
+ }
711
+
712
+ addMessage(text, type) {
713
+ const chat = document.getElementById('chat-container');
714
+ const messageDiv = document.createElement('div');
715
+ messageDiv.className = `message ${type}`;
716
+ messageDiv.textContent = text;
717
+ chat.appendChild(messageDiv);
718
+ chat.scrollTop = chat.scrollHeight;
719
+ }
720
+
721
+ addLoadingMessage() {
722
+ const chat = document.getElementById('chat-container');
723
+ const loadingDiv = document.createElement('div');
724
+ loadingDiv.className = 'message shyguy typing';
725
+ loadingDiv.id = 'loading-message';
726
+ loadingDiv.textContent = 'Thinking...';
727
+ chat.appendChild(loadingDiv);
728
+ chat.scrollTop = chat.scrollHeight;
729
+ }
730
+
731
+ removeLoadingMessage() {
732
+ const loadingMessage = document.getElementById('loading-message');
733
+ if (loadingMessage) {
734
+ loadingMessage.remove();
735
+ }
736
+ }
737
+
738
+ updateStats() {
739
+ document.getElementById('confidence').textContent = Math.round(this.state.confidence);
740
+ document.getElementById('anxiety').textContent = Math.round(this.state.anxiety);
741
+ document.getElementById('drinks').textContent = this.state.drinks;
742
+ document.getElementById('time').textContent =
743
+ this.state.time.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' });
744
  }
745
  }
 
746
 
747
+ let game;
748
+
749
+ function initializeGame() {
750
+ const apiKey = document.getElementById('api-key').value.trim();
751
+ if (!apiKey) {
752
+ alert('Please enter your Mistral API key');
753
+ return;
754
+ }
755
+
756
+ document.getElementById('api-key-container').style.display = 'none';
757
+ document.getElementById('game-content').style.display = 'block';
758
+
759
+ game = new ShyGuySimulator(apiKey);
760
  }
761
+
762
+ async function handleUserInput() {
763
+ if (!game) return;
764
+
765
+ const input = document.getElementById('user-input');
766
+ const text = input.value.trim();
767
+ if (text) {
768
+ await game.handleInput(text);
769
+ input.value = '';
770
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
771
  }
772
+
773
+ document.getElementById('user-input')?.addEventListener('keypress', function(e) {
774
+ if (e.key === 'Enter') {
775
+ handleUserInput();
776
+ }
777
+ });
778
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
779
  </body>
780
  </html>