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

Extend game to multiplayer with AWS deployment #3

Merged
merged 2 commits into from
Dec 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ updates:
- "github-actions"
commit-message:
prefix: "github-actions"
include: "scope"
include: "scope"
59 changes: 44 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,32 +53,61 @@ A sophisticated text-based RPG that leverages AI to generate unique content, fea
- Inventory system with consumables and equipment
- Gold-based economy with configurable sell prices

### Multiplayer Support
- Turn-based combat with player queue
- Multiple players and enemies in combat
- Real-time communication using WebSockets
- Player authentication and session management
- Game state saving and loading

## Installation

### Prerequisites
- Python 3.12+
- OpenAI API key (for AI-generated content)
- PostgreSQL (for game state and session management)

### Setup
1. Clone the repository:
```bash
git clone https://github.com/yourusername/terminal-quest.git
cd terminal-quest
```
```bash
git clone https://github.com/yourusername/terminal-quest.git
cd terminal-quest
```

2. Install dependencies:
```bash
pip3 install -r requirements.txt
```
```bash
pip3 install -r requirements.txt
```

3. Create a `.env` file:
```env
OPENAI_API_KEY=your_key_here
```
```env
OPENAI_API_KEY=your_key_here
DATABASE_URL=your_postgresql_database_url
```

4. Run the game:
```bash
python3 main.py
```
4. Set up the PostgreSQL database:
- Ensure PostgreSQL is running.
- Create the necessary database and tables using the provided SQL scripts or migrations.

5. Run the server:
```bash
python src/services/server.py
```

6. Run the client:
```bash
python client/client.py
```

## Running the Game on a Server

To run the game on a server and allow players to connect from their machines, follow these steps:

1. Set up an AWS EC2 instance or any other cloud server.
2. Install the necessary dependencies on the server.
3. Configure the server to run the game as a service.
4. Use a WebSocket server to handle real-time communication between the server and clients.
5. Players can connect to the server using a client application that communicates with the server via WebSockets.
Comment on lines +102 to +110
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Expand server deployment instructions

The deployment section needs more detailed instructions:

  1. AWS EC2 requirements:
    • Recommended instance type
    • Security group configuration
    • Region considerations
  2. WebSocket setup:
    • WebSocket server implementation details
    • Connection handling
    • Security considerations
  3. Environment variables needed for deployment
  4. Monitoring and logging setup

Consider adding this structure:

 ## Running the Game on a Server

 To run the game on a server and allow players to connect from their machines, follow these steps:

-1. Set up an AWS EC2 instance or any other cloud server.
-2. Install the necessary dependencies on the server.
-3. Configure the server to run the game as a service.
-4. Use a WebSocket server to handle real-time communication between the server and clients.
-5. Players can connect to the server using a client application that communicates with the server via WebSockets.
+1. AWS EC2 Setup
+   - Launch a t2.micro (or larger) instance with Ubuntu Server 22.04
+   - Configure security groups:
+     - TCP port 22 (SSH)
+     - TCP port 80/443 (HTTP/HTTPS)
+     - TCP port 8765 (WebSocket)
+
+2. Server Configuration
+   ```bash
+   # Install dependencies
+   sudo apt-get update
+   sudo apt-get install python3.12 postgresql nginx
+   
+   # Clone and setup the game
+   git clone https://github.com/yourusername/terminal-quest.git
+   cd terminal-quest
+   pip3 install -r requirements.txt
+   ```
+
+3. Environment Setup
+   ```bash
+   # Create .env file
+   cat > .env << EOL
+   DATABASE_URL=postgresql://user:pass@localhost:5432/terminal_quest
+   WEBSOCKET_HOST=0.0.0.0
+   WEBSOCKET_PORT=8765
+   EOL
+   ```
+
+4. WebSocket Server
+   - The game uses asyncio and websockets for real-time communication
+   - Default WebSocket port: 8765
+   - Handles events:
+     - Player connection/disconnection
+     - Combat actions
+     - Game state updates
+
+5. Running as a Service
+   ```bash
+   # Create systemd service
+   sudo nano /etc/systemd/system/terminal-quest.service
+   
+   # Add service configuration
+   [Unit]
+   Description=Terminal Quest Game Server
+   After=network.target
+   
+   [Service]
+   User=ubuntu
+   WorkingDirectory=/home/ubuntu/terminal-quest
+   ExecStart=/usr/bin/python3 main.py
+   Restart=always
+   
+   [Install]
+   WantedBy=multi-user.target
+   ```
+
+6. Client Connection
+   ```python
+   # Example client connection
+   import websockets
+   import asyncio
+   
+   async def connect_to_game():
+       uri = "ws://your-server-ip:8765"
+       async with websockets.connect(uri) as websocket:
+           # Handle game communication
+           pass
+   ```


## Project Structure

Expand Down Expand Up @@ -162,4 +191,4 @@ terminal_quest/
- Multiplayer support

## License
This project is licensed under the MIT License - see the LICENSE file for details.
This project is licensed under the MIT License - see the LICENSE file for details.
34 changes: 34 additions & 0 deletions alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# alembic.ini
[alembic]
# path to migration scripts
script_location = migrations

# database connection URL
sqlalchemy.url = postgresql://rpg_user:secure_password@localhost:5432/rpg_game

[loggers]
keys = root, sqlalchemy

[handlers]
keys = console

[formatters]
keys = generic

[logger_root]
level = WARN
handlers = console

[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine

[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic

[formatter_generic]
format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s
44 changes: 44 additions & 0 deletions client/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import asyncio
import websockets
import json


async def connect_to_server():
uri = "ws://localhost:8765"
async with websockets.connect(uri) as websocket:
Comment on lines +7 to +8
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Use secure WebSocket (wss://) and environment variables for configuration

The WebSocket URI and connection setup have security concerns:

  1. Using insecure WebSocket protocol (ws://)
  2. Hardcoded URI
-    uri = "ws://localhost:8765"
+    uri = os.getenv("GAME_SERVER_URI", "wss://localhost:8765")
     async with websockets.connect(uri) as websocket:

Committable suggestion skipped: line range outside the PR's diff.

# Authenticate player
await websocket.send(
json.dumps(
{"action": "authenticate", "username": "player", "password": "password"}
)
)
Comment on lines +10 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove hardcoded credentials and add proper error handling

Authentication credentials should not be hardcoded. Also, add error handling for authentication failures.

-        await websocket.send(
-            json.dumps(
-                {"action": "authenticate", "username": "player", "password": "password"}
-            )
-        )
+        try:
+            username = os.getenv("GAME_USERNAME")
+            password = os.getenv("GAME_PASSWORD")
+            if not username or not password:
+                raise ValueError("Missing authentication credentials")
+            
+            await websocket.send(
+                json.dumps(
+                    {"action": "authenticate", "username": username, "password": password}
+                )
+            )
+        except Exception as e:
+            logger.error(f"Authentication failed: {e}")
+            raise
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await websocket.send(
json.dumps(
{"action": "authenticate", "username": "player", "password": "password"}
)
)
try:
username = os.getenv("GAME_USERNAME")
password = os.getenv("GAME_PASSWORD")
if not username or not password:
raise ValueError("Missing authentication credentials")
await websocket.send(
json.dumps(
{"action": "authenticate", "username": username, "password": password}
)
)
except Exception as e:
logger.error(f"Authentication failed: {e}")
raise

response = await websocket.recv()
print(response)

# If class selection is required
response_data = json.loads(response)
if "classes" in response_data:
print("Available classes:")
for i, class_name in enumerate(response_data["classes"], 1):
print(f"{i}. {class_name}")

class_choice = int(input("Select your class: ")) - 1
selected_class = response_data["classes"][class_choice]
Comment on lines +25 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add input validation for class selection

The class selection lacks input validation and error handling.

-            class_choice = int(input("Select your class: ")) - 1
-            selected_class = response_data["classes"][class_choice]
+            while True:
+                try:
+                    class_choice = int(input("Select your class (1-{}): ".format(
+                        len(response_data["classes"])))) - 1
+                    if 0 <= class_choice < len(response_data["classes"]):
+                        selected_class = response_data["classes"][class_choice]
+                        break
+                    print("Invalid selection. Please try again.")
+                except ValueError:
+                    print("Please enter a valid number.")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class_choice = int(input("Select your class: ")) - 1
selected_class = response_data["classes"][class_choice]
while True:
try:
class_choice = int(input("Select your class (1-{}): ".format(
len(response_data["classes"])))) - 1
if 0 <= class_choice < len(response_data["classes"]):
selected_class = response_data["classes"][class_choice]
break
print("Invalid selection. Please try again.")
except ValueError:
print("Please enter a valid number.")


# Send class selection to server
await websocket.send(
json.dumps({"action": "select_class", "class_name": selected_class})
)
response = await websocket.recv()
print(response)

# Load game state
await websocket.send(
json.dumps({"action": "load_game_state", "player": {"name": "player"}})
)
response = await websocket.recv()
print(response)


if __name__ == "__main__":
asyncio.run(connect_to_server())
17 changes: 17 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: '3.8'

services:
db:
image: postgres:latest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Specify exact PostgreSQL version instead of 'latest' tag

Using the latest tag can lead to unexpected breaking changes during deployment. Pin to a specific version for better stability and predictability.

-    image: postgres:latest
+    image: postgres:15.4
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
image: postgres:latest
image: postgres:15.4

container_name: rpg_game_db
environment:
POSTGRES_DB: rpg_game
POSTGRES_USER: rpg_user
POSTGRES_PASSWORD: secure_password
Comment on lines +8 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Move sensitive credentials to environment variables

Hardcoding database credentials in the docker-compose file is a security risk. Use environment variables instead.

     environment:
-      POSTGRES_DB: rpg_game
-      POSTGRES_USER: rpg_user
-      POSTGRES_PASSWORD: secure_password
+      POSTGRES_DB: ${POSTGRES_DB:-rpg_game}
+      POSTGRES_USER: ${POSTGRES_USER:-rpg_user}
+      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
POSTGRES_DB: rpg_game
POSTGRES_USER: rpg_user
POSTGRES_PASSWORD: secure_password
POSTGRES_DB: ${POSTGRES_DB:-rpg_game}
POSTGRES_USER: ${POSTGRES_USER:-rpg_user}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/data
Comment on lines +4 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add health check and resource limits

Consider adding health checks and resource limits to ensure reliable operation in production.

   db:
     image: postgres:latest
     container_name: rpg_game_db
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U rpg_user -d rpg_game"]
+      interval: 10s
+      timeout: 5s
+      retries: 5
+    deploy:
+      resources:
+        limits:
+          memory: 1G
+        reservations:
+          memory: 512M
     environment:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
db:
image: postgres:latest
container_name: rpg_game_db
environment:
POSTGRES_DB: rpg_game
POSTGRES_USER: rpg_user
POSTGRES_PASSWORD: secure_password
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/data
db:
image: postgres:latest
container_name: rpg_game_db
healthcheck:
test: ["CMD-SHELL", "pg_isready -U rpg_user -d rpg_game"]
interval: 10s
timeout: 5s
retries: 5
deploy:
resources:
limits:
memory: 1G
reservations:
memory: 512M
environment:
POSTGRES_DB: rpg_game
POSTGRES_USER: rpg_user
POSTGRES_PASSWORD: secure_password
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/data


volumes:
db_data:
60 changes: 43 additions & 17 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from src.display.boss.boss_view import BossView
import time
from src.models.base_types import EffectResult
from src.services.session_management import SessionManagementService
from src.services.game_state import GameStateService

# Configure logging
logging.basicConfig(
Expand Down Expand Up @@ -62,26 +64,47 @@ def main():
base_view = BaseView()
boss_view = BossView()
boss_service = BossService()
session_service = SessionManagementService()
game_state_service = GameStateService()

# Player authentication
session_service.show_login_screen()
player = session_service.authenticate_player()
if not player:
MessageView.show_error("Authentication failed. Exiting game.")
return

# Character creation
character_view.show_character_creation()
player_name = input().strip()

# Use character creation service
try:
player = CharacterCreationService.create_character(player_name)
if not player:
MessageView.show_error("Character creation failed: Invalid name provided")
# Load game state
game_state = game_state_service.load_game_state(player)
if game_state:
player = game_state["player"]
shop = game_state["shop"]
else:
# Character creation
character_view.show_character_creation()
player_name = input().strip()

# Use character creation service
try:
player = CharacterCreationService.create_character(player_name)
if not player:
MessageView.show_error(
"Character creation failed: Invalid name provided"
)
return
except ValueError as e:
MessageView.show_error(f"Character creation failed: {str(e)}")
return
except ValueError as e:
MessageView.show_error(f"Character creation failed: {str(e)}")
return
except Exception as e:
MessageView.show_error("An unexpected error occurred during character creation")
return
except Exception as e:
MessageView.show_error(
"An unexpected error occurred during character creation"
)
return

# Initialize shop
shop = Shop()

# Initialize shop
shop = Shop()
base_view.clear_screen()

base_view.clear_screen()

Expand Down Expand Up @@ -222,6 +245,9 @@ def main():
MessageView.show_info("\nThank you for playing! See you next time...")
break

# Save game state after each turn
game_state_service.save_game_state(player, shop)

if player.health <= 0:
game_view.show_game_over(player)

Expand Down
1 change: 1 addition & 0 deletions migrations/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Generic single-database configuration.
76 changes: 76 additions & 0 deletions migrations/env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
from logging.config import fileConfig

from sqlalchemy import engine_from_config
from sqlalchemy import pool

from alembic import context

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# Interpret the config file for Python logging.
# This line sets up loggers basically.
if config.config_file_name is not None:
fileConfig(config.config_file_name)

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = None

# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.


def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.

This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.

Calls to context.execute() here emit the given string to the
script output.

"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)

with context.begin_transaction():
context.run_migrations()


def run_migrations_online() -> None:
"""Run migrations in 'online' mode.

In this scenario we need to create an Engine
and associate a connection with the context.

"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)

with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)

with context.begin_transaction():
context.run_migrations()


if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
26 changes: 26 additions & 0 deletions migrations/script.py.mako
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""${message}

Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
${imports if imports else ""}

# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}


def upgrade() -> None:
${upgrades if upgrades else "pass"}


def downgrade() -> None:
${downgrades if downgrades else "pass"}
Loading
Loading