theo-michel commited on
Commit
eae3d7c
·
verified ·
1 Parent(s): 47fc04f

Upload 51 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +7 -0
  2. assets/.DS_Store +0 -0
  3. assets/assets/.DS_Store +0 -0
  4. assets/assets/bar_sprite.png +0 -0
  5. assets/assets/barman.jpeg +0 -0
  6. assets/assets/barman_old.png +0 -0
  7. assets/assets/dj.jpeg +0 -0
  8. assets/assets/dj_old.png +3 -0
  9. assets/assets/dj_sprite.png +0 -0
  10. assets/assets/door_sprite.png +0 -0
  11. assets/assets/floor-tile.png +0 -0
  12. assets/assets/game-over-8bit-music-danijel-zambo-1-00-16.mp3 +0 -0
  13. assets/assets/game-over.png +3 -0
  14. assets/assets/image.webp +0 -0
  15. assets/assets/intro-image.jpg +0 -0
  16. assets/assets/jessica.jpeg +0 -0
  17. assets/assets/jessica_old.png +0 -0
  18. assets/assets/jessica_sprite.png +0 -0
  19. assets/assets/moonlit-whispers-theo-gerard-main-version-35960-02-34.mp3 +3 -0
  20. assets/assets/player.png +0 -0
  21. assets/assets/shyguy-headshot_old.png +0 -0
  22. assets/assets/shyguy.jpeg +0 -0
  23. assets/assets/shyguy.png +0 -0
  24. assets/assets/shyguy_headshot.jpeg +0 -0
  25. assets/assets/shyguy_left.png +0 -0
  26. assets/assets/shyguy_right.png +0 -0
  27. assets/assets/shyguy_sprite.png +0 -0
  28. assets/assets/sister.jpeg +0 -0
  29. assets/assets/sister_old.png +0 -0
  30. assets/assets/sister_sprite.png +0 -0
  31. assets/assets/tiny-steps-danijel-zambo-main-version-1433-01-48.mp3 +3 -0
  32. assets/assets/title-screen.png +3 -0
  33. assets/assets/tragic-piano-game-over-music-gfx-sounds-1-00-08.mp3 +0 -0
  34. assets/assets/victory.png +3 -0
  35. assets/assets/wall_sprite.png +0 -0
  36. assets/assets/wingman.jpeg +0 -0
  37. assets/assets/wingman_left.png +0 -0
  38. assets/assets/wingman_old.png +3 -0
  39. assets/assets/wingman_right.png +0 -0
  40. assets/assets/wingman_sprite.png +0 -0
  41. src/constants.js +7 -0
  42. src/conversation_llm.js +76 -0
  43. src/eleven_labs.js +90 -0
  44. src/game.js +64 -0
  45. src/game_engine.js +1159 -0
  46. src/index.js +9 -0
  47. src/llm.js +183 -0
  48. src/shyguy.js +127 -0
  49. src/shyguy_llm.js +109 -0
  50. src/speech_to_text.js +66 -0
.gitattributes CHANGED
@@ -33,3 +33,10 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ assets/assets/dj_old.png filter=lfs diff=lfs merge=lfs -text
37
+ assets/assets/game-over.png filter=lfs diff=lfs merge=lfs -text
38
+ assets/assets/moonlit-whispers-theo-gerard-main-version-35960-02-34.mp3 filter=lfs diff=lfs merge=lfs -text
39
+ assets/assets/tiny-steps-danijel-zambo-main-version-1433-01-48.mp3 filter=lfs diff=lfs merge=lfs -text
40
+ assets/assets/title-screen.png filter=lfs diff=lfs merge=lfs -text
41
+ assets/assets/victory.png filter=lfs diff=lfs merge=lfs -text
42
+ assets/assets/wingman_old.png filter=lfs diff=lfs merge=lfs -text
assets/.DS_Store ADDED
Binary file (6.15 kB). View file
 
assets/assets/.DS_Store ADDED
Binary file (10.2 kB). View file
 
assets/assets/bar_sprite.png ADDED
assets/assets/barman.jpeg ADDED
assets/assets/barman_old.png ADDED
assets/assets/dj.jpeg ADDED
assets/assets/dj_old.png ADDED

Git LFS Details

  • SHA256: 0efd462632a8002e38c7dc4ed3cddbce21bdb6684575700b22699c50112037d8
  • Pointer size: 132 Bytes
  • Size of remote file: 2.76 MB
assets/assets/dj_sprite.png ADDED
assets/assets/door_sprite.png ADDED
assets/assets/floor-tile.png ADDED
assets/assets/game-over-8bit-music-danijel-zambo-1-00-16.mp3 ADDED
Binary file (683 kB). View file
 
assets/assets/game-over.png ADDED

Git LFS Details

  • SHA256: b432c55365bb67d10381d935929e4fcb6d3e6e1d0b707601b30b1952b1a063fd
  • Pointer size: 132 Bytes
  • Size of remote file: 3.34 MB
assets/assets/image.webp ADDED
assets/assets/intro-image.jpg ADDED
assets/assets/jessica.jpeg ADDED
assets/assets/jessica_old.png ADDED
assets/assets/jessica_sprite.png ADDED
assets/assets/moonlit-whispers-theo-gerard-main-version-35960-02-34.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:042c696678781cbd276979868386b81d49e5623dc1bb0471d4a8a9db672d8c92
3
+ size 6224789
assets/assets/player.png ADDED
assets/assets/shyguy-headshot_old.png ADDED
assets/assets/shyguy.jpeg ADDED
assets/assets/shyguy.png ADDED
assets/assets/shyguy_headshot.jpeg ADDED
assets/assets/shyguy_left.png ADDED
assets/assets/shyguy_right.png ADDED
assets/assets/shyguy_sprite.png ADDED
assets/assets/sister.jpeg ADDED
assets/assets/sister_old.png ADDED
assets/assets/sister_sprite.png ADDED
assets/assets/tiny-steps-danijel-zambo-main-version-1433-01-48.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dd1bc1cfcdebe9999f561dd4cdd5a9bd32760764a7634ab11f13a9c53b2febd9
3
+ size 4334026
assets/assets/title-screen.png ADDED

Git LFS Details

  • SHA256: bf87396234fba30571a24ac0abb67b0cac1e4e993f8db5732cee2bf008f6204a
  • Pointer size: 132 Bytes
  • Size of remote file: 4.01 MB
assets/assets/tragic-piano-game-over-music-gfx-sounds-1-00-08.mp3 ADDED
Binary file (339 kB). View file
 
assets/assets/victory.png ADDED

Git LFS Details

  • SHA256: bfee3e2b474d892abe75a0bc6aa10e4efa8ae159114fd59b46255dc38fdc107a
  • Pointer size: 132 Bytes
  • Size of remote file: 3.35 MB
assets/assets/wall_sprite.png ADDED
assets/assets/wingman.jpeg ADDED
assets/assets/wingman_left.png ADDED
assets/assets/wingman_old.png ADDED

Git LFS Details

  • SHA256: 39d93bdc4e63c00c879ab5627832352a1eaf12f7f66157b80b57703612e3f80d
  • Pointer size: 132 Bytes
  • Size of remote file: 1.28 MB
assets/assets/wingman_right.png ADDED
assets/assets/wingman_sprite.png ADDED
src/constants.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ export const WINGMAN_LABEL = "wingman";
2
+ export const SHYGUY_LABEL = "shyguy";
3
+ export const SISTER_LABEL = "sister";
4
+ export const GIRL_LABEL = "girl";
5
+ export const BAR_LABEL = "barman";
6
+ export const DJ_LABEL = "dj";
7
+ export const EXIT_LABEL = "exit";
src/conversation_llm.js ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import LLM from "./llm";
2
+
3
+ export class ConversationLLM {
4
+ constructor(character1Name, character2Name, character1Prompt, character2Prompt, situation_prompt, outputFormatPrompt, functionDescriptions, functionPrompt) {
5
+ this.character1Name = character1Name;
6
+ this.character2Name = character2Name;
7
+ this.character1Prompt = character1Prompt;
8
+ this.character2Prompt = character2Prompt;
9
+ this.situation_prompt = situation_prompt;
10
+ this.outputFormatPrompt = outputFormatPrompt;
11
+ this.functionDescriptions = functionDescriptions;
12
+ this.functionPrompt = functionPrompt;
13
+
14
+ }
15
+
16
+ async generateConversation(numTurns = 3) {
17
+ try {
18
+ let conversation = [];
19
+ const llm = new LLM();
20
+
21
+ for (let i = 0; i < numTurns; i++) {
22
+
23
+ // Alternate between characters for each turn
24
+ const isCharacter1Turn = i % 2 === 0;
25
+ const currentSpeaker = isCharacter1Turn ? this.character1Prompt : this.character2Prompt;
26
+ const currentListener = isCharacter1Turn ? this.character2Prompt : this.character1Prompt;
27
+ const currentSpeakerName = isCharacter1Turn ? this.character1Name : this.character2Name;
28
+ const currentListenerName = isCharacter1Turn ? this.character2Name : this.character1Name;
29
+
30
+ // Format the conversation history as a proper chat message array
31
+ const conversationHistory = [...conversation];
32
+
33
+ // Create system message for current speaker
34
+ const systemMessage = {
35
+ role: 'system',
36
+ content: `${this.situation_prompt}\nRoleplay as: ${currentSpeakerName}\nMake only the response to the user. Only speech, no speech style. You have the following personality: ${currentSpeaker}. You talk to ${currentListenerName}.`
37
+ };
38
+
39
+ // Get response from LLM with proper message format
40
+ const response = await llm.getChatCompletion(
41
+ systemMessage.content,
42
+ conversationHistory.length > 0
43
+ ? JSON.stringify(conversationHistory)
44
+ : "Start the conversation"
45
+ );
46
+
47
+ // Ensure the response is in the correct format with the proper character role
48
+ const parsedResponse = {
49
+ role: currentSpeakerName, // Use the character name instead of prompt
50
+ content: this.parseConversation(response)
51
+ };
52
+
53
+ conversation.push(parsedResponse);
54
+ }
55
+
56
+ const analysis = await llm.getFunctionKey(
57
+ this.functionDescriptions,
58
+ this.functionPrompt + JSON.stringify(conversation)
59
+ );
60
+
61
+ return {
62
+ conversation,
63
+ analysis
64
+ };
65
+ } catch (error) {
66
+ console.error('Error generating conversation:', error);
67
+ throw error;
68
+ }
69
+ }
70
+ parseConversation(llmResponse) {
71
+
72
+ return llmResponse;
73
+ }
74
+ }
75
+
76
+
src/eleven_labs.js ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { SHYGUY_LABEL, SISTER_LABEL, GIRL_LABEL, BAR_LABEL, DJ_LABEL } from "./constants.js";
2
+
3
+ export class ElevenLabsClient {
4
+ constructor() {
5
+ this.apiKey = "sk_62d0fc2ccddb1a4036f624dd8c411383fd9b9990755ea086";
6
+ this.baseUrl = "https://api.elevenlabs.io/v1";
7
+ }
8
+
9
+ static characterToVoiceIdMapping = {
10
+ [SHYGUY_LABEL]: "bGNROVfU5WbK6F0AyHII",
11
+ [SISTER_LABEL]: "VfTRhexMRVuPmOe9aogj",
12
+ [GIRL_LABEL]: "zQPM9vJjjzGxbs457rQj",
13
+ [BAR_LABEL]: "XA2bIQ92TabjGbpO2xRr",
14
+ [DJ_LABEL]: "T0pkYhIZ7UMOc26gqqeX",
15
+ };
16
+
17
+ async playAudioForCharacter(character, text) {
18
+ const voiceId = ElevenLabsClient.characterToVoiceIdMapping[character];
19
+ if (!voiceId) {
20
+ throw new Error(`No voice mapping found for character: ${character}`);
21
+ }
22
+ const audioBlob = await this.createSpeech({
23
+ text: text,
24
+ voiceId: voiceId,
25
+ });
26
+ const audioUrl = URL.createObjectURL(audioBlob);
27
+ const audio = new Audio(audioUrl);
28
+
29
+ // hack to wait for the audio to finish playing
30
+ return new Promise((res) => {
31
+ audio.play();
32
+ audio.onended = res;
33
+ });
34
+ }
35
+
36
+ async createSpeech({
37
+ text,
38
+ voiceId,
39
+ modelId = "eleven_multilingual_v2",
40
+ outputFormat = "mp3_44100_128",
41
+ voiceSettings = null,
42
+ pronunciationDictionaryLocators = null,
43
+ seed = null,
44
+ previousText = null,
45
+ nextText = null,
46
+ previousRequestIds = null,
47
+ nextRequestIds = null,
48
+ usePvcAsIvc = false,
49
+ applyTextNormalization = "auto",
50
+ }) {
51
+ const url = `${this.baseUrl}/text-to-speech/${voiceId}?output_format=${outputFormat}`;
52
+
53
+ const requestBody = {
54
+ text,
55
+ model_id: modelId,
56
+ voice_settings: voiceSettings,
57
+ pronunciation_dictionary_locators: pronunciationDictionaryLocators,
58
+ seed,
59
+ previous_text: previousText,
60
+ next_text: nextText,
61
+ previous_request_ids: previousRequestIds,
62
+ next_request_ids: nextRequestIds,
63
+ use_pvc_as_ivc: usePvcAsIvc,
64
+ apply_text_normalization: applyTextNormalization,
65
+ };
66
+
67
+ // Remove null values from request body
68
+ Object.keys(requestBody).forEach((key) => requestBody[key] === null && delete requestBody[key]);
69
+
70
+ try {
71
+ const response = await fetch(url, {
72
+ method: "POST",
73
+ headers: {
74
+ "xi-api-key": this.apiKey,
75
+ "Content-Type": "application/json",
76
+ },
77
+ body: JSON.stringify(requestBody),
78
+ });
79
+
80
+ if (!response.ok) {
81
+ throw new Error(`ElevenLabs API error: ${response.status} ${response.statusText}`);
82
+ }
83
+
84
+ // Return audio blob
85
+ return await response.blob();
86
+ } catch (error) {
87
+ throw new Error(`Failed to create speech: ${error.message}`);
88
+ }
89
+ }
90
+ }
src/game.js ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Shyguy } from "./shyguy.js";
2
+ import { GameEngine } from "./game_engine.js";
3
+ import { ShyGuyLLM } from "./shyguy_llm.js";
4
+ import { StoryEngine } from "./story_engine.js";
5
+ import { SpeechToTextClient } from "./speech_to_text.js";
6
+ import { ElevenLabsClient } from "./eleven_labs.js";
7
+
8
+ export class Game {
9
+ constructor() {
10
+ this.firstRun = true;
11
+
12
+ this.reset = this.reset.bind(this);
13
+ this.initializeComponents();
14
+ }
15
+
16
+ initializeComponents() {
17
+ // Create fresh instances of all components
18
+ this.shyguy = new Shyguy();
19
+ this.speechToTextClient = new SpeechToTextClient();
20
+ this.elevenLabsClient = new ElevenLabsClient();
21
+ this.shyguyLLM = new ShyGuyLLM(this.shyguy);
22
+ this.storyEngine = new StoryEngine(this.shyguy);
23
+ this.gameEngine = new GameEngine(
24
+ this.shyguy,
25
+ this.shyguyLLM,
26
+ this.storyEngine,
27
+ this.speechToTextClient,
28
+ this.elevenLabsClient
29
+ );
30
+ }
31
+
32
+ async run() {
33
+ this.gameEngine.init(this.firstRun);
34
+ this.gameEngine.setResetCallback(this.reset);
35
+ this.gameEngine.playBackgroundMusic();
36
+ this.gameEngine.lowerMusicVolume();
37
+ }
38
+
39
+ reset() {
40
+ this.firstRun = false;
41
+ // Clean up old game engine
42
+ if (this.gameEngine) {
43
+ // Remove event listeners and clean up
44
+ document.removeEventListener("keydown", this.gameEngine.handleKeyDown);
45
+ document.removeEventListener("keyup", this.gameEngine.handleKeyUp);
46
+ this.gameEngine.sendButton?.removeEventListener("click", this.gameEngine.handleSendMessage);
47
+ this.gameEngine.dialogueContinueButton?.removeEventListener("click", this.gameEngine.handleDialogueContinue);
48
+ this.gameEngine.playAgainBtn?.removeEventListener("click", this.gameEngine.handlePlayAgain);
49
+ this.gameEngine.microphoneButton?.removeEventListener("click", this.gameEngine.handleMicrophone);
50
+ this.gameEngine.startGameBtn?.removeEventListener("click", this.gameEngine.handleStartGame);
51
+ this.gameEngine.dialogueNextButton?.removeEventListener("click", this.gameEngine.handleDialogueNext);
52
+
53
+ // Stop the game loop
54
+ this.gameEngine.shouldContinue = false;
55
+ }
56
+
57
+ setTimeout(() => {
58
+ // Create fresh instances
59
+ this.initializeComponents();
60
+
61
+ this.run();
62
+ }, 100);
63
+ }
64
+ }
src/game_engine.js ADDED
@@ -0,0 +1,1159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { BAR_LABEL, DJ_LABEL, EXIT_LABEL, GIRL_LABEL, SISTER_LABEL, WINGMAN_LABEL, SHYGUY_LABEL } from "./constants";
2
+ import { nameToLabel } from "./story_engine.js";
3
+
4
+ const WINGMAN_SPEED = 5;
5
+ const SHYGUY_SPEED = 0.5;
6
+
7
+ const IS_DEBUG = true;
8
+
9
+ class SpriteEntity {
10
+ constructor(x0, y0, imageSrc, speed = 0, width = 24, height = 64, frameRate = 8, frameCount = 1) {
11
+ this.x = x0;
12
+ this.y = y0;
13
+ this.width = width;
14
+ this.height = height;
15
+ this.image = new Image();
16
+ this.image.src = imageSrc;
17
+ this.frameRate = frameRate;
18
+ this.frameCount = frameCount;
19
+
20
+ // properties for the game engine
21
+ this.moving = false;
22
+ this.speed = speed;
23
+
24
+ // frame index in the sprite sheet
25
+ this.frameX = 0;
26
+ this.frameY = 0; // 0 for right, 1 for left
27
+ }
28
+
29
+ stop() {
30
+ this.moving = false;
31
+ }
32
+
33
+ start() {
34
+ this.moving = true;
35
+ }
36
+
37
+ setSpeed(speed) {
38
+ this.speed = speed;
39
+ }
40
+ }
41
+
42
+ class GuidedSpriteEntity extends SpriteEntity {
43
+ constructor(x0, y0, imageSrc, speed = 0, width = 24, height = 64, frameRate = 8, frameCount = 1) {
44
+ super(x0, y0, imageSrc, speed, width, height, frameRate, frameCount);
45
+ this.target = null;
46
+ }
47
+
48
+ setTarget(target) {
49
+ this.target = target;
50
+ }
51
+ }
52
+
53
+ class SpriteImage {
54
+ constructor(imageSrc, width = 32, height = 32) {
55
+ this.image = new Image();
56
+ this.image.src = imageSrc;
57
+ this.width = width;
58
+ this.height = height;
59
+ }
60
+ }
61
+
62
+ class Target {
63
+ constructor(label, x, y, width, height, color, enabled = true) {
64
+ this.label = label;
65
+ this.x = x;
66
+ this.y = y;
67
+ this.width = width;
68
+ this.height = height;
69
+ this.debugColor = color;
70
+ this.enabled = enabled;
71
+ }
72
+ }
73
+
74
+ export class GameEngine {
75
+ static introMessages = [
76
+ {
77
+ message:
78
+ "Hey man, this is really not my cup of tea. I see Jessica in the corner, I wonder if I can finally tell her I love her.",
79
+ character: SHYGUY_LABEL,
80
+ },
81
+ {
82
+ message: "Man, tonight is your night. I'll get you through it and you'll go home with Jessica.",
83
+ character: WINGMAN_LABEL,
84
+ },
85
+ {
86
+ message: "Geez, that's impossible! Even if I replay the night a million times, I couldn't do it.",
87
+ character: SHYGUY_LABEL,
88
+ },
89
+ {
90
+ message: "Okay, just follow my advice! I'll push you around if needed.",
91
+ character: WINGMAN_LABEL,
92
+ },
93
+ ];
94
+
95
+ constructor(shyguy, shyguyLLM, storyEngine, speechToTextClient, elevenLabsClient) {
96
+ this.shyguy = shyguy;
97
+ this.shyguyLLM = shyguyLLM;
98
+ this.storyEngine = storyEngine;
99
+ this.speechToTextClient = speechToTextClient;
100
+ this.elevenLabsClient = elevenLabsClient;
101
+
102
+ this.canvasWidth = 960;
103
+ this.canvasHeight = 640;
104
+ this.canvas = document.getElementById("gameCanvas");
105
+ if (!this.canvas) {
106
+ console.error("Canvas not found");
107
+ }
108
+ this.ctx = this.canvas.getContext("2d");
109
+
110
+ // View management
111
+ this.gameView = document.getElementById("gameView");
112
+ this.dialogueView = document.getElementById("dialogueView");
113
+ this.currentView = "game";
114
+
115
+ this.shouldContinue = true;
116
+
117
+ this.gameOver = false;
118
+ this.gameSuccessful = false;
119
+
120
+ this.gameChatContainer = document.getElementById("chatMessages");
121
+ this.messageInput = document.getElementById("messageInput");
122
+ this.sendButton = document.getElementById("sendButton");
123
+ this.microphoneButton = document.getElementById("micButton");
124
+ this.gameOverImage = document.getElementById("gameOverImage");
125
+ this.gameOverText = document.getElementById("gameOverText");
126
+
127
+ this.dialogueChatContainer = document.getElementById("dialogueMessages");
128
+ this.dialogueContinueButton = document.getElementById("dialogueContinueButton");
129
+ this.dialogueNextButton = document.getElementById("dialogueNextButton");
130
+
131
+ this.gameFrame = 0;
132
+ this.keys = {
133
+ ArrowUp: false,
134
+ ArrowDown: false,
135
+ ArrowLeft: false,
136
+ ArrowRight: false,
137
+ };
138
+
139
+ // Bind methods
140
+ this.switchView = this.switchView.bind(this);
141
+ this.update = this.update.bind(this);
142
+ this.draw = this.draw.bind(this);
143
+ this.run = this.run.bind(this);
144
+ this.handleKeyDown = this.handleKeyDown.bind(this);
145
+ this.handleKeyUp = this.handleKeyUp.bind(this);
146
+ this.setNewTarget = this.setNewTarget.bind(this);
147
+ this.checkTargetReached = this.checkTargetReached.bind(this);
148
+ this.updateGuidedSpriteDirection = this.updateGuidedSpriteDirection.bind(this);
149
+ this.updateSprite = this.updateSprite.bind(this);
150
+ this.handleSpriteCollision = this.handleSpriteCollision.bind(this);
151
+ this.initDebugControls = this.initDebugControls.bind(this);
152
+ this.stopShyguyAnimation = this.stopShyguyAnimation.bind(this);
153
+ this.handlePlayAgain = this.handlePlayAgain.bind(this);
154
+ this.handleMicrophone = this.handleMicrophone.bind(this);
155
+ this.handleSendMessage = this.handleSendMessage.bind(this);
156
+ this.handleMicrophone = this.handleMicrophone.bind(this);
157
+ this.handleDialogueContinue = this.handleDialogueContinue.bind(this);
158
+ this.handleFirstStartGame = this.handleFirstStartGame.bind(this);
159
+ this.setGameOver = this.setGameOver.bind(this);
160
+ this.handleDialogueNext = this.handleDialogueNext.bind(this);
161
+
162
+ this.pushEnabled = false;
163
+ this.voiceEnabled = !IS_DEBUG;
164
+
165
+ // Debug controls
166
+ this.initDebugControls();
167
+
168
+ // if we have other obstacles, we can add them here
169
+ this.gridMapTypes = {
170
+ floor: 0,
171
+ wall: 1,
172
+ door: 2,
173
+ };
174
+
175
+ // load assets for drawing the scene
176
+ this.wall = new SpriteImage("/assets/assets/wall_sprite.png");
177
+ this.floor = new SpriteImage("/assets/assets/floor-tile.png");
178
+ this.door = new SpriteImage("/assets/assets/door_sprite.png");
179
+
180
+ this.gridCols = Math.ceil(this.canvasWidth / this.wall.width);
181
+ this.gridRows = Math.ceil(this.canvasHeight / this.wall.height);
182
+
183
+ // initialize grid map
184
+ this.backgroundGridMap = [];
185
+ this.initBackgroundGridMap();
186
+
187
+ // initialize players
188
+ const cx = this.canvasWidth / 2;
189
+ const cy = this.canvasHeight / 2;
190
+ this.shyguySprite = new GuidedSpriteEntity(cx, cy, "/assets/assets/shyguy_sprite.png", SHYGUY_SPEED);
191
+ this.wingmanSprite = new SpriteEntity(
192
+ this.wall.width,
193
+ this.canvasHeight - this.wall.height - 64,
194
+ "/assets/assets/wingman_sprite.png",
195
+ WINGMAN_SPEED
196
+ );
197
+
198
+ this.jessicaSprite = new SpriteImage("/assets/assets/jessica_sprite.png", 64, 64);
199
+ this.djSprite = new SpriteImage("/assets/assets/dj_sprite.png", 64, 64);
200
+ this.barSprite = new SpriteImage("/assets/assets/bar_sprite.png", 64, 64);
201
+ this.sisterSprite = new SpriteImage("/assets/assets/sister_sprite.png", 64, 64);
202
+
203
+ this.targets = {
204
+ exit: new Target(EXIT_LABEL, this.wall.width, this.wall.height, this.wall.width, this.wall.height, "red", true),
205
+ girl: new Target(
206
+ GIRL_LABEL,
207
+ this.canvasWidth - this.wall.width - this.jessicaSprite.width,
208
+ (this.canvasHeight - this.wall.height - this.jessicaSprite.height) / 2,
209
+ this.jessicaSprite.width,
210
+ this.jessicaSprite.height,
211
+ "pink",
212
+ true
213
+ ),
214
+ bar: new Target(
215
+ BAR_LABEL,
216
+ (this.canvasWidth - this.wall.width - this.barSprite.width) / 2,
217
+ this.wall.height,
218
+ this.barSprite.width,
219
+ this.barSprite.height,
220
+ "blue",
221
+ true
222
+ ),
223
+ dj: new Target(
224
+ DJ_LABEL,
225
+ this.wall.width,
226
+ (this.canvasHeight - this.wall.height - this.djSprite.height) / 2,
227
+ this.djSprite.width,
228
+ this.djSprite.height,
229
+ "green",
230
+ true
231
+ ),
232
+ sister: new Target(
233
+ SISTER_LABEL,
234
+ this.canvasWidth - this.wall.width - this.sisterSprite.width,
235
+ this.wall.height,
236
+ this.sisterSprite.width,
237
+ this.sisterSprite.height,
238
+ "yellow",
239
+ true
240
+ ),
241
+ };
242
+
243
+ // Add game over view
244
+ this.gameOverView = document.getElementById("gameOverView");
245
+ this.playAgainBtn = document.getElementById("playAgainBtn");
246
+
247
+ this.isRecording = false;
248
+
249
+ // Add these lines
250
+ this.introView = document.getElementById("introView");
251
+ this.startGameBtn = document.getElementById("startGameBtn");
252
+
253
+ this.backgroundMusic = new Audio('assets/assets/tiny-steps-danijel-zambo-main-version-1433-01-48.mp3');
254
+ this.backgroundMusic.loop = true;
255
+
256
+ this.gameOverMusic = new Audio('/assets/assets/game-over-8bit-music-danijel-zambo-1-00-16.mp3');
257
+ this.gameOverMusic.loop = false;
258
+
259
+ this.victoryMusic = new Audio('/assets/assets/moonlit-whispers-theo-gerard-main-version-35960-02-34.mp3');
260
+ this.victoryMusic.loop = false;
261
+
262
+ // Move character images to class state
263
+ this.leftCharacterImg = document.getElementById("leftCharacterImg");
264
+ this.rightCharacterImg = document.getElementById("rightCharacterImg");
265
+ this.hideCharacterImages();
266
+ }
267
+
268
+ showCharacterImages() {
269
+ this.leftCharacterImg.style.display = "block";
270
+ this.rightCharacterImg.style.display = "block";
271
+ }
272
+
273
+ hideCharacterImages() {
274
+ this.leftCharacterImg.style.display = "none";
275
+ this.rightCharacterImg.style.display = "none";
276
+ }
277
+
278
+ init(firstRun = true) {
279
+ this.canvas.width = this.canvasWidth;
280
+ this.canvas.height = this.canvasHeight;
281
+
282
+ document.addEventListener("keydown", this.handleKeyDown);
283
+ document.addEventListener("keyup", this.handleKeyUp);
284
+
285
+ // Initialize with game view
286
+
287
+ this.sendButton.addEventListener("click", this.handleSendMessage);
288
+ this.dialogueContinueButton.addEventListener("click", this.handleDialogueContinue);
289
+ this.dialogueNextButton.addEventListener("click", this.handleDialogueNext);
290
+ this.playAgainBtn.addEventListener("click", this.handlePlayAgain);
291
+ this.microphoneButton.addEventListener("click", this.handleMicrophone);
292
+
293
+ if (firstRun) {
294
+ this.startGameBtn.addEventListener("click", this.handleFirstStartGame);
295
+ this.switchView("intro");
296
+ } else {
297
+ if (this.currentView !== "game") {
298
+ this.switchView("game");
299
+ }
300
+ this.run();
301
+ this.shyguySprite.setTarget(this.targets.exit);
302
+ }
303
+ }
304
+
305
+ async handleFirstStartGame() {
306
+ this.switchView("dialogue");
307
+ this.leftCharacterImg.src = "/assets/assets/wingman.jpeg";
308
+ this.rightCharacterImg.src = "/assets/assets/shyguy_headshot.jpeg";
309
+ this.showCharacterImages();
310
+ this.hideContinueButton();
311
+
312
+ for (const introMessage of GameEngine.introMessages) {
313
+ const { message, character } = introMessage;
314
+ this.addChatMessage(this.dialogueChatContainer, message, character, true);
315
+ if (this.voiceEnabled) {
316
+ await this.elevenLabsClient.playAudioForCharacter(character, message);
317
+ } else {
318
+ await new Promise((resolve) => setTimeout(resolve, 1000));
319
+ }
320
+ }
321
+
322
+ this.showNextButton();
323
+ }
324
+
325
+ showNextButton() {
326
+ if (this.dialogueNextButton) {
327
+ this.dialogueNextButton.style.display = "block";
328
+ }
329
+ }
330
+
331
+ hideNextButton() {
332
+ if (this.dialogueNextButton) {
333
+ this.dialogueNextButton.style.display = "none";
334
+ }
335
+ }
336
+
337
+ handleDialogueNext() {
338
+ this.clearChat(this.dialogueChatContainer);
339
+ this.leftCharacterImg.src = "";
340
+ this.rightCharacterImg.src = "";
341
+ this.hideCharacterImages();
342
+ this.hideNextButton();
343
+ this.showContinueButton();
344
+ this.handleStartGame();
345
+ }
346
+
347
+ async handleStartGame() {
348
+ this.switchView("game");
349
+ this.playBackgroundMusic();
350
+ this.run();
351
+ this.shyguySprite.setTarget(this.targets.exit);
352
+ }
353
+
354
+ setResetCallback(func) {
355
+ this.resetCallback = func;
356
+ }
357
+
358
+ resetGame() {
359
+ if (this.resetCallback) {
360
+ this.resetCallback();
361
+ }
362
+ }
363
+
364
+ initBackgroundGridMap() {
365
+ for (let row = 0; row < this.gridRows; row++) {
366
+ this.backgroundGridMap[row] = [];
367
+ for (let col = 0; col < this.gridCols; col++) {
368
+ // Set walls and obstacles (in future)
369
+ if (row === 0 || row === this.gridRows - 1 || col === 0 || col === this.gridCols - 1) {
370
+ this.backgroundGridMap[row][col] = this.gridMapTypes.wall;
371
+ } else {
372
+ this.backgroundGridMap[row][col] = this.gridMapTypes.floor;
373
+ }
374
+ }
375
+ }
376
+ this.backgroundGridMap[0][1] = this.gridMapTypes.door;
377
+ }
378
+
379
+ checkWallCollision(sprite, newX, newY) {
380
+ const x = newX;
381
+ const y = newY;
382
+ // For a sprite twice as big as grid, divide by half the sprite width/height
383
+ const gridX = Math.floor(x / (sprite.width * 1.33));
384
+ const gridY = Math.floor(y / (sprite.height / 2));
385
+
386
+ // Check all grid cells the sprite overlaps
387
+ // For a sprite twice as big, it can overlap up to 4 cells
388
+ for (let row = gridY; row <= Math.floor((y + sprite.height) / (sprite.height / 2)); row++) {
389
+ for (let col = gridX; col <= Math.floor((x + sprite.width) / (sprite.width * 1.33)); col++) {
390
+ if (row >= 0 && row < this.gridRows && col >= 0 && col < this.gridCols) {
391
+ if (this.backgroundGridMap[row][col] === this.gridMapTypes.wall) {
392
+ return true;
393
+ }
394
+ }
395
+ }
396
+ }
397
+
398
+ return false;
399
+ }
400
+
401
+ checkSpriteCollision(newX, newY, sprite1, sprite2) {
402
+ return (
403
+ newX < sprite2.x + sprite2.width &&
404
+ newX + sprite1.width > sprite2.x &&
405
+ newY < sprite2.y + sprite2.height &&
406
+ newY + sprite1.height > sprite2.y
407
+ );
408
+ }
409
+
410
+ handleSpriteCollision(sprite1, sprite2) {
411
+ if (!this.pushEnabled) {
412
+ return true; // Return true to block movement as before
413
+ }
414
+
415
+ // Calculate velocity difference
416
+ let dx = 0;
417
+ let dy = 0;
418
+ if (this.keys.ArrowUp) dy = -sprite1.speed;
419
+ else if (this.keys.ArrowDown) dy = sprite1.speed;
420
+ else if (this.keys.ArrowLeft) dx = -sprite1.speed;
421
+ else if (this.keys.ArrowRight) dx = sprite1.speed;
422
+
423
+ // If arrow player isn't moving, stop button player
424
+ if (dx === 0 && dy === 0) {
425
+ return true;
426
+ }
427
+
428
+ // Calculate effective push speed (difference in velocities)
429
+ const pushSpeed = Math.max(0, sprite1.speed - sprite2.speed);
430
+
431
+ // If arrow player is faster, push button player
432
+ if (pushSpeed > 0) {
433
+ let newX = sprite2.x + (dx !== 0 ? dx : 0);
434
+ let newY = sprite2.y + (dy !== 0 ? dy : 0);
435
+
436
+ // Only apply the push if it won't result in a wall collision
437
+ if (!this.checkWallCollision(sprite2, newX, newY)) {
438
+ sprite2.x = newX;
439
+ sprite2.y = newY;
440
+ }
441
+ }
442
+
443
+ return true; // Still prevent arrow player from moving through button player
444
+ }
445
+
446
+ updateGuidedSprite() {
447
+ if (!this.shyguySprite.target) return;
448
+
449
+ const dx = this.shyguySprite.target.x - this.shyguySprite.x;
450
+ const dy = this.shyguySprite.target.y - this.shyguySprite.y;
451
+ const distance = Math.sqrt(dx * dx + dy * dy);
452
+
453
+ const moveX = (dx / distance) * this.shyguySprite.speed;
454
+ const moveY = (dy / distance) * this.shyguySprite.speed;
455
+
456
+ let newX = this.shyguySprite.x + moveX;
457
+ let newY = this.shyguySprite.y + moveY;
458
+
459
+ // Check wall collision first
460
+ if (!this.checkWallCollision(this.shyguySprite, newX, newY)) {
461
+ const willCollide = this.checkSpriteCollision(newX, newY, this.shyguySprite, this.wingmanSprite);
462
+
463
+ if (willCollide) {
464
+ if (this.pushEnabled) {
465
+ // Push mechanics enabled - try to push wingman
466
+ const pushSpeed = Math.max(0, this.shyguySprite.speed - this.wingmanSprite.speed);
467
+
468
+ if (pushSpeed > 0) {
469
+ let wingmanNewX = this.wingmanSprite.x + moveX;
470
+ let wingmanNewY = this.wingmanSprite.y + moveY;
471
+
472
+ if (!this.checkWallCollision(this.wingmanSprite, wingmanNewX, wingmanNewY)) {
473
+ this.wingmanSprite.x = wingmanNewX;
474
+ this.wingmanSprite.y = wingmanNewY;
475
+ this.shyguySprite.x = newX;
476
+ this.shyguySprite.y = newY;
477
+ this.shyguySprite.moving = true;
478
+ }
479
+ }
480
+ }
481
+
482
+ // If push is disabled or push failed, try to path around
483
+ if (this.shyguySprite.x === newX && this.shyguySprite.y === newY) {
484
+ const leftPath = { x: newX - this.wingmanSprite.width, y: newY };
485
+ const rightPath = { x: newX + this.wingmanSprite.width, y: newY };
486
+ const upPath = { x: newX, y: newY - this.wingmanSprite.height };
487
+ const downPath = { x: newX, y: newY + this.wingmanSprite.height };
488
+
489
+ const paths = [leftPath, rightPath, upPath, downPath];
490
+ let bestPath = null;
491
+ let bestDistance = Infinity;
492
+
493
+ for (const path of paths) {
494
+ if (
495
+ !this.checkWallCollision(this.shyguySprite, path.x, path.y) &&
496
+ !this.checkSpriteCollision(path.x, path.y, this.shyguySprite, this.wingmanSprite)
497
+ ) {
498
+ const pathDistance = Math.sqrt(
499
+ Math.pow(this.shyguySprite.target.x - path.x, 2) + Math.pow(this.shyguySprite.target.y - path.y, 2)
500
+ );
501
+ if (pathDistance < bestDistance) {
502
+ bestDistance = pathDistance;
503
+ bestPath = path;
504
+ }
505
+ }
506
+ }
507
+
508
+ if (bestPath) {
509
+ this.shyguySprite.x = bestPath.x;
510
+ this.shyguySprite.y = bestPath.y;
511
+ this.shyguySprite.moving = true;
512
+ }
513
+ }
514
+ } else {
515
+ // No collision, proceed normally
516
+ this.shyguySprite.x = newX;
517
+ this.shyguySprite.y = newY;
518
+ this.shyguySprite.moving = true;
519
+ }
520
+ }
521
+ }
522
+
523
+ updateSprite() {
524
+ let newX = this.wingmanSprite.x;
525
+ let newY = this.wingmanSprite.y;
526
+ let isMoving = false;
527
+
528
+ if (this.keys.ArrowUp) {
529
+ newY -= this.wingmanSprite.speed;
530
+ isMoving = true;
531
+ }
532
+ if (this.keys.ArrowDown) {
533
+ newY += this.wingmanSprite.speed;
534
+ isMoving = true;
535
+ }
536
+ if (this.keys.ArrowLeft) {
537
+ newX -= this.wingmanSprite.speed;
538
+ this.wingmanSprite.frameY = 0; // left
539
+ isMoving = true;
540
+ }
541
+ if (this.keys.ArrowRight) {
542
+ newX += this.wingmanSprite.speed;
543
+ this.wingmanSprite.frameY = 1; // right
544
+ isMoving = true;
545
+ }
546
+
547
+ // Check wall collision first
548
+ if (!this.checkWallCollision(this.wingmanSprite, newX, newY)) {
549
+ // Check collision with shyguy
550
+ const willCollide = this.checkSpriteCollision(newX, newY, this.wingmanSprite, this.shyguySprite);
551
+
552
+ if (willCollide) {
553
+ if (this.pushEnabled) {
554
+ // Try to push shyguy if push is enabled
555
+ this.handleSpriteCollision(this.wingmanSprite, this.shyguySprite);
556
+ }
557
+ // If push is disabled or push failed, don't move
558
+ return;
559
+ }
560
+
561
+ // No collision, proceed with movement
562
+ this.wingmanSprite.x = newX;
563
+ this.wingmanSprite.y = newY;
564
+ }
565
+
566
+ this.wingmanSprite.moving = isMoving;
567
+ }
568
+
569
+ handleKeyDown(e) {
570
+ if (e.key in this.keys) {
571
+ this.keys[e.key] = true;
572
+ this.wingmanSprite.moving = true;
573
+ } else if (e.key === "Enter" && this.currentView === "game" && !e.shiftKey) {
574
+ e.preventDefault();
575
+ this.handleSendMessage();
576
+ }
577
+ }
578
+
579
+ handleKeyUp(e) {
580
+ if (e.key in this.keys) {
581
+ this.keys[e.key] = false;
582
+ this.wingmanSprite.moving = Object.values(this.keys).some((key) => key);
583
+ }
584
+ }
585
+
586
+ setNewTarget(target) {
587
+ if (target && target.enabled) {
588
+ this.shyguySprite.setTarget(target);
589
+ this.updateGuidedSpriteDirection(this.shyguySprite);
590
+ }
591
+ if (!target) {
592
+ this.shyguySprite.setTarget(null);
593
+ }
594
+ }
595
+
596
+ checkTargetReached(sprite, target) {
597
+ // Check if sprite overlaps with target using AABB collision detection
598
+ const spriteLeft = sprite.x;
599
+ const spriteRight = sprite.x + sprite.width;
600
+ const spriteTop = sprite.y;
601
+ const spriteBottom = sprite.y + sprite.height;
602
+
603
+ const targetLeft = target.x;
604
+ const targetRight = target.x + target.width;
605
+ const targetTop = target.y;
606
+ const targetBottom = target.y + target.height;
607
+
608
+ // Check for overlap on both x and y axes
609
+ const xOverlap = spriteRight >= targetLeft && spriteLeft <= targetRight;
610
+ const yOverlap = spriteBottom >= targetTop && spriteTop <= targetBottom;
611
+
612
+ return xOverlap && yOverlap;
613
+ }
614
+
615
+ updateGuidedSpriteDirection(sprite) {
616
+ if (!sprite.target) return;
617
+
618
+ const dx = sprite.target.x - sprite.x;
619
+
620
+ // Update direction based only on horizontal movement
621
+ if (dx !== 0) {
622
+ sprite.frameY = dx > 0 ? 1 : 0; // 0 for right, 1 for left
623
+ }
624
+ }
625
+
626
+ updateSpriteAnimation(sprite) {
627
+ if (sprite.moving) {
628
+ if (this.gameFrame % sprite.frameRate === 0) {
629
+ sprite.frameX = (sprite.frameX + 1) % sprite.frameCount;
630
+ }
631
+ } else {
632
+ sprite.frameX = 0;
633
+ }
634
+ }
635
+
636
+ async update() {
637
+ this.gameFrame++;
638
+
639
+ // Update Shyguy position
640
+ if (this.shyguySprite.target && this.shyguySprite.target.enabled) {
641
+ this.updateGuidedSprite(this.shyguySprite);
642
+ if (this.shyguySprite.moving) {
643
+ this.updateSpriteAnimation(this.shyguySprite);
644
+ }
645
+ }
646
+
647
+ // update Wingman position
648
+ this.updateSprite(this.wingmanSprite);
649
+ if (this.wingmanSprite.moving) {
650
+ this.updateSpriteAnimation(this.wingmanSprite);
651
+ }
652
+
653
+ for (const target of Object.values(this.targets)) {
654
+ const isClose = this.checkTargetReached(this.shyguySprite, target);
655
+
656
+ // TODO: reenable the target so the player can visit it again
657
+ if (!target.enabled) {
658
+ if (!isClose) {
659
+ target.enabled = true;
660
+ }
661
+ continue;
662
+ }
663
+
664
+ if (isClose) {
665
+ // pause the game
666
+ target.enabled = false;
667
+ this.stopShyguyAnimation(target);
668
+
669
+ if (target.label === EXIT_LABEL) {
670
+ this.gameOver = true;
671
+ this.gameSuccessful = false;
672
+ this.setGameOver(true);
673
+ this.switchView("gameOver");
674
+ } else {
675
+ await this.handleDialogueWithStoryEngine(target.label);
676
+ }
677
+ break;
678
+ }
679
+ }
680
+ }
681
+
682
+ async handleDialogueWithStoryEngine(label) {
683
+ this.switchView("dialogue");
684
+ this.hideContinueButton();
685
+
686
+ // Show loading indicator
687
+ const dialogueBox = document.querySelector(".dialogue-box");
688
+ dialogueBox.classList.add("loading");
689
+
690
+ const response = await this.storyEngine.onEncounter(label);
691
+
692
+ // Hide loading indicator
693
+ dialogueBox.classList.remove("loading");
694
+
695
+ // Update character images using class properties
696
+ if (this.leftCharacterImg && response.char2imgpath) {
697
+ this.leftCharacterImg.src = response.char2imgpath;
698
+ this.leftCharacterImg.style.display = "block";
699
+ }
700
+
701
+ if (this.rightCharacterImg && response.char1imgpath) {
702
+ this.rightCharacterImg.src = response.char1imgpath;
703
+ this.rightCharacterImg.style.display = "block";
704
+ }
705
+
706
+ const conversation = response.conversation;
707
+
708
+ // TODO: set the images if they are available
709
+
710
+ for (const message of conversation) {
711
+ const { role, content } = message;
712
+ const label = nameToLabel(role);
713
+ this.addChatMessage(this.dialogueChatContainer, content, label, true);
714
+
715
+ // Only play audio if voice is enabled
716
+ if (this.voiceEnabled) {
717
+ try {
718
+ this.lowerMusicVolumeALot();
719
+ await this.elevenLabsClient.playAudioForCharacter(label, content);
720
+ this.restoreMusicVolume();
721
+ } catch (error) {
722
+ console.error("Error playing audio:", label);
723
+ }
724
+ }
725
+ }
726
+
727
+ if (response.gameSuccesful) {
728
+ this.gameOver = true;
729
+ this.gameSuccessful = true;
730
+ } else if (response.gameOver) {
731
+ this.gameOver = true;
732
+ this.gameSuccessful = false;
733
+ } else {
734
+ this.gameOver = false;
735
+ this.gameSuccessful = false;
736
+ }
737
+
738
+ this.showContinueButton();
739
+ }
740
+
741
+ stopShyguyAnimation(target) {
742
+ this.shyguySprite.moving = false;
743
+ this.shyguySprite.frameX = 0;
744
+ this.shyguySprite.target = null;
745
+ }
746
+
747
+ draw() {
748
+ this.ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
749
+
750
+ // Draw grid map
751
+ for (let row = 0; row < this.gridRows; row++) {
752
+ for (let col = 0; col < this.gridCols; col++) {
753
+ const x = col * this.wall.width;
754
+ const y = row * this.wall.height;
755
+
756
+ if (this.backgroundGridMap[row][col] === this.gridMapTypes.wall) {
757
+ this.ctx.drawImage(this.wall.image, x, y, this.wall.width, this.wall.height);
758
+ } else if (this.backgroundGridMap[row][col] === this.gridMapTypes.floor) {
759
+ this.ctx.drawImage(this.floor.image, x, y, this.floor.width, this.floor.height);
760
+ } else if (this.backgroundGridMap[row][col] === this.gridMapTypes.door) {
761
+ this.ctx.drawImage(this.door.image, x, y, this.door.width, this.door.height);
762
+ }
763
+ }
764
+ }
765
+
766
+ this.drawTargetSprite(this.jessicaSprite, this.targets.girl);
767
+ this.drawTargetSprite(this.barSprite, this.targets.bar);
768
+ this.drawTargetSprite(this.djSprite, this.targets.dj);
769
+ this.drawTargetSprite(this.sisterSprite, this.targets.sister);
770
+
771
+ // Draw shyguy
772
+ this.ctx.drawImage(
773
+ this.shyguySprite.image,
774
+ this.shyguySprite.frameX * this.shyguySprite.width,
775
+ this.shyguySprite.frameY * this.shyguySprite.height,
776
+ this.shyguySprite.width,
777
+ this.shyguySprite.height,
778
+ this.shyguySprite.x,
779
+ this.shyguySprite.y,
780
+ this.shyguySprite.width,
781
+ this.shyguySprite.height
782
+ );
783
+
784
+ // Draw wingman
785
+ this.ctx.drawImage(
786
+ this.wingmanSprite.image,
787
+ this.wingmanSprite.frameX * this.wingmanSprite.width,
788
+ this.wingmanSprite.frameY * this.wingmanSprite.height,
789
+ this.wingmanSprite.width,
790
+ this.wingmanSprite.height,
791
+ this.wingmanSprite.x,
792
+ this.wingmanSprite.y,
793
+ this.wingmanSprite.width,
794
+ this.wingmanSprite.height
795
+ );
796
+ }
797
+
798
+ drawTargetSprite(sprite, target) {
799
+ this.ctx.drawImage(sprite.image, target.x, target.y, target.width, target.height);
800
+ }
801
+
802
+ switchView(viewName) {
803
+ if (viewName === this.currentView) return;
804
+
805
+ this.currentView = viewName;
806
+
807
+ // Hide all views first
808
+ this.introView.classList.remove("active");
809
+ this.gameView.classList.remove("active");
810
+ this.dialogueView.classList.remove("active");
811
+ this.gameOverView.classList.remove("active");
812
+
813
+ // Show the requested view
814
+ switch (viewName) {
815
+ case "intro":
816
+ this.introView.classList.add("active");
817
+ break;
818
+ case "game":
819
+ this.gameView.classList.add("active");
820
+ break;
821
+ case "dialogue":
822
+ this.dialogueView.classList.add("active");
823
+ break;
824
+ case "gameOver":
825
+ this.gameOverView.classList.add("active");
826
+ break;
827
+ }
828
+ }
829
+
830
+ enablePush() {
831
+ this.pushEnabled = true;
832
+ }
833
+
834
+ disablePush() {
835
+ this.pushEnabled = false;
836
+ }
837
+
838
+ initDebugControls() {
839
+ const targetDoorBtn = document.getElementById("targetDoorBtn");
840
+ const targetGirlBtn = document.getElementById("targetGirlBtn");
841
+ const targetBarBtn = document.getElementById("targetBarBtn");
842
+ const targetDjBtn = document.getElementById("targetDjBtn");
843
+ const targetSisterBtn = document.getElementById("targetSisterBtn");
844
+ const stopNavBtn = document.getElementById("stopNavBtn");
845
+ const togglePushBtn = document.getElementById("togglePushBtn");
846
+ const speedBoostBtn = document.getElementById("speedBoostBtn");
847
+ const toggleVoiceBtn = document.getElementById("toggleVoiceBtn");
848
+
849
+ targetDoorBtn.addEventListener("click", () => this.setNewTarget(this.targets.exit));
850
+ targetGirlBtn.addEventListener("click", () => this.setNewTarget(this.targets.girl));
851
+ targetBarBtn.addEventListener("click", () => this.setNewTarget(this.targets.bar));
852
+ targetDjBtn.addEventListener("click", () => this.setNewTarget(this.targets.dj));
853
+ targetSisterBtn.addEventListener("click", () => this.setNewTarget(this.targets.sister));
854
+ stopNavBtn.addEventListener("click", () => this.setNewTarget(null));
855
+
856
+ // Add push mechanics toggle
857
+ togglePushBtn.addEventListener("click", () => {
858
+ if (this.pushEnabled) {
859
+ this.disablePush();
860
+ } else {
861
+ this.enablePush();
862
+ }
863
+ togglePushBtn.textContent = this.pushEnabled ? "Disable Push" : "Enable Push";
864
+ });
865
+
866
+ // Add speed boost toggle
867
+ speedBoostBtn.addEventListener("click", () => {
868
+ if (this.shyguySprite.speed === SHYGUY_SPEED) {
869
+ this.shyguySprite.setSpeed(10);
870
+ speedBoostBtn.textContent = "Normal Speed";
871
+ } else {
872
+ this.shyguySprite.setSpeed(SHYGUY_SPEED);
873
+ speedBoostBtn.textContent = "Speed Boost";
874
+ }
875
+ });
876
+
877
+ // Add voice toggle handler
878
+ toggleVoiceBtn.addEventListener("click", () => {
879
+ this.voiceEnabled = !this.voiceEnabled;
880
+ toggleVoiceBtn.textContent = this.voiceEnabled ? "Disable Voice" : "Enable Voice";
881
+ });
882
+ }
883
+
884
+ // Update status text
885
+ updateStatus(message) {
886
+ const statusText = document.getElementById("statusText");
887
+ if (statusText) {
888
+ statusText.textContent = message;
889
+ }
890
+ }
891
+
892
+ clearChat(container) {
893
+ if (container) {
894
+ container.innerHTML = "";
895
+ }
896
+ }
897
+
898
+ addChatMessage(container, message, character, shyguyIsMain) {
899
+ if (!container) return;
900
+
901
+ const isMain = shyguyIsMain ? character === SHYGUY_LABEL : character !== SHYGUY_LABEL;
902
+
903
+ const messageDiv = document.createElement("div");
904
+ messageDiv.className = `chat-message ${isMain ? "right-user" : "left-user"}`;
905
+
906
+ const bubble = document.createElement("div");
907
+ bubble.className = "message-bubble";
908
+ bubble.textContent = message;
909
+
910
+ messageDiv.appendChild(bubble);
911
+ container.appendChild(messageDiv);
912
+
913
+ // Auto scroll to bottom
914
+ container.scrollTop = container.scrollHeight;
915
+ }
916
+
917
+ resolveAction(action) {
918
+ // TODO: resolve the action
919
+ switch (action) {
920
+ case "stay_idle":
921
+ this.setNewTarget(null);
922
+ break;
923
+ case "go_bar":
924
+ this.setNewTarget(this.targets.bar);
925
+ break;
926
+ case "go_dj":
927
+ this.setNewTarget(this.targets.dj);
928
+ break;
929
+ case "go_sister":
930
+ this.setNewTarget(this.targets.sister);
931
+ break;
932
+ case "go_girl":
933
+ this.setNewTarget(this.targets.girl);
934
+ break;
935
+ case "go_home":
936
+ this.setNewTarget(this.targets.exit);
937
+ break;
938
+ default:
939
+ break;
940
+ }
941
+ }
942
+
943
+ async sendMessageToShyguy(message) {
944
+ this.addChatMessage(this.gameChatContainer, message, WINGMAN_LABEL, false);
945
+ this.messageInput.value = "";
946
+
947
+ this.shyguyLLM.getShyGuyResponse(message).then(async (response) => {
948
+ const dialogue = response.dialogue;
949
+ const action = response.action;
950
+
951
+ this.addChatMessage(this.gameChatContainer, dialogue, SHYGUY_LABEL, false);
952
+
953
+ // Only play audio if voice is enabled
954
+ if (this.voiceEnabled) {
955
+ this.disableGameInput();
956
+ this.lowerMusicVolumeALot();
957
+ await this.elevenLabsClient.playAudioForCharacter(SHYGUY_LABEL, dialogue);
958
+ this.enableGameInput();
959
+ this.restoreMusicVolume();
960
+ }
961
+
962
+ // TODO: save conversation history
963
+ await this.shyguy.learnFromWingman(message);
964
+ console.log("[ShyguyLLM]: Next action: ", action);
965
+ this.resolveAction(action);
966
+ });
967
+ }
968
+
969
+ async handleSendMessage() {
970
+ const message = this.messageInput.value.trim();
971
+ if (message.length === 0) return;
972
+ this.sendMessageToShyguy(message);
973
+ }
974
+
975
+ async run() {
976
+ // wait for 16ms
977
+ await new Promise((resolve) => setTimeout(resolve, 16));
978
+ await this.update();
979
+ this.draw();
980
+ if (this.shouldContinue) {
981
+ requestAnimationFrame(this.run);
982
+ }
983
+ }
984
+
985
+ handlePlayAgain() {
986
+ this.clearChat(this.gameChatContainer);
987
+ this.resetGame();
988
+ this.switchView("game");
989
+ }
990
+
991
+ async handleMicrophone() {
992
+ if (!this.isRecording) {
993
+ // Start recording
994
+ this.isRecording = true;
995
+ this.microphoneButton.classList.add("recording");
996
+ this.microphoneButton.innerHTML = '<i class="fas fa-stop"></i>';
997
+
998
+ // Lower music volume while recording
999
+ this.lowerMusicVolumeALot();
1000
+ await this.speechToTextClient.startRecording();
1001
+ } else {
1002
+ // Stop recording
1003
+ this.isRecording = false;
1004
+ this.microphoneButton.classList.remove("recording");
1005
+ this.microphoneButton.innerHTML = '<i class="fas fa-microphone"></i>';
1006
+
1007
+ const result = await this.speechToTextClient.stopRecording();
1008
+ // Restore music volume after recording
1009
+ this.restoreMusicVolume();
1010
+ this.sendMessageToShyguy(result.text);
1011
+ }
1012
+ }
1013
+
1014
+ showContinueButton() {
1015
+ this.dialogueContinueButton.style.display = "block";
1016
+ }
1017
+
1018
+ hideContinueButton() {
1019
+ this.dialogueContinueButton.style.display = "none";
1020
+ }
1021
+
1022
+ setGameOver(fromExit) {
1023
+ this.stopBackgroundMusic();
1024
+
1025
+ if (this.gameSuccessful) {
1026
+ this.gameOverImage.src = "assets/assets/victory.png";
1027
+ this.playVictoryMusic();
1028
+ } else {
1029
+ this.gameOverImage.src = "assets/assets/game-over.png";
1030
+ this.playGameOverMusic();
1031
+ }
1032
+
1033
+ if (fromExit) {
1034
+ this.gameOverText.textContent = "You lost! Shyguy ran away!";
1035
+ return;
1036
+ }
1037
+
1038
+ this.gameOverText.textContent = this.gameSuccessful
1039
+ ? "You won! Shyguy got a date!"
1040
+ : "You lost! Shyguy got rejected!";
1041
+ }
1042
+
1043
+ handleDialogueContinue() {
1044
+ this.clearChat(this.dialogueChatContainer);
1045
+
1046
+ // Hide character images
1047
+ const leftCharacterImg = document.getElementById("leftCharacterImg");
1048
+ const rightCharacterImg = document.getElementById("rightCharacterImg");
1049
+
1050
+ if (leftCharacterImg) {
1051
+ leftCharacterImg.style.display = "none";
1052
+ }
1053
+ if (rightCharacterImg) {
1054
+ rightCharacterImg.style.display = "none";
1055
+ }
1056
+
1057
+ // decide if game is over
1058
+ if (this.gameOver) {
1059
+ this.setGameOver(false);
1060
+ this.switchView("gameOver");
1061
+ return;
1062
+ }
1063
+
1064
+ // Enable push if shyguy has had at least one beer
1065
+ if (this.shyguy.num_beers > 0) {
1066
+ this.enablePush();
1067
+ }
1068
+
1069
+ this.switchView("game");
1070
+ this.shyguyLLM.getShyGuyResponse("").then((response) => {
1071
+ const next_action = response.action;
1072
+
1073
+ this.resolveAction(next_action);
1074
+ });
1075
+ }
1076
+
1077
+ disableGameInput() {
1078
+ this.sendButton.setAttribute("disabled", "");
1079
+ this.microphoneButton.setAttribute("disabled", "");
1080
+ this.messageInput.setAttribute("disabled", "");
1081
+ }
1082
+
1083
+ enableGameInput() {
1084
+ this.sendButton.removeAttribute("disabled");
1085
+ this.microphoneButton.removeAttribute("disabled");
1086
+ this.messageInput.removeAttribute("disabled");
1087
+ }
1088
+
1089
+ playBackgroundMusic() {
1090
+ this.backgroundMusic.play().catch(error => {
1091
+ console.error("Error playing background music:", error);
1092
+ });
1093
+ }
1094
+
1095
+ stopBackgroundMusic() {
1096
+ this.backgroundMusic.pause();
1097
+ this.backgroundMusic.currentTime = 0;
1098
+ }
1099
+
1100
+ playGameOverMusic() {
1101
+ this.gameOverMusic.play().catch(error => {
1102
+ console.error("Error playing game over music:", error);
1103
+ });
1104
+ }
1105
+
1106
+ playVictoryMusic() {
1107
+ this.victoryMusic.play().catch(error => {
1108
+ console.error("Error playing victory music:", error);
1109
+ });
1110
+ }
1111
+
1112
+ stopAllMusic() {
1113
+ this.stopBackgroundMusic();
1114
+ this.gameOverMusic.pause();
1115
+ this.gameOverMusic.currentTime = 0;
1116
+ this.victoryMusic.pause();
1117
+ this.victoryMusic.currentTime = 0;
1118
+ }
1119
+
1120
+ lowerMusicVolume() {
1121
+ // Store original volumes if not already stored
1122
+ if (!this.originalVolumes) {
1123
+ this.originalVolumes = {
1124
+ background: this.backgroundMusic.volume,
1125
+ gameOver: this.gameOverMusic.volume,
1126
+ victory: this.victoryMusic.volume
1127
+ };
1128
+ }
1129
+
1130
+ // Lower all music volumes to 20% of their original values
1131
+ this.backgroundMusic.volume = this.originalVolumes.background * 0.2;
1132
+ this.gameOverMusic.volume = this.originalVolumes.gameOver * 0.2;
1133
+ this.victoryMusic.volume = this.originalVolumes.victory * 0.2;
1134
+ }
1135
+ lowerMusicVolumeALot() {
1136
+ // Store original volumes if not already stored
1137
+ if (!this.originalVolumes) {
1138
+ this.originalVolumes = {
1139
+ background: this.backgroundMusic.volume,
1140
+ gameOver: this.gameOverMusic.volume,
1141
+ victory: this.victoryMusic.volume
1142
+ };
1143
+ }
1144
+
1145
+ // Lower all music volumes to 20% of their original values
1146
+ this.backgroundMusic.volume = this.originalVolumes.background * 0.01;
1147
+ this.gameOverMusic.volume = this.originalVolumes.gameOver * 0.01;
1148
+ this.victoryMusic.volume = this.originalVolumes.victory * 0.01;
1149
+ }
1150
+
1151
+ restoreMusicVolume() {
1152
+ // Restore original volumes if they exist
1153
+ if (this.originalVolumes) {
1154
+ this.backgroundMusic.volume = this.originalVolumes.background * 0.2;
1155
+ this.gameOverMusic.volume = this.originalVolumes.gameOver * 0.2;
1156
+ this.victoryMusic.volume = this.originalVolumes.victory * 0.2;
1157
+ }
1158
+ }
1159
+ }
src/index.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ import { Game } from "./game.js";
2
+
3
+
4
+
5
+ // start the game when DOM is loaded
6
+ document.addEventListener("DOMContentLoaded", () => {
7
+ const game = new Game();
8
+ game.run();
9
+ });
src/llm.js ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class LLM {
2
+ constructor() {
3
+ this.apiKey = "vvO2N5PA9dj8cO6BwcIB8oH4YRnQI3Tn";
4
+ }
5
+
6
+ async getChatCompletion(systemPrompt, userInput) {
7
+ const messages = [
8
+ {
9
+ role: "system",
10
+ content: systemPrompt,
11
+ },
12
+ {
13
+ role: "user",
14
+ content: userInput,
15
+ },
16
+ ];
17
+
18
+ try {
19
+ const response = await fetch("https://api.mistral.ai/v1/chat/completions", {
20
+ method: "POST",
21
+ headers: {
22
+ "Content-Type": "application/json",
23
+ Authorization: `Bearer ${this.apiKey}`,
24
+ },
25
+ body: JSON.stringify({
26
+ model: "mistral-large-latest",
27
+ messages: messages,
28
+ temperature: 0.7,
29
+ max_tokens: 150,
30
+ }),
31
+ });
32
+
33
+ const data = await response.json();
34
+ return data.choices[0].message.content;
35
+ } catch (error) {
36
+ console.error("LLM Error:", error);
37
+ throw error;
38
+ }
39
+ }
40
+
41
+ async #getFunctionCall(systemPrompt, userInput, tools) {
42
+ const messages = [
43
+ {
44
+ role: "system",
45
+ content: systemPrompt,
46
+ },
47
+ {
48
+ role: "user",
49
+ content: userInput,
50
+ },
51
+ ];
52
+
53
+ try {
54
+ const response = await fetch("https://api.mistral.ai/v1/chat/completions", {
55
+ method: "POST",
56
+ headers: {
57
+ "Content-Type": "application/json",
58
+ Authorization: `Bearer ${this.apiKey}`,
59
+ },
60
+ body: JSON.stringify({
61
+ model: "mistral-large-latest",
62
+ messages: messages,
63
+ tools: tools,
64
+ tool_choice: "any", // Forces tool use
65
+ }),
66
+ });
67
+
68
+ const data = await response.json();
69
+
70
+ // Extract function call details from the response
71
+ const toolCall = data.choices[0].message.tool_calls[0];
72
+ return {
73
+ functionName: toolCall.function.name,
74
+ arguments: JSON.parse(toolCall.function.arguments),
75
+ toolCallId: toolCall.id,
76
+ };
77
+ } catch (error) {
78
+ console.error("Function Call Error:", error);
79
+ throw error;
80
+ }
81
+ }
82
+
83
+ async getFunctionKey(functionDescriptions, prompt) {
84
+ // Convert the key-value pairs into the tools format required by the API
85
+ const tools = functionDescriptions.map(({ key, description, parameters = {} }) => ({
86
+ type: "function",
87
+ function: {
88
+ name: key,
89
+ description: description,
90
+ parameters: {
91
+ type: "object",
92
+ properties: {
93
+ ...Object.fromEntries(
94
+ Object.entries(parameters).map(([paramName, paramConfig]) => [
95
+ paramName,
96
+ {
97
+ type: paramConfig.type || "string", // Use provided type or default to "string"
98
+ description: paramConfig.description,
99
+ },
100
+ ])
101
+ ),
102
+ },
103
+ required: Object.keys(parameters), // Make all parameters required
104
+ },
105
+ },
106
+ }));
107
+
108
+ // Use the private getFunctionCall method to make the API call
109
+ const result = await this.#getFunctionCall(
110
+ "You are a helpful assistant. Based on the user's input, choose the most appropriate function to call.",
111
+ prompt,
112
+ tools
113
+ );
114
+
115
+ return {
116
+ functionName: result.functionName,
117
+ parameters: result.arguments,
118
+ };
119
+ }
120
+
121
+ async getJsonCompletion(systemPrompt, userInput) {
122
+ const messages = [
123
+ {
124
+ role: "system",
125
+ content: systemPrompt,
126
+ },
127
+ {
128
+ role: "user",
129
+ content: userInput,
130
+ },
131
+ ];
132
+
133
+ try {
134
+ const response = await fetch("https://api.mistral.ai/v1/chat/completions", {
135
+ method: "POST",
136
+ headers: {
137
+ "Content-Type": "application/json",
138
+ Authorization: `Bearer ${this.apiKey}`,
139
+ },
140
+ body: JSON.stringify({
141
+ model: "mistral-large-latest",
142
+ messages: messages,
143
+ temperature: 0.7,
144
+ max_tokens: 256,
145
+ response_format: { type: "json_object" },
146
+ }),
147
+ });
148
+
149
+ const data = await response.json();
150
+ console.log(data);
151
+ return JSON.parse(data.choices[0].message.content);
152
+ } catch (error) {
153
+ console.error("JSON LLM Error:", error);
154
+ throw error;
155
+ }
156
+ }
157
+ }
158
+
159
+ export default LLM;
160
+
161
+ // Function call Usage example
162
+ // const functionDescriptions = [
163
+ // {
164
+ // key: "searchProducts",
165
+ // description: "Search for products in the catalog",
166
+ // parameters: {
167
+ // query: {
168
+ // type: "string",
169
+ // description: "Search query"
170
+ // },
171
+ // maxPrice: {
172
+ // type: "number",
173
+ // description: "Maximum price filter"
174
+ // },
175
+ // inStock: {
176
+ // type: "boolean",
177
+ // description: "Filter for in-stock items only"
178
+ // }
179
+ // }
180
+ // }
181
+ // ];
182
+
183
+ // const result = await llm.getFunctionKey(functionDescriptions, "Find red shoes in footwear");
src/shyguy.js ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import LLM from "./llm";
2
+
3
+ export class Shyguy {
4
+ constructor() {
5
+ this.num_beers = 0;
6
+ this.courage = 1;
7
+ this.personality = "This is the Shyguy. He is shy and introverted. He is also a bit of a nerd. He fell in love with Jessica. With Jessica, he talks about algorithms.";
8
+ this.lessons_learned = "";
9
+ this.conversation_history = "";
10
+ this.song_playing = "Let it be";
11
+ this.imgpath = "assets/assets/shyguy_headshot.jpeg";
12
+ this.last_actions = [];
13
+ }
14
+
15
+ getSystemPrompt() {
16
+ if (this.num_beers >= 3) {
17
+ return `${this.personality}. His courage is ${this.courage} on the level 1 to 10. If his courage is higher than 5, he is self-confident. He had too many beers and he is drunk. He talks about how drunk he is. Follow the following lessons: ${this.lessons_learned}`;
18
+ } else if (this.num_beers == 2) {
19
+ return `This is Shyguy. He had two beers, so he feels relaxed and he can talk with anyone. Follow the following lessons: ${this.lessons_learned}`;
20
+ }
21
+ else {
22
+ return `${this.personality}. He had ${this.num_beers} numbers of beers and his courage is ${this.courage} on the level 1 to 10. If his courage is higher than 5, he is self-confident. After having 3 bears, he says single words with a lot of hesitation and says that he feels bad and he talks about how drunk he is. If courage is low, he hesitates to speak. Follow the following lessons: ${this.lessons_learned}`;
23
+ }
24
+ }
25
+
26
+ appendLesson(lesson) {
27
+ this.lessons_learned += lesson + "\n";
28
+ }
29
+
30
+ appendConversationHistory(conversation_history) {
31
+ this.conversation_history += conversation_history + "\n";
32
+ }
33
+
34
+ async learnLesson(entityName){
35
+ const summaryLLM = new LLM();
36
+ const summary = await summaryLLM.getChatCompletion(
37
+ `Summarize in one sentence what Shyguy should say when talking to ${entityName}. Do not confuse Jessica and Jessica's sister. If there is nothing relevant about what to say to Jessica, say Nothing relevant.`,
38
+ this.conversation_history
39
+ );
40
+ this.appendLesson(`When talking to ${entityName}, ${summary}`);
41
+ }
42
+
43
+ async learnFromWingman(wingman_message) {
44
+ const summaryLLM = new LLM();
45
+ console.log("Wingman message: ", wingman_message);
46
+ const summary = await summaryLLM.getChatCompletion(
47
+ `Give a summary of what is learned from the message. Summary is one sentence. The wingman is always talking. For example, if the wingman says "Let's have a beer", the output should be "Shyguy wants a beer". If the wingman says "Let's have vodka", the output should be "Shyguy wants vodka".`,
48
+ wingman_message
49
+ );
50
+ this.appendLesson(summary);
51
+ }
52
+
53
+ getAvailableActions() {
54
+ let actions = {};
55
+ const lastAction = this.last_actions[this.last_actions.length - 1];
56
+
57
+ // When sober, can only go to the bar or home
58
+ if (this.num_beers === 0) {
59
+ actions = {
60
+ "go_bar": {
61
+ description: "Head to the bar.",
62
+ location: "bar",
63
+ },
64
+ "go_home": {
65
+ description: "Give up and head home",
66
+ location: "exit",
67
+ },
68
+ "stay_idle": {
69
+ description: "Stay idle",
70
+ location: "idle",
71
+ }
72
+ };
73
+ }
74
+ else if (this.num_beers >= 2) {
75
+ // After 2+ beers, all actions except going home are available
76
+ actions = {
77
+ "go_bar": {
78
+ description: "Head to the bar for liquid courage",
79
+ location: "bar",
80
+ },
81
+ "go_dj": {
82
+ description: "Talk to the DJ about playing a song",
83
+ location: "dj_booth",
84
+ },
85
+ "go_sister": {
86
+ description: "Approach your crush's sister",
87
+ location: "sister",
88
+ },
89
+ "go_girl": {
90
+ description: "Approach your crush",
91
+ location: "girl",
92
+ }
93
+ };
94
+ } else {
95
+ // After 1 beer but less than 2, all actions are available
96
+ actions = {
97
+ "go_bar": {
98
+ description: "Head to the bar for liquid courage",
99
+ location: "bar",
100
+ },
101
+ "go_home": {
102
+ description: "Give up and head home",
103
+ location: "exit",
104
+ },
105
+ "go_dj": {
106
+ description: "Talk to the DJ about playing a song",
107
+ location: "dj_booth",
108
+ },
109
+ "go_sister": {
110
+ description: "Approach your crush's sister",
111
+ location: "sister",
112
+ },
113
+ "go_girl": {
114
+ description: "Approach your crush",
115
+ location: "girl",
116
+ }
117
+ };
118
+ }
119
+
120
+ // Remove the last action from available actions
121
+ if (lastAction && actions[lastAction]) {
122
+ delete actions[lastAction];
123
+ }
124
+
125
+ return actions;
126
+ }
127
+ }
src/shyguy_llm.js ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { LLM } from "./llm.js";
2
+ export class ShyGuyLLM {
3
+ constructor(shyguy) {
4
+ this.llm = new LLM();
5
+ this.shyguy = shyguy;
6
+ this.currentConversation = []; // Array to store current conversation messages
7
+ const availableActions = this.shyguy.getAvailableActions();
8
+ this.baseSystemPrompt = `You are ShyGuy, a socially awkward character at a homecoming party. You are shy.
9
+ The wingman progressively makes you more confident, and if you drink alcohol.
10
+ You know the wingman, he is your friend. You are talking to him.
11
+ You are willing to have a little alcohol.
12
+ The action should correspond to the dialogue.
13
+ Your responses should be in JSON format with two fields:
14
+ enum action {
15
+ ${Object.keys(availableActions).join(", ")}
16
+ }
17
+ for example :
18
+ {
19
+ "action": "go_bar",
20
+ "dialogue": "Okay, let's go to the bar."
21
+ }`;
22
+ }
23
+
24
+ getSystemPrompt() {
25
+ let addToPrompt = "";
26
+ // Add historical conversation context if it exists
27
+ if (this.shyguy.conversation_history) {
28
+ addToPrompt += `\n\nPrevious interactions:\n${this.shyguy.conversation_history}`;
29
+ }
30
+ if (this.shyguy.num_beers > 0) {
31
+ addToPrompt += `You are likely to go to the DJ or sister.\n`;
32
+ }
33
+ if (this.shyguy.num_beers > 2) {
34
+ addToPrompt += `You are likely to go to the DJ or sister. You are very likely to go to the girl. You feel confident about talking to the girl.\n`;
35
+ }
36
+ if (this.shyguy.num_beers > 3) {
37
+ addToPrompt += `You are drunk and you start talking about how bad you feel. You are annoying. You talk about algorithms and then how bad you feel.\n`;
38
+ }
39
+ if (this.shyguy.courage < 2) {
40
+ addToPrompt += `You are shy. However, you believe wingman.`;
41
+ }
42
+ if (this.shyguy.courage >= 2) {
43
+ addToPrompt += `You talk normally.`;
44
+ }
45
+ if (this.shyguy.courage > 5) {
46
+ addToPrompt += `You are self-confident.`;
47
+ }
48
+ if (this.shyguy.courage > 8) {
49
+ addToPrompt += `You are too self-confident and annoying.`;
50
+ }
51
+ addToPrompt += `\nYou drank ${this.shyguy.num_beers} beers. Your courage is ${this.shyguy.courage}.`;
52
+ if (this.currentConversation.length > 0) {
53
+ addToPrompt += `\n\nCurrent conversation context:\n`;
54
+ this.currentConversation.forEach(msg => {
55
+ addToPrompt += `${msg.role}: ${msg.content}\n`;
56
+ });
57
+ }
58
+
59
+ return this.baseSystemPrompt + addToPrompt;
60
+ }
61
+
62
+ addToCurrentConversation(role, content) {
63
+ this.currentConversation.push({
64
+ role: role,
65
+ content: content
66
+ });
67
+ }
68
+
69
+ clearCurrentConversation() {
70
+ this.currentConversation = [];
71
+ }
72
+
73
+ async getShyGuyResponse(player_message) {
74
+ try {
75
+ const availableActions = this.shyguy.getAvailableActions();
76
+ const actionsPrompt = `\nYour currently available actions are: ${Object.keys(availableActions)
77
+ .map((action) => `\n- ${action}: ${availableActions[action].description}`)
78
+ .join("")}`;
79
+
80
+ // Add the situation to current conversation
81
+ this.addToCurrentConversation('wingman', player_message);
82
+
83
+ const fullPrompt = this.getSystemPrompt() + actionsPrompt;
84
+ const response = await this.llm.getJsonCompletion(fullPrompt, player_message);
85
+
86
+ // Add ShyGuy's response to current conversation
87
+ this.addToCurrentConversation('shyguy', response.dialogue);
88
+
89
+ // Add to overall conversation history
90
+ this.shyguy.conversation_history += `\nShyguy: ${response.dialogue}\n`;
91
+
92
+ // Validate response format
93
+ if (!response.action || !response.dialogue) {
94
+ throw new Error("Invalid response format from LLM");
95
+ }
96
+
97
+ return {
98
+ action: response.action,
99
+ dialogue: response.dialogue,
100
+ };
101
+ } catch (error) {
102
+ console.error("ShyGuy Response Error:", error);
103
+ return {
104
+ action: "go_home",
105
+ dialogue: "Umm... I... uh...",
106
+ };
107
+ }
108
+ }
109
+ }
src/speech_to_text.js ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export class SpeechToTextClient {
2
+ constructor() {
3
+ // this.apiKey = "HF_API_KEY";
4
+ this.isRecording = false;
5
+ this.mediaRecorder = null;
6
+ this.audioChunks = [];
7
+ }
8
+
9
+ async startRecording() {
10
+ try {
11
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
12
+ this.mediaRecorder = new MediaRecorder(stream);
13
+ this.audioChunks = [];
14
+
15
+ this.mediaRecorder.ondataavailable = (event) => {
16
+ this.audioChunks.push(event.data);
17
+ };
18
+
19
+ this.mediaRecorder.start();
20
+ this.isRecording = true;
21
+ } catch (error) {
22
+ console.error("Error starting recording:", error);
23
+ throw error;
24
+ }
25
+ }
26
+
27
+ async stopRecording() {
28
+ return new Promise((resolve, reject) => {
29
+ this.mediaRecorder.onstop = async () => {
30
+ try {
31
+ const audioBlob = new Blob(this.audioChunks, { type: 'audio/webm' });
32
+ const result = await this.transcribeAudio(audioBlob);
33
+ resolve(result);
34
+ } catch (error) {
35
+ reject(error);
36
+ }
37
+ };
38
+
39
+ this.mediaRecorder.stop();
40
+ this.isRecording = false;
41
+ this.mediaRecorder.stream.getTracks().forEach(track => track.stop());
42
+ });
43
+ }
44
+
45
+ async transcribeAudio(audioBlob) {
46
+ try {
47
+ const response = await fetch(
48
+ "https://q86j6jmwc3jujazp.us-east-1.aws.endpoints.huggingface.cloud",
49
+ {
50
+ headers: {
51
+ "Accept": "application/json",
52
+ "Authorization": `Bearer ${this.apiKey}`,
53
+ "Content-Type": "audio/webm"
54
+ },
55
+ method: "POST",
56
+ body: audioBlob,
57
+ }
58
+ );
59
+ const result = await response.json();
60
+ return result;
61
+ } catch (error) {
62
+ console.error("Error transcribing audio:", error);
63
+ throw error;
64
+ }
65
+ }
66
+ }