Skip to content

Commit

Permalink
Merge pull request #226 from felixvonsamson/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
yassir-akram authored Dec 19, 2024
2 parents 18076fc + 1e7114d commit 0e11b65
Show file tree
Hide file tree
Showing 242 changed files with 10,428 additions and 7,986 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
instance/*
tests/*/instance/*
checkpoints/*
__pycache__
.DS_Store
4 changes: 3 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
"--port", "5001",
"--simulate_file", "actions_history.log",
"--simulate_checkpoint_every_k_ticks", "1000",
"--force_yes"
"--simulate_stop_on_server_error",
"--simulate_stop_on_assertion_error"

]
}
]
Expand Down
25 changes: 24 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"json.format.keepLines": true,
// cSpell
"cSpell.words": [
"APPROXIMATIVE",
"apscheduler",
"arccos",
"backref",
Expand All @@ -51,23 +52,33 @@
"coldwave",
"coldwaves",
"Compr",
"Constrtruct",
"Controllables",
"cumsum",
"cumul",
"datapoint",
"Deepwater",
"DGRAM",
"docstrings",
"dont",
"Effic",
"elif",
"emsp",
"endfor",
"endmacro",
"Energetica",
"engineio",
"ensp",
"eventlet",
"Fukushima",
"gameplay",
"gevent",
"httpauth",
"Hydrostrg",
"iloc",
"infobox",
"infotable",
"infotext",
"IPCC",
"isnot",
"isort",
Expand All @@ -78,7 +89,13 @@
"lvls",
"mapsize",
"Météo",
"mglst",
"minimizable",
"minmax",
"mult",
"nbsp",
"onclick",
"oninput",
"perlin",
"Photovoltaics",
"pnoise",
Expand All @@ -92,12 +109,17 @@
"sessionmaker",
"solars",
"sqlalchemy",
"stylesheet",
"subarrays",
"subkey",
"thinsp",
"timesteps",
"tojson",
"Unsubscription",
"Uran",
"uselist",
"vectorize",
"visualisation",
"webcredentials",
"webpush",
"werkzeug",
Expand All @@ -108,5 +130,6 @@
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
"python.testing.pytestEnabled": true,
"js/ts.implicitProjectConfig.checkJs": true
}
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ Energetica uses Flask and Python for its backend, SQL for its database, and Jinj

## Running a Local Instance

Make sure you have Python 3.10 or above installed.
<!-- Using StrEnum requires >=3.11 -->
Make sure you have Python version 3.11 or 3.12 installed.

Clone the repository:

Expand Down
160 changes: 67 additions & 93 deletions website/__init__.py → energetica/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""This code is run once at the start of the game"""
"""Initializes the app and the game engine."""

# pylint: disable=wrong-import-order,wrong-import-position
# ruff: noqa: E402
Expand All @@ -18,7 +18,6 @@
from datetime import datetime
from pathlib import Path

# import cProfile
from gevent import monkey

monkey.patch_all(thread=True, time=True)
Expand All @@ -29,19 +28,25 @@
from flask_login import LoginManager, current_user
from flask_sock import Sock
from flask_socketio import SocketIO
from flask_sqlalchemy import SQLAlchemy

from website.simulate import simulate

db = SQLAlchemy()

import website.game_engine

from .database.player import Player
from energetica.api.http import http
from energetica.api.socketio_handlers import add_handlers
from energetica.api.websocket import add_sock_handlers, websocket_blueprint
from energetica.auth import auth
from energetica.database import db
from energetica.database.map import Hex
from energetica.database.messages import Chat
from energetica.database.player import Player
from energetica.game_engine import GameEngine
from energetica.init_test_players import init_test_players
from energetica.simulate import simulate
from energetica.utils.climate_helpers import data_init_climate
from energetica.utils.tick_execution import state_update
from energetica.views import changelog, landing, location_choice_views, overviews, views, wiki


def get_or_create_flask_secret_key() -> str:
"""SECRET_KEY for Flask. Loads it from disk if it exists, creates one and stores it otherwise"""
"""Load or create SECRET_KEY for Flask."""
filepath = "instance/flask_secret_key.txt"
if os.path.exists(filepath):
with open(filepath, "r", encoding="utf-8") as f:
Expand All @@ -54,53 +59,46 @@ def get_or_create_flask_secret_key() -> str:


def get_or_create_vapid_keys() -> tuple[str, str]:
"""
Public private key pair for vapid push notifications. Loads these from disk if they exists, creates a new pair and
stores if otherwise
"""
"""Load or create VAPID key pair for push notifications."""
public_key_filepath = "instance/vapid_public_key.txt"
private_key_filepath = "instance/vapid_private_key.txt"
if os.path.exists(public_key_filepath) and os.path.exists(private_key_filepath):
with open(public_key_filepath, "r", encoding="utf-8") as f:
public_key = f.read().strip()
with open(private_key_filepath, "r", encoding="utf-8") as f:
private_key = f.read().strip()
if Path(public_key_filepath).exists() and Path(private_key_filepath).exists():
public_key = Path(public_key_filepath).read_text(encoding="utf-8").strip()
private_key = Path(private_key_filepath).read_text(encoding="utf-8").strip()
return public_key, private_key
else:
# Generate a new ECDSA key pair
private_key_obj = SigningKey.generate(curve=NIST256p)
public_key_obj = private_key_obj.get_verifying_key()
# Generate a new ECDSA key pair
private_key_obj = SigningKey.generate(curve=NIST256p)
public_key_obj = private_key_obj.get_verifying_key()

# Encode the keys using URL- and filename-safe base64 without padding
private_key = base64.urlsafe_b64encode(private_key_obj.to_string()).rstrip(b"=").decode("utf-8")
public_key = base64.urlsafe_b64encode(b"\x04" + public_key_obj.to_string()).rstrip(b"=").decode("utf-8")
# Encode the keys using URL- and filename-safe base64 without padding
private_key = base64.urlsafe_b64encode(private_key_obj.to_string()).rstrip(b"=").decode("utf-8")
public_key = base64.urlsafe_b64encode(b"\x04" + public_key_obj.to_string()).rstrip(b"=").decode("utf-8")

# Write the keys to their respective files
with open(public_key_filepath, "w", encoding="utf-8") as f:
f.write(public_key)
with open(private_key_filepath, "w", encoding="utf-8") as f:
f.write(private_key)
# Write the keys to their respective files
Path(public_key_filepath).write_text(public_key, encoding="utf-8")
Path(private_key_filepath).write_text(private_key, encoding="utf-8")

return public_key, private_key
return public_key, private_key


def create_app(
clock_time,
in_game_seconds_per_tick,
run_init_test_players,
rm_instance,
random_seed,
simulate_file,
simulate_stop_on_mismatch,
simulate_stop_on_server_error,
simulate_stop_on_assertion_error,
simulate_checkpoint_every_k_ticks,
simulate_checkpoint_ticks,
simulate_till,
simulate_profiling,
clock_time=30,
in_game_seconds_per_tick=240,
run_init_test_players=False,
rm_instance=False,
random_seed=42,
simulate_file=None,
simulate_stop_on_mismatch=False,
simulate_stop_on_server_error=False,
simulate_stop_on_assertion_error=False,
simulate_checkpoint_every_k_ticks=10000,
simulate_checkpoint_ticks=[],
simulate_till=None,
simulate_profiling=False,
skip_adding_handlers=False,
**kwargs,
):
"""This function sets up the app and the game engine"""
"""Set up the app and the game engine."""
# gets lock to avoid multiple instances
if platform.system() == "Linux":
lock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
Expand Down Expand Up @@ -136,6 +134,8 @@ def create_app(
with simulate_file as file:
actions = [json.loads(line) for line in file]
assert actions[0]["action_type"] == "init_engine"
clock_time = actions[0]["clock_time"]
in_game_seconds_per_tick = actions[0]["in_game_seconds_per_tick"]
random_seed = actions[0]["random_seed"]
start_date = datetime.fromisoformat(actions[0]["start_date"])

Expand All @@ -156,9 +156,7 @@ def create_app(
last_action_id = action_id_by_tick[simulate_till] if simulate_till else len(actions) - 1

# creates the engine (and loading the save if it exists)
engine = website.game_engine.GameEngine(clock_time, in_game_seconds_per_tick, random_seed, start_date)

from .utils.game_engine import data_init_climate
engine = GameEngine(clock_time, in_game_seconds_per_tick, random_seed, start_date)

Path("instance/player_data").mkdir(parents=True, exist_ok=True)
if not os.path.isfile("instance/server_data/climate_data.pck"):
Expand All @@ -177,7 +175,7 @@ def create_app(
engine.log("Loaded last checkpoint from disk.")
else:
if loaded_tick:
with tarfile.open("checkpoints/simulation/checkpoint_{loaded_tick}.tar.gz") as file:
with tarfile.open(f"checkpoints/simulation/checkpoint_{loaded_tick}.tar.gz") as file:
file.extractall("./")
engine.log(f"Loaded checkpoints/simulation/checkpoint_{loaded_tick}.tar.gz from disk.")

Expand All @@ -191,24 +189,20 @@ def create_app(
# initialize socketio :
socketio = SocketIO(app, cors_allowed_origins="*") # engineio_logger=True
engine.socketio = socketio
from .api.socketio_handlers import add_handlers

add_handlers(socketio=socketio, engine=engine)
if not skip_adding_handlers:
add_handlers(socketio=socketio, engine=engine)

# initialize sock for WebSockets:
sock = Sock(app)
engine.sock = sock
from .api.websocket import add_sock_handlers

add_sock_handlers(sock=sock, engine=engine)
if not skip_adding_handlers:
add_sock_handlers(sock=sock, engine=engine)

# add blueprints (website repositories) :
from .api.http import http
from .api.websocket import websocket_blueprint
from .auth import auth
from .views import changelog, location_choice_views, overviews, views, wiki

app.register_blueprint(location_choice_views, url_prefix="/")
app.register_blueprint(landing, url_prefix="/")
app.register_blueprint(views, url_prefix="/")
app.register_blueprint(overviews, url_prefix="/production_overview")
app.register_blueprint(wiki, url_prefix="/wiki")
Expand All @@ -219,45 +213,37 @@ def create_app(

@app.route("/subscribe", methods=["GET", "POST"])
def subscribe():
"""
POST creates a new subscription
GET returns vapid public key
"""
"""POST: Create a new subscription. GET: Return VAPID public key."""
if request.method == "GET":
return jsonify({"public_key": app.config["VAPID_PUBLIC_KEY"]})
subscription = request.json
if "endpoint" not in subscription:
return jsonify({"response": "Invalid subscription"})
engine.notification_subscriptions[current_user.id].append(subscription)
engine.data["notification_subscriptions"][current_user.id].append(subscription)
return jsonify({"response": "Subscription successful"})

@app.route("/unsubscribe", methods=["POST"])
def unsubscribe():
"""
POST removes a subscription
"""
"""POST: remove a subscription."""
subscription = request.json
if subscription in engine.notification_subscriptions[current_user.id]:
engine.notification_subscriptions[current_user.id].remove(subscription)
if subscription in engine.data["notification_subscriptions"][current_user.id]:
engine.data["notification_subscriptions"][current_user.id].remove(subscription)
return jsonify({"response": "Unsubscription successful"})

@app.route("/apple-app-site-association")
def apple_app_site_association():
"""
Returns the apple-app-site-association JSON data needed for supporting
associated domains needed for shared webcredentials
"""Return the apple-app-site-association JSON data.
Needed for supporting associated domains needed for shared webcredentials
"""
return send_file("static/apple-app-site-association", as_attachment=True)

from .database.map import Hex
from .database.messages import Chat

# initialize database :
with app.app_context():
db.create_all()
# if map data not already stored in database, read map.csv and store it in database
if Hex.query.count() == 0:
with open("website/static/data/map.csv", "r") as file:
with open("energetica/static/data/map.csv", "r") as file:
csv_reader = csv.DictReader(file)
for row in csv_reader:
hex = Hex(
Expand Down Expand Up @@ -289,15 +275,12 @@ def apple_app_site_association():
login_manager.init_app(app)

@login_manager.user_loader
def load_user(id):
player = Player.query.get(int(id))
return player
def load_user(id) -> Player:
return db.session.get(Player, int(id))

# initialize the schedulers and add the recurrent functions :
# This function is to run the following only once, TO REMOVE IF DEBUG MODE IS SET TO FALSE
if os.environ.get("WERKZEUG_RUN_MAIN") == "true":
from .utils.game_engine import state_update

scheduler = APScheduler()
scheduler.init_app(app)

Expand Down Expand Up @@ -336,15 +319,6 @@ def load_user(id):
engine.log("running init_test_players")
with app.app_context():
# Temporary automated player creation for testing
from .init_test_players import init_test_players

init_test_players(engine)
# Manually trigger the scheduler to run the state_update function as soon as possible
scheduler.add_job(
func=state_update,
args=(engine, app),
id="state_update_immediate",
trigger="date",
)

return socketio, sock, app

return socketio, app
Loading

0 comments on commit 0e11b65

Please sign in to comment.