Skip to content

Commit

Permalink
Merge pull request #3 from mericozkayagan/extend-multiplayer
Browse files Browse the repository at this point in the history
Extend game to multiplayer with AWS deployment
  • Loading branch information
mericozkayagan authored Dec 14, 2024
2 parents d5d7922 + 1d87abc commit 9ce2406
Show file tree
Hide file tree
Showing 24 changed files with 1,094 additions and 179 deletions.
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.

## 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:
# Authenticate player
await websocket.send(
json.dumps(
{"action": "authenticate", "username": "player", "password": "password"}
)
)
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]

# 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
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

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

0 comments on commit 9ce2406

Please sign in to comment.