"""
This file contains gale-admin's click commands: create-project
(scaffolds a new project's main.py/settings.py/src/assets layout) and
create-state (scaffolds a new BaseState subclass inside one).
Author: Alejandro Mujica (aledrums@gmail.com)
"""
import os
import re
from pathlib import Path
import click
[docs]
def touch(path: Path, text: str = None):
with open(path, "w") as f:
if text is not None:
f.write(text)
[docs]
def to_pascal_case(identifier: str) -> str:
words = identifier.split("_")
return "".join([w.capitalize() for w in words])
[docs]
def to_spaced_name(identifier: str) -> str:
words = identifier.split("_")
return " ".join([w.capitalize() for w in words])
[docs]
def to_snake_case(identifier: str) -> str:
identifier = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", identifier)
return re.sub("([a-z0-9])([A-Z])", r"\1_\2", identifier).lower()
@click.group
def cli():
pass
GAME_CLASS_TEMPLATE = '''"""
This module was autogenerated by gale.
"""
import pygame
from gale.game import Game
from gale.input_handler import InputData, InputHandler, InputListener
from gale.state import StateMachine
class {game_class}(Game, InputListener):
def init(self) -> None:
self.state_machine = StateMachine()
InputHandler.register_listener(self)
def update(self, dt: float) -> None:
self.state_machine.update(dt)
def render(self, surface: pygame.Surface) -> None:
self.state_machine.render(surface)
def on_input(self, input_id: str, input_data: InputData) -> None:
if (input_id == 'quit' and input_data.pressed):
self.quit()
'''
GAME_MAIN_TEMPLATE = '''"""
This module was autogenerated by gale.
"""
import settings
from src.{game_file} import {game_class}
if __name__ == '__main__':
game = {game_class}(
"{game_title}",
settings.WINDOW_WIDTH, settings.WINDOW_HEIGHT,
settings.VIRTUAL_WIDTH, settings.VIRTUAL_HEIGHT
)
game.exec()
'''
SETTINGS_TEMPLATE = '''"""
This module was autogenerated by gale.
"""
import pathlib
import pygame
from gale import frames
from gale import input_handler
input_handler.InputHandler.set_keyboard_action(input_handler.KEY_ESCAPE, 'quit')
# Size we want to emulate
VIRTUAL_WIDTH = 320
VIRTUAL_HEIGHT = 180
# Size of our actual window
WINDOW_WIDTH = 1280
WINDOW_HEIGHT = 720
BASE_DIR = pathlib.Path(__file__).parent
# Register your textures from the graphics folder, for instance:
# TEXTURES = {
# 'my_texture': pygame.image.load(BASE_DIR / "assets" / "graphics" / "my_texture.png")
# }
TEXTURES = {}
# Register your frames, for instance:
# FRAMES = {
# 'my_frames': frames.generate_frames(TEXTURES['my_texture'], 16, 16)
# }
FRAMES = {}
pygame.mixer.init()
# Register your sound from the sounds folder, for instance:
# SOUNDS = {
# 'my_sound': pygame.mixer.Sound(BASE_DIR / "assets" / "sounds" / "my_sound.wav"),
# }
SOUNDS = {}
pygame.font.init()
# Register your fonts from the fonts folder, for instance:
# FONTS = {
# 'small': pygame.font.Font(BASE_DIR / "assets" / "fonts" / "font.ttf", 8)
# }
FONTS = {}
'''
@click.command
@click.argument("name")
def create_project(name: str) -> None:
if os.path.exists(name):
click.echo(f"Project {name} already exists.")
return
os.mkdir(name)
app_path = os.path.join(os.getcwd(), name)
game_class = to_pascal_case(name)
game_file = to_snake_case(game_class)
game_title = to_spaced_name(name)
touch(
os.path.join(app_path, "main.py"),
GAME_MAIN_TEMPLATE.format(
game_class=game_class, game_title=game_title, game_file=game_file
),
)
touch(os.path.join(app_path, "settings.py"), SETTINGS_TEMPLATE)
touch(os.path.join(app_path, "README.md"), f"# {game_title}")
os.mkdir(os.path.join(app_path, "src"))
touch(
os.path.join(app_path, "src", f"{game_file}.py"),
GAME_CLASS_TEMPLATE.format(game_class=game_class),
)
os.mkdir(os.path.join(app_path, "assets"))
for directory in ["sounds", "graphics", "fonts"]:
os.mkdir(os.path.join(app_path, "assets", directory))
click.echo(f"Project {name} created")
cli.add_command(create_project)
STATE_TEMPLATE = '''"""
This module was autogenerated by gale.
"""
from typing import Any, Dict, Tuple
import pygame
from gale.input_handler import InputData
from gale.state import BaseState
class {state_class}(BaseState):
def enter(self, *args: Tuple[Any], **kwargs: Dict[str, Any]) -> None:
pass
def exit(self) -> None:
pass
def on_input(self, input_id: str, input_data: InputData) -> None:
pass
def update(self, dt: float) -> None:
pass
def render(self, surface: pygame.Surface) -> None:
pass
'''
@click.command()
@click.argument("name")
def create_state(name: str) -> None:
if not os.path.isdir("src"):
click.echo(
"No 'src' directory found. Run this from inside a project created "
"with 'gale-admin create-project'."
)
return
normalized = to_snake_case(name)
if not normalized.endswith("_state"):
normalized += "_state"
state_class = to_pascal_case(normalized)
states_dir = os.path.join("src", "states")
os.makedirs(states_dir, exist_ok=True)
init_path = os.path.join(states_dir, "__init__.py")
if not os.path.exists(init_path):
touch(init_path)
state_path = os.path.join(states_dir, f"{to_snake_case(state_class)}.py")
if os.path.exists(state_path):
click.echo(f"State {state_class} already exists.")
return
touch(state_path, STATE_TEMPLATE.format(state_class=state_class))
click.echo(f"State {state_class} created at {state_path}")
cli.add_command(create_state)