// OpenAI Whispers API Voice Controlled Game - tomesck2 + gowrann2 CA318 2023// By Kevin James Tomescu - 21435066 - tomesck2 + Niamh Gowran - 21389501 - gowrann2// Initializing variablesvar apikey ="";var mediaRecorder;var audioChunks =[];var recordingLength =1250;// Length of each recording snippet in millisecondsvar isProcessingAudio =false;// flag for audio processingvar canReceiveCommand =true;// flag for receiving commandsvar gravity =0.025;// Gravity effectvar velocity =0;// vertical velocityvar velocityX =0;// Horizontal velocityvar moveSpeed =5;// Speed for left/right movements// variables for side scrolling backgroundvar backgroundX =0;var backgroundSpeed =-0.02;// negative to have it move leftvar score =0;// Initialize scorevar lives =3;// Start with 3 livesvar collisionCooldown =false;// Cooldown for collision detectionvar myObstacle;var pipeFrequency =1500;// Millisecondsvar lastPipeTime =Date.now();var myObstacles =[];//HTML code for the page, including the API key input and the buttons to start recording.//The result of the API call is displayed in the div with id="apiResponse".
document.write(`<div id="gameContainer" style="text-align: center; margin: auto; width: 50%;"><div id="enterkey"><h3>Enter API key</h3>Enter API key:<input style='width:25vw;' maxlength='2000' id="apikey" value=''><button onclick='setkey();'class='ab-normbutton'>Set API key</button></div><canvas id="gameCanvas" width="700" height="500"></canvas><div id="gameInfo"><p>Say your command when the timer hits 0!</p><p>Commands are Go UP,Go DOWN,Go LEFT,Go RIGHT - speak to control your character.</p><div id="timerDisplay"></div><div id="whispersResult"><h3>Whispers API Response:</h3><div id="apiResponse"></div></div><div id="scoreDisplay"><h3>Score:<span id="score">0</span></h3></div><div id="livesDisplay"><h3>Lives:<span id="lives">3</span></h3></div><button onclick="resetGame()">RestartGame</button></div>`);// CSS for the game container
$("#gameContainer").css({
textAlign:"center",
maxWidth:"1000px",
margin:"auto",});// CSS for the canvas
$("#gameCanvas").css({
border:"3px solid black","background-color":"transparent",
margin:"20px 0",// Add margin to separate from other elements});var backgroundImage =newImage();
backgroundImage.crossOrigin ="Anonymous";
backgroundImage.onload =function(){
drawBackground();
drawSquare();};
backgroundImage.src ="https://media.istockphoto.com/id/1171564349/video/retro-land.jpg?s=640x640&k=20&c=UytoK5VuR3nXezFTIryRH6D4mHUE846zUBZhlIJA5cw=";function drawBackground(){// Draw the moving background
ctx.drawImage(backgroundImage, backgroundX,0, canvas.width, canvas.height);// Draw a second image to the right of the first to loop the background
ctx.drawImage(
backgroundImage,
backgroundX + canvas.width,0,
canvas.width,
canvas.height
);}function updateScoreDisplay(){
document.getElementById("score").innerHTML = score;}function updateLivesDisplay(){
document.getElementById("lives").innerHTML = lives;}function startGame(){
myObstacle =new component(10,300,"green",300,120);
gameLoop();// Start the game loop}function gameLoop(){var currentTime =Date.now();//myObstacle.update();// Update the state of the game
updateGameState();// Clear the canvas
ctx.clearRect(0,0, canvas.width, canvas.height);// Update and draw the background
backgroundX += backgroundSpeed;if(backgroundX <=-canvas.width){
backgroundX =0;}
drawBackground();// Draw the square
drawSquare();if(currentTime - lastPipeTime > pipeFrequency){var extraDistance =Math.random()*(400-200)+100;// Random additional distance off-screen between 100 and 300var obstacleStartX = canvas.width + extraDistance;var maxHeight =200;var minHeight =50;var heightTop =Math.floor(Math.random()*(maxHeight - minHeight +1))+ minHeight;var heightBottom =Math.floor(Math.random()*(maxHeight - minHeight +1))+ minHeight;
myObstacles.push(new component(30, heightTop,"green", obstacleStartX,0));
myObstacles.push(new component(30,
heightBottom,"green",
obstacleStartX,
canvas.height - heightBottom
));
lastPipeTime = currentTime;// Adjust pipeFrequency for staggered distances
pipeFrequency =Math.random()*(2500-1500)+1500;// Random frequency between 1.5 to 2.5 seconds}// Update and draw each obstaclefor(var i =0; i < myObstacles.length; i++){
myObstacles[i].x -=0.5;// Move obstacle
myObstacles[i].update();// Draw obstacle}
myObstacles.forEach((obstacle)=>{if(!obstacle.passed && square.x > obstacle.x + obstacle.width){
score++;// Increment score
obstacle.passed =true;// Mark this pipe as passed
console.log("Pipe passed");
updateScoreDisplay();}});if(checkCollision(square, myObstacles)){
alert("You hit a pipe!");if(lives <=0){
gameOver();return;}}// Queue the next loop
requestAnimationFrame(gameLoop);}function gameOver(){
ctx.font ="48px sans-serif";
ctx.fillStyle ="red";
ctx.textAlign ="center";
ctx.fillText("Game Over!", canvas.width /2, canvas.height /2);}function resetGame(){// Reset game variables
score =0;
lives =3;
backgroundX =0;
velocity =0;
velocityX =0;
myObstacles =[];// Reset obstacles array
lastPipeTime =Date.now();// Reset pipe timer// Update displays
updateScoreDisplay();
updateLivesDisplay();// Restart the game loop
requestAnimationFrame(gameLoop);}function updateGameState(){// Update background position for side-scrolling
backgroundX += backgroundSpeed;if(backgroundX <=-canvas.width){
backgroundX =0;}// Apply gravity and vertical movement
velocity += gravity;
square.y += velocity;// Apply horizontal movement
square.x += velocityX;// Implement friction or stopping condition so the square stops moving when commands are not given
velocityX *=0.9;// This will slow down the square's movement over time// Add boundaries to stop the square from moving outside the canvasif(square.x <0){
square.x =0;
velocityX =0;}elseif(square.x + square.size > canvas.width){
square.x = canvas.width - square.size;
velocityX =0;}if(square.y + square.size > canvas.height){
square.y = canvas.height - square.size;
velocity =0;}elseif(square.y <0){
square.y =0;
velocity =0;}}// Set the API key and display a message// credit to https://ancientbrain.com/viewjs.php?world=2850716357 for the starter code for this functionfunction setkey(){
apikey = $("#apikey").val().trim();
$("#enterkey").html("<b>API key has been set. Setting up...</b>");
startGame();
startContinuousRecording().then(()=>{
$("#enterkey").html("<b>Ready to receive commands!</b>");});}function updateTimerDisplay(timeLeft){
$("#timerDisplay").text(timeLeft +"s");}// Start recording audio from the microphone// credit to https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder for mediaRecorder documentation and example
async function startContinuousRecording(){returnnewPromise(async (resolve, reject)=>{try{const stream = await navigator.mediaDevices.getUserMedia({ audio:true});
mediaRecorder =newMediaRecorder(stream);
mediaRecorder.ondataavailable =(event)=>{if(!isProcessingAudio){
audioChunks.push(event.data);
sendAudioToWhispersAPI();}};
setInterval(()=>{if(mediaRecorder.state ==="recording"){
mediaRecorder.stop();}else{
audioChunks =[];
isProcessingAudio =false;// Reset the audio processing flag
mediaRecorder.start();// start recording again}}, recordingLength);
resolve();}catch(error){
reject(error);}});}// Send audio to Flask proxy server and display the result// credit to https://platform.openai.com/docs/guides/speech-to-text for the API documentation// credit to https://developer.mozilla.org/en-US/docs/Web/API/FileReader for the FileReader documentation// credit to https://api.jquery.com/jquery.ajax/ for the jQuery AJAX documentation// credit to https://developer.mozilla.org/en-US/docs/Glossary/Base64 for the base64 documentationfunction sendAudioToWhispersAPI(){
isProcessingAudio =true;// set the flag to true// make sure there are audio chunks availableif(audioChunks.length ===0){
console.error("No audio data available");return;}// Properly construct the Blob from audio chunksconst audioBlob =newBlob(audioChunks,{ type:"audio/wav"});// Check if the created object is indeed a Blobif(!(audioBlob instanceof Blob)){
console.error("Failed to create a Blob:", audioBlob);return;}const reader =newFileReader();
reader.onloadend =function(){const base64data = reader.result;// Prepare the request payload, including the modelconst payload ={
api_endpoint:"https://api.openai.com/v1/audio/transcriptions",// OpenAI Transcriptions API endpoint
api_key: apikey,// API key
model:"whisper-1",// Model name
data: base64data,// Audio data in base64 format, as required by the API};// send request and handle response
$.ajax({
type:"POST",
url:"https://gowtom-proxy.azurewebsites.net/speech-to-text",// URL of our Flask server
data: JSON.stringify(payload),
contentType:"application/json",
success:function(response){
$("#apiResponse").text(JSON.stringify(response,null,2));if(response && response.text){
processApiResponse(response.text);
isProcessingAudio =false;}else{
isProcessingAudio =false;
console.error("Unexpected response format:", response);}},
error:function(_, __, error){
$("#apiResponse").html("<font color='red'><b>An error occurred: "+ error +"</b></font>");},});};
reader.readAsDataURL(audioBlob);// Convert blob to base64}// Game Logicconst canvas = document.getElementById("gameCanvas");const ctx = canvas.getContext("2d");
let square ={ x: canvas.width /2-25, y: canvas.height -50, size:50};function drawSquare(){
ctx.clearRect(0,0, canvas.width, canvas.height);
drawBackground();
ctx.fillStyle ="red";
ctx.fillRect(square.x, square.y, square.size, square.size);}// Process the response from Whispers API to control the game
async function processApiResponse(responseText){if(!canReceiveCommand)return;
canReceiveCommand =false;
await moveSquare(
responseText
.replace(/[^\w\s]/g,"").toLowerCase().trim());var timeLeft = recordingLength /1000;
updateTimerDisplay(timeLeft);var timer = setInterval(()=>{
timeLeft--;
updateTimerDisplay(timeLeft);if(timeLeft <=0){
clearInterval(timer);
canReceiveCommand =true;
updateTimerDisplay(0);}},1000);}function component(width, height, color, x, y){this.width = width;this.height = height;this.x = x;this.y = y;this.passed =false;this.update =function(){
ctx.fillStyle = color;
ctx.fillRect(this.x,this.y,this.width,this.height);};}function checkCollision(square, obstacles){if(collisionCooldown)returnfalse;for(var i =0; i < obstacles.length; i++){var obstacle = obstacles[i];var myleft = square.x;var myright = square.x + square.size;var mytop = square.y;var mybottom = square.y + square.size;var otherleft = obstacle.x;var otherright = obstacle.x + obstacle.width;var othertop = obstacle.y;var otherbottom = obstacle.y + obstacle.height;if(
mybottom < othertop ||
mytop > otherbottom ||
myright < otherleft ||
myleft > otherright
){continue;}
lives -=1;
updateLivesDisplay();
myObstacles.forEach((obstacle)=>(obstacle.passed =false));
collisionCooldown =true;
setTimeout(()=>{
collisionCooldown =false;},1000);// Cooldown for 1 second (adjust as needed)returntrue;// collision detected}returnfalse;// no collision}
async function moveSquare(direction){const commands = direction.toLowerCase().split(" ");// Split the direction into individual commands
commands.forEach((command)=>{if(!["up","op","oop","down","left","right","write","ride","uh","oh",].includes(command)){// If the command is not one of the valid directions, log an error message and return
console.log("Could not move the square, the command was incorrect");return;}// Handle the command and move the characterswitch(command){case"up":case"op":case"oop":case"uh":case"oh":// Negative velocity to move up
velocity =-4;break;case"down":
velocity =2.5;break;case"left":
velocityX =-moveSpeed;break;case"right":case"write":case"ride":// Move the square right by adding the moveAmount to its x-coordinate
velocityX = moveSpeed;break;}});
drawSquare();// Redraw the square on the canvas after moving}// Credits / references// https://ancientbrain.com/viewjs.php?world=2850716357// https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder// https://platform.openai.com/docs/guides/speech-to-text// https://developer.mozilla.org/en-US/docs/Web/API/FileReader// https://api.jquery.com/jquery.ajax/// https://developer.mozilla.org/en-US/docs/Glossary/Base64// https://openai.com/research/whisper// https://github.com/openai/whisper// https://www.youtube.com/watch?v=jj5ADM2uywg Flappy Bird JS Tutorial// https://www.w3schools.com/graphics/game_intro.asp - W3Schools HTML JS Flappy Bird Guide