Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: WebSocket Integration, Web client view, and Dockerization #28

Open
wants to merge 24 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4a51bf9
fix: circular import
SverreNystad Sep 16, 2024
9b17cb6
feat: Add WebSocket support for Tetris game and html client code
SverreNystad Sep 16, 2024
ced6344
refactor: extract singleplayer.js for WebSocket client code
SverreNystad Sep 16, 2024
6e4246f
feat: Improve Tetris game loop with automatic block dropping and adju…
SverreNystad Sep 16, 2024
4bddcc0
feat: Add WebSocket endpoint for game information
SverreNystad Sep 16, 2024
a88d0ed
fix: add JSONResponse import to fix WebSocket game info endpoint
SverreNystad Sep 16, 2024
160fe77
build: Add Dockerfile and docker-compose.yml for containerization
SverreNystad Sep 16, 2024
63bd7dd
feat: Add endpoint to retrieve available agents
SverreNystad Sep 16, 2024
8e3cca2
docs: Add how to run web version to readme
SverreNystad Sep 16, 2024
0e2a8ef
refactor: Improve TetrisWebGameManager code structure and variable na…
SverreNystad Sep 16, 2024
9c6d280
refactor: Rename WebSocket variables in agentplayer.js for clarity
SverreNystad Sep 16, 2024
4626838
refactor: Delete frontend/routes.js
SverreNystad Sep 16, 2024
1452e63
refactor: Improve TetrisWebGameManager code structure and variable na…
SverreNystad Sep 16, 2024
a0a344a
feat: add second canvas element for agent and allow for parallel play
SverreNystad Sep 16, 2024
f148654
refactor: Update WebSocket URLs to use BASE_URL and WS_BASE_URL const…
SverreNystad Sep 17, 2024
6f80ad3
fix: Prevent default spacebar scrolling in agentplayer.js
SverreNystad Sep 17, 2024
b430f74
refactor: Update agentplayer.js to use arrow functions for WebSocket …
SverreNystad Sep 17, 2024
2f2db07
feat: Add "nextPiece" property to json sent to client
SverreNystad Sep 17, 2024
a78ac26
chore: add node to gitignore
SverreNystad Sep 17, 2024
6a9197d
chore: Update .gitignore to include package-lock.json
SverreNystad Sep 17, 2024
5bcc5eb
feat: add highscores post and get endpoints
SverreNystad Sep 17, 2024
18262db
feat: mark landing place with sentinel value when sending game state
SverreNystad Sep 22, 2024
5a6054e
feat: Include "nextBlock" property in game state JSON
SverreNystad Sep 22, 2024
dcca97e
feat: Remove unused "nextPiece" property from game state JSON
SverreNystad Sep 22, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 135 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,138 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

.vscode/
.vscode/


# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
web_modules/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional stylelint cache
.stylelintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Next.js build output
.next
out

# Nuxt.js build / generate output
.nuxt
dist

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# vuepress v2.x temp and cache directory
.temp
.cache

# Docusaurus cache and generated files
.docusaurus

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port

# Stores VSCode versions used for testing VSCode extensions
.vscode-test

# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

**package-lock.json
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Use an official Python runtime as a parent image
FROM python:3.11-slim

# Set the working directory in the container
WORKDIR /app

# Copy the requirements.txt file into the container
COPY requirements.txt .

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application code into the container
COPY . .

# Expose the port the app runs on
EXPOSE 8000

# Run the FastAPI server using uvicorn
CMD ["uvicorn", "web_app:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This project is our attempt at making an AI that can play Tetris. First of all w
- Heuristic agent with set weights
- Genetic algorithm to find the best weights for the heuristic agent

The game is playable/viable both in the terminal and in a GUI. The GUI is made with Pygame.
The game is playable/viable both in the terminal, in a GUI and Web. The GUI is made with Pygame.

## How to run and install

Expand All @@ -38,6 +38,7 @@ pip install -r requirements.txt

## Usage

### GUI version
To play the game yourself, run the following command:

```bash
Expand All @@ -58,6 +59,14 @@ To train the genetic agent, run the following command:
python main.py train
```

### Web version
To run the web version of the game, run the following command:

```bash
docker compose up --build
```
Then go to `http://localhost:80` in your browser.

## Testing

To run the test suite, run the following command from the root directory of the project:
Expand Down
24 changes: 24 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
version: "3"

services:
backend:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
networks:
- app-network

frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "80:80"
networks:
- app-network

networks:
app-network:
driver: bridge
8 changes: 8 additions & 0 deletions frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Use the official Nginx image to serve the frontend
FROM nginx:alpine

# Copy the frontend files into the appropriate Nginx directory
COPY . /usr/share/nginx/html

# Expose port 80 for the frontend
EXPOSE 80
64 changes: 64 additions & 0 deletions frontend/agentplayer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { BASE_URL, WS_BASE_URL } from "./routes.js";
import { drawBoard } from "./tetris-common.js";

// DOM elements
const agentSelect = document.getElementById("agent-select");
const startDemoBtn = document.getElementById("start-demo");
let agentWebSocket = null;

const canvasAgentId = "agentplayer-canvas";

// Fetch available agents from the server and populate the dropdown
async function loadAgents() {
const response = await fetch(`${BASE_URL}/agents`);
const agents = await response.json();

agents.forEach((agent) => {
const option = document.createElement("option");
option.value = agent;
option.textContent = agent;
agentSelect.appendChild(option);
});
}

// Start WebSocket connection for agent demo
function startDemo() {
const selectedAgent = agentSelect.value;

// Close the existing WebSocket connection if any
if (agentWebSocket) {
agentWebSocket.close();
}

agentWebSocket = new WebSocket(`${WS_BASE_URL}/demo/${selectedAgent}`);

agentWebSocket.onopen = () => {
console.log(`WebSocket connection established with ${selectedAgent} agent`);
};

agentWebSocket.onmessage = (event) => {
const gameState = JSON.parse(event.data);
console.log("Game state received from server:", gameState);
// Render the updated board using shared function
drawBoard(gameState.board, canvasAgentId);
};

agentWebSocket.onclose = () => {
console.log("WebSocket connection closed");
};

agentWebSocket.onerror = (error) => {
console.error("WebSocket error:", error);
};
}

window.addEventListener("keydown", (e) => {
if (e.key === " ") {
e.preventDefault(); // Prevent default spacebar scrolling
}
});
// Load the agents when the page is loaded
window.addEventListener("load", loadAgents);

// Start the demo when the button is clicked
startDemoBtn.addEventListener("click", startDemo);
21 changes: 21 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<title>TetrisAI</title>
</head>
<body>
<h1>TetrisAI</h1>

<label for="agent-select">Select Agent:</label>
<!-- Get the options from endpoint -->
<select id="agent-select"></select>
<button id="start-demo">Start Demo</button>
<canvas id="agentplayer-canvas" width="400" height="920"></canvas>
<canvas id="singleplayer-canvas" width="400" height="920"></canvas>

<script type="module" src="routes.js"></script>
<script type="module" src="tetris-common.js"></script>
<script type="module" src="singleplayer.js"></script>
<script type="module" src="agentplayer.js"></script>
</body>
</html>
2 changes: 2 additions & 0 deletions frontend/routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const BASE_URL = "http://127.0.0.1:8000";
export const WS_BASE_URL = "ws://127.0.0.1:8000/ws";
40 changes: 40 additions & 0 deletions frontend/singleplayer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { WS_BASE_URL } from "./routes.js";
import { drawBoard } from "./tetris-common.js";

const singleplayerWebSocket = new WebSocket(`${WS_BASE_URL}/game`);
console.log(WS_BASE_URL);

const canvasSinglePlayerId = "singleplayer-canvas";

singleplayerWebSocket.onopen = () => {
console.log("Single-player WebSocket connection established");
};

singleplayerWebSocket.onmessage = (event) => {
const gameState = JSON.parse(event.data);
console.log("Game state received from server:", gameState);
drawBoard(gameState.board, canvasSinglePlayerId);
};

singleplayerWebSocket.onclose = () => {
console.log("Single-player WebSocket connection closed");
};

singleplayerWebSocket.onerror = (error) => {
console.error("WebSocket error:", error);
};

// Sending input events to the server
window.addEventListener("keydown", (e) => {
if (e.key === "ArrowDown") {
singleplayerWebSocket.send("SOFT_DROP");
} else if (e.key === "ArrowLeft") {
singleplayerWebSocket.send("MOVE_LEFT");
} else if (e.key === "ArrowRight") {
singleplayerWebSocket.send("MOVE_RIGHT");
} else if (e.key === " ") {
singleplayerWebSocket.send("HARD_DROP");
} else if (e.key === "ArrowUp") {
singleplayerWebSocket.send("ROTATE_CLOCKWISE");
}
});
Loading
Loading