diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..c585e19389d195ff2268d354ecf676d738a578de --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +out \ No newline at end of file diff --git a/README.md b/README.md index 3e0f3beb5be86a48eb01fccca833d45b6abc5c66..1179e61504d3035f01bb39acca169f48995d6136 100644 --- a/README.md +++ b/README.md @@ -1 +1,50 @@ -# pop-term-2 +# POP Raylib starter + +Simple starter application for POP C assignment. + +Please see materials on Blackboard for more details. + +# Building + +To build run the command: + +```bash +/opt/pop/bin/build-wasm.sh src/main.c +``` + +This will generate a directory *out* with the WASM and index.html files for the +Raylib program. + +# Running + +The very first time you run a POP WASM application you must run the command: + +```bash +/opt/pop/bin/allocate_port.sh +``` + +You might need to start a new terminal instance for the update to take effect. +To check that everything is fine run the command: + +```bash +echo $MY_PORT +``` + +This should output a 5 digit number. + + +To run the Raylib program in *out* simply run the command: + +```bash +/opt/pop/bin/run-wasm.sh +``` + +This will run a web server that serves the *out* on the port you allocated above. This is forwarded from the +remote server to your local machine, which means you can simply open the corresponding web page within a browser +on your local machine using the address: + +```bash +localhost:XXXXX +``` + +where *XXXXX* is the port number you allocated above. \ No newline at end of file diff --git a/resources/pop_logo.png b/resources/pop_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3709225ac8c45af2627a0803eb88ef8fe1ece9f4 Binary files /dev/null and b/resources/pop_logo.png differ diff --git a/resources/pop_logo_small.png b/resources/pop_logo_small.png new file mode 100644 index 0000000000000000000000000000000000000000..f04460cce44ddff7288b71e6dee158d9e670cf14 Binary files /dev/null and b/resources/pop_logo_small.png differ diff --git a/shell.html b/shell.html new file mode 100644 index 0000000000000000000000000000000000000000..71defad5c4d92a412be460fbb083984b8ddba42f --- /dev/null +++ b/shell.html @@ -0,0 +1,301 @@ +<!doctype html> +<html lang="EN-us"> + <head> + <meta charset="utf-8"> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> + + <title>POP Web Game</title> + + <meta name="title" content="POP Raylib Web game"> + <meta name="description" content="POP game using raylib"> + <meta name="keywords" content="raylib, programming, examples, html5, C, C++, library, learn, games, videogames"> + <meta name="viewport" content="width=device-width"> + + + <!-- Favicon --> + <link rel="shortcut icon" href="https://www.raylib.com/favicon.ico"> + + <style> + body { + font-family: arial; + margin: 0; + padding: none; + } + + #header { + width: 100%; + height: 80px; + background-color: #888888; + } + + /* NOTE: raylib logo is embedded in the page as base64 png image */ + #logo { + width:64px; + height:64px; + float:left; + position:relative; + margin:10px; + background-image:url('data:image/png;base64,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') + } + + .emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; } + div.emscripten { text-align: center; } + div.emscripten_border { border: 1px solid black; } + + /* NOTE: Canvas *must not* have any border or padding, or mouse coords will be wrong */ + canvas.emscripten { + border: 0px none; + background: black; + width: 512; + height: 512; + } + + .spinner { + height: 30px; + width: 30px; + margin: 0; + margin-top: 20px; + margin-left: 20px; + display: inline-block; + vertical-align: top; + -webkit-animation: rotation .8s linear infinite; + -moz-animation: rotation .8s linear infinite; + -o-animation: rotation .8s linear infinite; + animation: rotation 0.8s linear infinite; + border-left: 5px solid black; + border-right: 5px solid black; + border-bottom: 5px solid black; + border-top: 5px solid red; + border-radius: 100%; + background-color: rgb(245, 245, 245); + } + @-webkit-keyframes rotation { + from {-webkit-transform: rotate(0deg);} + to {-webkit-transform: rotate(360deg);} + } + @-moz-keyframes rotation { + from {-moz-transform: rotate(0deg);} + to {-moz-transform: rotate(360deg);} + } + @-o-keyframes rotation { + from {-o-transform: rotate(0deg);} + to {-o-transform: rotate(360deg);} + } + @keyframes rotation { + from {transform: rotate(0deg);} + to {transform: rotate(360deg);} + } + + #status { + display: inline-block; + vertical-align: top; + margin-top: 30px; + margin-left: 20px; + font-weight: bold; + color: rgb(40, 40, 40); + } + + #progress { + height: 0px; + width: 0px; + } + + #controls { + display: inline-block; + float: right; + vertical-align: top; + margin-top: 15px; + margin-right: 20px; + } + + #output { + width: 100%; + height: 140px; + margin: 0 auto; + margin-top: 10px; + display: block; + background-color: black; + color: rgb(37, 174, 38); + font-family: 'Lucida Console', Monaco, monospace; + outline: none; + } + + input[type=button] { + background-color: lightgray; + border: 4px solid darkgray; + color: black; + text-decoration: none; + cursor: pointer; + width: 140px; + height: 50px; + margin-left: 10px; + } + + input[type=button]:hover { + background-color: #f5f5f5ff; + border-color: black; + } + </style> + </head> + <body> + <div id="header"> + <a id="logo" href="https://www.raylib.com"></a> + + <div class="spinner" id='spinner'></div> + <div class="emscripten" id="status">Downloading...</div> + + <span id='controls'> + <span><input type="button" value="📜 SOURCE CODE" onclick="location.href='https://github.com/raysan5/raylib';"/></span> + <span><input type="button" value="🖵 FULLSCREEN" onclick="Module.requestFullscreen(false, false)"></span> + <span><input type="button" id="btn-audio" value="🔇 MUTE" onclick="toggleAudio()"></span> + </span> + + <div class="emscripten"> + <progress value="0" max="100" id="progress" hidden=1></progress> + </div> + </div> + + <div class="emscripten_border"> + <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()" tabindex=-1></canvas> + </div> + + <textarea id="output" rows="8"></textarea> + + <script type='text/javascript' src="https://cdn.jsdelivr.net/gh/eligrey/FileSaver.js/dist/FileSaver.min.js"> </script> + <script type='text/javascript'> + function saveFileFromMEMFSToDisk(memoryFSname, localFSname) // This can be called by C/C++ code + { + var isSafari = false; // Not supported, navigator.userAgent access is being restricted + //var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); + var data = FS.readFile(memoryFSname); + var blob; + + if (isSafari) blob = new Blob([data.buffer], { type: "application/octet-stream" }); + else blob = new Blob([data.buffer], { type: "application/octet-binary" }); + + // NOTE: SaveAsDialog is a browser setting. For example, in Google Chrome, + // in Settings/Advanced/Downloads section you have a setting: + // 'Ask where to save each file before downloading' - which you can set true/false. + // If you enable this setting it would always ask you and bring the SaveAsDialog + saveAs(blob, localFSname); + } + </script> + <script type='text/javascript'> + var statusElement = document.querySelector('#status'); + var progressElement = document.querySelector('#progress'); + var spinnerElement = document.querySelector('#spinner'); + var Module = { + preRun: [], + postRun: [], + print: (function() { + var element = document.querySelector('#output'); + + if (element) element.value = ''; // Clear browser cache + + return function(text) { + if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); + // These replacements are necessary if you render to raw HTML + //text = text.replace(/&/g, "&"); + //text = text.replace(/</g, "<"); + //text = text.replace(/>/g, ">"); + //text = text.replace('\n', '<br>', 'g'); + console.log(text); + + if (element) { + element.value += text + "\n"; + element.scrollTop = element.scrollHeight; // focus on bottom + } + }; + })(), + printErr: function(text) { + if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' '); + + console.error(text); + }, + canvas: (function() { + var canvas = document.querySelector('#canvas'); + + // As a default initial behavior, pop up an alert when webgl context is lost. + // To make your application robust, you may want to override this behavior before shipping! + // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2 + canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false); + + return canvas; + })(), + setStatus: function(text) { + if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' }; + if (text === Module.setStatus.last.text) return; + + var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/); + var now = Date.now(); + + if (m && now - Module.setStatus.last.time < 30) return; // If this is a progress update, skip it if too soon + + Module.setStatus.last.time = now; + Module.setStatus.last.text = text; + + if (m) { + text = m[1]; + progressElement.value = parseInt(m[2])*100; + progressElement.max = parseInt(m[4])*100; + progressElement.hidden = true; + spinnerElement.hidden = false; + } else { + progressElement.value = null; + progressElement.max = null; + progressElement.hidden = true; + if (!text) spinnerElement.style.display = 'none'; + } + + statusElement.innerHTML = text; + }, + totalDependencies: 0, + monitorRunDependencies: function(left) { + this.totalDependencies = Math.max(this.totalDependencies, left); + Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.'); + }, + //noInitialRun: true + }; + + Module.setStatus('Downloading...'); + + window.onerror = function() { + Module.setStatus('Exception thrown, see JavaScript console'); + spinnerElement.style.display = 'none'; + Module.setStatus = function(text) { if (text) Module.printErr('[post-exception status] ' + text); }; + }; + </script> + + <!-- REF: https://developers.google.com/web/updates/2018/11/web-audio-autoplay --> + <script type='text/javascript'> + var audioBtn = document.querySelector('#btn-audio'); + + // An array of all contexts to resume on the page + const audioContexList = []; + (function() { + // A proxy object to intercept AudioContexts and + // add them to the array for tracking and resuming later + self.AudioContext = new Proxy(self.AudioContext, { + construct(target, args) { + const result = new target(...args); + audioContexList.push(result); + if (result.state == "suspended") audioBtn.value = "🔈 RESUME"; + return result; + } + }); + })(); + + function toggleAudio() { + var resumed = false; + audioContexList.forEach(ctx => { + if (ctx.state == "suspended") { ctx.resume(); resumed = true; } + else if (ctx.state == "running") ctx.suspend(); + }); + + if (resumed) audioBtn.value = "🔇 MUTE"; + else audioBtn.value = "🔈 RESUME"; + } + </script> + {{{ SCRIPT }}} + </body> +</html> \ No newline at end of file diff --git a/src/connect4.c b/src/connect4.c new file mode 100644 index 0000000000000000000000000000000000000000..2d934e9721dd12ec84f2dd9ea189daa058d7efd3 --- /dev/null +++ b/src/connect4.c @@ -0,0 +1,157 @@ +#include "raylib.h" + +//Define how many columns and rows there should be +#define ROWS 6 +#define COLUMNS 7 +#define CELL_SIZE 100 + +enum Cell { + EMPTY, + PLAYER1, + PLAYER2 +}; + +enum Cell board[ROWS][COLUMNS] = { { EMPTY } }; +enum Cell current_player = PLAYER1; +bool game_won = false; +char win_message[64] = ""; +extern char player1Name[64]; +extern char player2Name[64]; + +//Setup falling disc structure and set default below +typedef struct { + bool active; + int col; + float y; + Color color; +} FallingDisc; + +FallingDisc fallingDisc = { false, 0, 0.0f, LIGHTGRAY }; + +void draw_background_grid() { + for (int row = 0; row < ROWS; row++) { + for (int col = 0; col < COLUMNS; col++) { + //Center X, Center Y, radius, colour + DrawCircle(col * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, CELL_SIZE / 2 - 5, LIGHTGRAY); + } + } +} + +void draw_board() { + for (int row = 0; row < ROWS; row++) { + for (int col = 0; col < COLUMNS; col++) { + Color cell_color; + if (board[row][col] == PLAYER1) { + cell_color = RED; + } else if (board[row][col] == PLAYER2) { + cell_color = YELLOW; + } else { + continue; // Skip empty cells; background will handle this + } + DrawCircle(col * CELL_SIZE + CELL_SIZE / 2, row * CELL_SIZE + CELL_SIZE / 2, CELL_SIZE / 2 - 5, cell_color); + } + } +} + + +bool check_win(enum Cell player) { + // Horizontal + for (int row = 0; row < ROWS; row++) { + for (int col = 0; col < COLUMNS - 3; col++) { + if (board[row][col] == player && board[row][col + 1] == player && board[row][col + 2] == player && board[row][col + 3] == player) { + return true; + } + } + } + // Vertical + for (int row = 0; row < ROWS - 3; row++) { + for (int col = 0; col < COLUMNS; col++) { + if (board[row][col] == player && board[row + 1][col] == player && board[row + 2][col] == player && board[row + 3][col] == player) { + return true; + } + } + } + // Top Left to Bottom Right + for (int row = 0; row < ROWS - 3; row++) { + for (int col = 0; col < COLUMNS - 3; col++) { + if (board[row][col] == player && board[row + 1][col + 1] == player && board[row + 2][col + 2] == player && board[row + 3][col + 3] == player) { + return true; + } + } + } + // Top Right to Bottom Left + for (int row = 3; row < ROWS; row++) { + for (int col = 0; col < COLUMNS - 3; col++) { + if (board[row][col] == player && board[row - 1][col + 1] == player && board[row - 2][col + 2] == player && board[row - 3][col + 3] == player) { + return true; + } + } + } + return false; +} + + +void AnimateFallingDisc(int target_row, int target_col, float *falling_y_position) { + // Move the disc down by 10 units in each update + *falling_y_position += 10.0f; + + // Check if disc is at target + if (*falling_y_position >= target_row * CELL_SIZE + CELL_SIZE / 2) { + board[target_row][target_col] = current_player; + + if (check_win(current_player)) { + game_won = true; + sprintf(win_message, "%s Wins!", current_player == PLAYER1 ? player1Name : player2Name); + } + + current_player = (current_player == PLAYER1) ? PLAYER2 : PLAYER1; + // Reset the falling animation for the next turn + *falling_y_position = -1.0f; + } else { + // Draw the disc at the current falling position + DrawCircle(target_col * CELL_SIZE + CELL_SIZE / 2, *falling_y_position, CELL_SIZE / 2 - 5, + (current_player == PLAYER1) ? RED : YELLOW); + } +} + + +void gameStart() { + static int animated_row = -1; + static int animated_col = -1; + static float animated_y = 0.0f; + + if (animated_row >= 0) { + AnimateFallingDisc(animated_row, animated_col, &animated_y); + + // If disc landed reset state + if (animated_y == -1.0f) { + animated_row = -1; + } + } else if (!game_won && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + //Column on mouse click + int column = GetMouseX() / CELL_SIZE; + + // Find the first empty row in the selected column (starting from the bottom) + for (int row = ROWS - 1; row >= 0; row--) { + if (board[row][column] == EMPTY) { + // Set the row and column where the disc will fall + animated_row = row; + animated_col = column; + animated_y = 0.0f; // Reset the Y position to begin animation + break; + } + } + } + + draw_background_grid(); + if (animated_row >= 0) { + DrawCircle(animated_col * CELL_SIZE + CELL_SIZE / 2, animated_y, CELL_SIZE / 2 - 5, + (current_player == PLAYER1) ? RED : YELLOW); + } + draw_board(); + + //Show game win message + if (game_won) { + DrawText(win_message, 10, ROWS * CELL_SIZE + 10, 20, BLACK); + } +} diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000000000000000000000000000000000000..4cf3e45d7b70411432f096c9330a6153e5d2d86f --- /dev/null +++ b/src/main.c @@ -0,0 +1,128 @@ +#include "raylib.h" +#include <stdio.h> +#include <string.h> +#include "connect4.c" + +// Variables for main menu +bool showMainMenu = true; +bool showNameScreen = false; +char player1Name[64] = ""; +char player2Name[64] = ""; +bool enterPlayer1Name = true; + + +/** + * Check if player names are empty. If so set default names + */ +void validateGameNames() { + if (strlen(player1Name) == 0) { + strcpy(player1Name, "Player 1"); + } + if (strlen(player2Name) == 0) { + strcpy(player2Name, "Player 2"); + } +} + +/** + * Checks if button passed through is being hovered over. Returns true if cursor is over button + */ +bool IsMouseOverButton(Rectangle button) { + Vector2 mousePoint = GetMousePosition(); + return CheckCollisionPointRec(mousePoint, button); +} + +// Allow users to enter a username and display that to the screen. +void HandlePlayerNameInput() { + int key = GetKeyPressed(); + char* currentPlayerName = enterPlayer1Name ? player1Name : player2Name; + if (key >= 32 && key <= 126) { + int len = strlen(currentPlayerName); + if (len < 63) { + currentPlayerName[len] = (char)key; + currentPlayerName[len + 1] = '\0'; + } + } + if (IsKeyPressed(KEY_ENTER)) { + //Set flag to false to show player 2 + if (enterPlayer1Name) { + enterPlayer1Name = false; + //Second enter is player 2, show game screen + } else { + showNameScreen = false; + } + } + //Remove last character from the string by adding null terminator + if (IsKeyPressed(KEY_BACKSPACE)) { + int len = strlen(currentPlayerName); + if (len > 0) { + currentPlayerName[len - 1] = '\0'; + } + } +} + +void ShowMainMenu() { + Rectangle startButton = { 100, 150, 325, 40 }; + Rectangle nameButton = { 100, 200, 325, 40 }; + + + DrawText("Connect 4 Game", 100, 100, 20, BLACK); + + if (IsMouseOverButton(startButton)) { + //Draw new rectangle different colour to give hover affect + DrawRectangleRec(startButton, LIGHTGRAY); + //If 'start' button is clicked start game without selecting usernames + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + showMainMenu = false; + } + } else { + //If button is not hovered keep darker gray button on screen + DrawRectangleRec(startButton, GRAY); + } + + //Same as above but for name selection button + if (IsMouseOverButton(nameButton)) { + DrawRectangleRec(nameButton, LIGHTGRAY); + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) { + showMainMenu = false; + showNameScreen = true; + } + } else { + DrawRectangleRec(nameButton, GRAY); + } + DrawText("Start Game (without names)", 110, 160, 20, BLACK); + DrawText("Start Game (name selection)", 110, 210, 20, BLACK); +} + +//Show name screen, allow users to enter username +void ShowNameScreen() { + DrawText("Enter Player Names", 100, 100, 20, BLACK); + if (enterPlayer1Name) { + DrawText("Enter Player 1 Name:", 100, 150, 20, BLACK); + DrawText(player1Name, 350, 150, 20, BLACK); + } else { + DrawText("Enter Player 2 Name:", 100, 150, 20, BLACK); + DrawText(player2Name, 350, 150, 20, BLACK); + } + HandlePlayerNameInput(); +} + +int main(void) { + InitWindow(COLUMNS * CELL_SIZE, ROWS * CELL_SIZE + 100, "Connect 4"); + + while (!WindowShouldClose()) { + BeginDrawing(); + ClearBackground(RAYWHITE); + if (showMainMenu) { + ShowMainMenu(); + } else if (showNameScreen) { + ShowNameScreen(); + } else { + validateGameNames(); + gameStart(); + } + EndDrawing(); + } + + CloseWindow(); + return 0; +}