diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1bb7ecc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY . . + +RUN pip install --no-cache-dir . + +CMD ["pqcli", "--basic"] \ No newline at end of file diff --git a/README.md b/README.md index f1e7073..6fbdf1b 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,43 @@ $ cd pq-cli $ pip install --user . ``` +## Docker / Docker Compose + +The repository includes a `Dockerfile` and `docker-compose.yml` that run the +game with `--basic` and keep save data in a persistent named volume. + +There is no registry pipeline required for this setup: build locally, then run +with Compose. + +```console +# Build image locally from the Dockerfile +docker compose build + +# First run (interactive): create your character in the shared save volume +docker compose run --rm pqcli-init + +# Later runs on a server (detached, default save slot 1): +docker compose up -d pqcli + +# List existing saves in the same volume: +docker compose run --rm pqcli-init pqcli --basic --list-saves + +# Change detached slot if needed (example: slot 2): +PQCLI_SAVE_SLOT=2 docker compose up -d pqcli + +# Follow logs / stop detached container: +docker compose logs -f pqcli +docker compose stop pqcli +``` + +On first run, if no save data exists in the mounted volume, `pqcli` will +automatically launch an interactive character-creation bootstrap in CLI mode +before starting the normal interface. + +For server usage, run first-time setup without a forced slot (`pqcli-init`), +then run the long-lived detached service (`pqcli`) that loads slot `1` by +default. + ## Contributing ```sh diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..b7a3d90 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,26 @@ +services: + pqcli-init: + build: . + image: ghcr.io/${GITHUB_REPOSITORY_OWNER:-local}/pq-cli:latest + stdin_open: true + tty: true + command: ["pqcli", "--basic"] + environment: + XDG_CONFIG_HOME: /data + volumes: + - pqcli-data:/data + + pqcli: + build: . + stdin_open: true + tty: true + stop_signal: SIGINT + stop_grace_period: 30s + command: ["sh", "-c", "pqcli --basic --load-save ${PQCLI_SAVE_SLOT:-1}"] + environment: + XDG_CONFIG_HOME: /data + volumes: + - pqcli-data:/data + +volumes: + pqcli-data: \ No newline at end of file diff --git a/pqcli/__main__.py b/pqcli/__main__.py index 7d870a1..d2b2302 100644 --- a/pqcli/__main__.py +++ b/pqcli/__main__.py @@ -66,6 +66,21 @@ def list_players(roster: Roster, file: T.Optional[T.Any] = None) -> None: print(f"{i}. {player.name}", file=file) +def bootstrap_first_run(roster: Roster, args: argparse.Namespace) -> None: + if roster.players or args.list_saves or args.load_save: + return + + print("No saved characters found. Starting first-run character creation.") + ui = BasicUserInterface(roster, None, args) + while not roster.players: + player = ui.create_player(auto_play=False) + if player: + return + if not ui.confirm("No character created. Do you want to try again?"): + print("A character is required for first run.", file=sys.stderr) + raise SystemExit(1) + + def main() -> None: args = parse_args() roster = Roster.load(SAVE_PATH) @@ -83,6 +98,8 @@ def main() -> None: list_players(roster, file=sys.stderr) exit(1) + bootstrap_first_run(roster, args) + try: ui = args.ui(roster, player, args) ui.run() diff --git a/pqcli/ui/basic/__init__.py b/pqcli/ui/basic/__init__.py index b50ec6b..c5a06ce 100644 --- a/pqcli/ui/basic/__init__.py +++ b/pqcli/ui/basic/__init__.py @@ -101,7 +101,7 @@ def main_menu(self) -> None: self.quit() break - def create_player(self) -> T.Optional[Player]: + def create_player(self, *, auto_play: bool = True) -> T.Optional[Player]: name = input("Name your new character: ") if not name: print("Cancelled.") @@ -123,7 +123,9 @@ def create_player(self) -> T.Optional[Player]: name=name, race=race, class_=class_, stats=stats ) self.roster.players.append(player) - if self.confirm("Do you want to play as your new character?"): + if auto_play and self.confirm( + "Do you want to play as your new character?" + ): self.play(player) return player diff --git a/pyproject.toml b/pyproject.toml index cd14fea..db2318a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "xdg-base-dirs>=6.0.0,<7.0.0", "urwid>=2.1.2,<3.0.0", "urwid-readline>=0.13,<1.0.0", + "pre-commit>=4.5.0", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index ca64393..67bc8b3 100644 --- a/uv.lock +++ b/uv.lock @@ -61,6 +61,7 @@ name = "pqcli" version = "1.0.4" source = { editable = "." } dependencies = [ + { name = "pre-commit" }, { name = "urwid" }, { name = "urwid-readline" }, { name = "xdg-base-dirs" }, @@ -78,6 +79,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "pre-commit", specifier = ">=4.5.0" }, { name = "urwid", specifier = ">=2.1.2,<3.0.0" }, { name = "urwid-readline", specifier = ">=0.13,<1.0.0" }, { name = "windows-curses", marker = "extra == 'windows'", specifier = ">=2.3.0,<3.0.0" },