This commit is contained in:
Waylon Walker 2023-05-15 11:37:32 -05:00
commit f3e79af444
No known key found for this signature in database
GPG key ID: 66E2BF2B4190EFE4
22 changed files with 1798 additions and 0 deletions

View file

@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2023-present Waylon S. Walker <waylon@waylonwalker.com>
#
# SPDX-License-Identifier: MIT
__version__ = "0.0.0.dev1"

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2023-present Waylon S. Walker <waylon@waylonwalker.com>
#
# SPDX-License-Identifier: MIT

View file

@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2023-present Waylon S. Walker <waylon@waylonwalker.com>
#
# SPDX-License-Identifier: MIT
import sys
if __name__ == '__main__':
from .cli import marvin_sw_text_adventure.game import game
sys.exit(game())

View file

View file

@ -0,0 +1,55 @@
import typer
from marvin_sw_text_adventure.cli.common import verbose_callback
from marvin_sw_text_adventure.console import console
from marvin_sw_text_adventure.cli.game import game_app
# from marvin_sw_text_adventure.cli.config import config_app
# from marvin_sw_text_adventure.cli.tui import tui_app
app = typer.Typer(
name="marvin_sw_text_adventure",
help="a text adventure game leveraging @askmarvinai",
)
app.add_typer(game_app)
# app.add_typer(config_app)
# app.add_typer(tui_app)
def version_callback(value: bool) -> None:
"""Callback function to print the version of the marvin-sw-text-adventure package.
Args:
value (bool): Boolean value to determine if the version should be printed.
Raises:
typer.Exit: If the value is True, the version will be printed and the program will exit.
Example:
version_callback(True)
"""
if value:
from marvin_sw_text_adventure.__about__ import __version__
typer.echo(f"{__version__}")
raise typer.Exit()
@app.callback()
def main(
version: bool = typer.Option(
None,
"--version",
callback=version_callback,
is_eager=True,
),
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
) -> None:
return
if __name__ == "__main__":
typer.run(main)

View file

@ -0,0 +1,6 @@
from marvin_sw_text_adventure.console import console
def verbose_callback(value: bool) -> None:
if value:
console.quiet = False

View file

@ -0,0 +1,29 @@
from rich.console import Console
import typer
from marvin_sw_text_adventure.cli.common import verbose_callback
from marvin_sw_text_adventure.config import config as configuration
config_app = typer.Typer()
@config_app.callback()
def config(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
):
"configuration cli"
@config_app.command()
def show(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
):
Console().print(configuration)

View file

@ -0,0 +1,31 @@
from rich.console import Console
import typer
from marvin_sw_text_adventure.cli.common import verbose_callback
from marvin_sw_text_adventure.game import game as game_run
game_app = typer.Typer()
@game_app.callback()
def game(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
):
"game cli"
@game_app.command()
def run(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
):
from marvin_sw_text_adventure.console import console
console.log("Starting game")
game_run()

View file

@ -0,0 +1,18 @@
import typer
from marvin_sw_text_adventure.cli.common import verbose_callback
from marvin_sw_text_adventure.tui.app import run_app
tui_app = typer.Typer()
@tui_app.callback(invoke_without_command=True)
def i(
verbose: bool = typer.Option(
False,
callback=verbose_callback,
help="show the log messages",
),
):
"interactive tui"
run_app()

View file

@ -0,0 +1,3 @@
from marvin_sw_text_adventure.standard_config import load
config = load("marvin_sw_text_adventure")

View file

@ -0,0 +1,3 @@
from rich.console import Console
console = Console()

View file

@ -0,0 +1,147 @@
from marvin import ai_fn
import pydantic
from pydantic import Field
from typing import List, Tuple
from rich.prompt import Prompt
from marvin_sw_text_adventure.console import console
@ai_fn
def get_star_wars_character() -> str:
"return a brand new never before heard of star wars character"
@ai_fn
def create_backstory(name: str) -> str:
"create a backstory for a character"
class Mission(pydantic.BaseModel):
name: str = Field(..., description='The name of a brand new never before heard of star wars mission')
place: str = Field(..., description='The place of the mission')
leader: str = Field(..., description='The name of the leader of the mission')
year: int = Field(..., description='The year of the mission')
description: str = Field(
..., description='The description of the brand new never before heard of star wars mission')
risk: int = Field(...,
description='The risk of the mission in health the character will loose')
reward: int = Field(...,
description='The reward of the mission in imperial credits')
image_prompt: str = Field(...,
description='An ai image generator prompt of the mission')
class MissionResult(pydantic.BaseModel):
success: bool = Field(..., description='The success of the mission')
imperial_credits_spent: int = Field(...,
description='The imperial credits spent on the mission')
imperial_credits_earned: int = Field(...,
description='The imperial credits earned on the mission')
health_lost: int = Field(..., description='The health lost on the mission')
health_gained: int = Field(..., description='The health gained on the mission')
story: str = Field(..., description='The story of the mission')
image_prompt: str = Field(...,
description='An ai image generator prompt of the mission')
fuel_used: int = Field(..., description='The fuel used on the mission')
class Ship(pydantic.BaseModel):
name: str = Field(..., description='The name of the ship')
ship_type: str = Field(..., description='The type of the ship')
year_built: int = Field(..., description='The year built of the ship')
capacity: int = Field(..., description='The capacity of the ship')
fuel_type: str = Field(..., description='The fuel type of the ship')
fuel_level: int = Field(..., description='The fuel level of the ship')
image_prompt: str = Field(...,
description='An ai image generator prompt of the mission')
class StarWarsCharacter(pydantic.BaseModel):
name: str = Field(...,
description='The name of a brand new never before heard of star wars character')
imperial_credits: int = Field(...,
description='The imperial credits of the character')
health: int = Field(..., description='The health of the character')
home_planet: str = Field(...,
description='The name of the planet where the character was born')
age: int = Field(..., description='The age of the character')
birth_year_bby: int = Field(..., description='The birth year of the character')
backstory: str = Field(..., description='The backstory of the character')
ship: Ship = Field(...,
description='The name of the ship where the character operates')
side: str = Field(..., description='The side of the character')
city: str = Field(..., description='The name of the city where the character lives')
friends: List[str] = Field(...,
description='The names of the friends of the character')
team: str = Field(...,
description='The name of the team that the character belongs to')
enemies: List[str] = Field(...,
description='The names of the enemies of the character')
mission: Mission = Field(..., description='The current mission of the character')
role: str = Field(..., description='The role of the character')
image_prompt: str = Field(...,
description='An ai image generator prompt of the mission')
previous_missions: List[Tuple[Mission, MissionResult]] = Field(
[], description='The previous missions of the character')
@ai_fn
def did_complete_mission(character: StarWarsCharacter, action: str) -> MissionResult:
"check if a character completed the mission or if they failed"
@ai_fn
def get_next_mission(character: StarWarsCharacter, action: str, last_mission_success: bool) -> Mission:
"""given a character, their action and the last mission success, return the next mission"""
@ai_fn
def create_character() -> StarWarsCharacter:
"create a new character"
def game():
console.print('generating your character')
prompt = Prompt()
character = create_character()
console.print(character)
while character.health > 0 and character.imperial_credits > 0 and character.ship.fuel_level > 0:
console.print('Your character status is'.center(50, '-'))
console.print(f'health: {character.health}')
console.print(f'imperial credits: {character.imperial_credits}')
console.print(f'fuel level: {character.ship.fuel_level}')
console.print()
console.print('Your current mission is'.center(50, '-'))
console.print(character.mission)
action = prompt.ask('What would you like to do? ')
result = did_complete_mission(character, action)
character.previous_missions.append((character.mission, result))
character.imperial_credits -= result.imperial_credits_spent
character.imperial_credits += result.imperial_credits_earned
character.health -= result.health_lost
character.health += result.health_gained
character.ship.fuel_level -= result.fuel_used
console.print()
console.print('Your mission has been completed')
console.print(result.story)
console.print(f'You earned {result.imperial_credits_earned} imperial credits')
console.print(f'You spent {result.imperial_credits_spent} imperial credits')
console.print(f'You gained {result.health_gained} health')
console.print(f'You lost {result.health_lost} health')
character.mission = get_next_mission(character, action, result.success)
if character.health <= 0:
console.print('You are dead')
if character.imperial_credits <= 0:
console.print('You lost all your imperial credits')
if character.ship.fuel_level <= 0:
console.print('You lost all your fuel')
if __name__ == '__main__':
game()

View file

@ -0,0 +1,239 @@
"""Standard Config.
A module to load tooling config from a users project space.
Inspired from frustrations that some tools have a tool.ini, .tool.ini,
setup.cfg, or pyproject.toml. Some allow for global configs, some don't. Some
properly follow the users home directory, others end up in a weird temp
directory. Windows home directory is only more confusing. Some will even
respect the users `$XDG_HOME` directory.
This file is for any project that can be configured in plain text such as `ini`
or `toml` and not requiring a .py file. Just name your tool and let users put
config where it makes sense to them, no need to figure out resolution order.
## Usage:
``` python
from standard_config import load
# Retrieve any overrides from the user
overrides = {'setting': True}
config = load('my_tool', overrides)
```
## Resolution Order
* First global file with a tool key
* First local file with a tool key
* Environment variables prefixed with `TOOL`
* Overrides
### Tool Specific Ini files
Ini file formats must include a `<tool>` key.
``` ini
[my_tool]
setting = True
```
### pyproject.toml
Toml files must include a `tool.<tool>` key
``` toml
[tool.my_tool]
setting = True
```
### setup.cfg
setup.cfg files must include a `tool:<tool>` key
``` ini
[tool:my_tool]
setting = True
```
### global files to consider
* <home>/tool.ini
* <home>/.tool
* <home>/.tool.ini
* <home>/.config/tool.ini
* <home>/.config/.tool
* <home>/.config/.tool.ini
### local files to consider
* <project_home>/tool.ini
* <project_home>/.tool
* <project_home>/.tool.ini
* <project_home>/pyproject.toml
* <project_home>/setup.cfg
"""
import os
from pathlib import Path
from typing import Dict, List, Union
import anyconfig
# path_spec_type = List[Dict[str, Union[Path, str, List[str\}\}\}\}
path_spec_type = List
def _get_global_path_specs(tool: str) -> path_spec_type:
"""
Generate a list of standard pathspecs for global config files.
Args:
tool (str): name of the tool to configure
"""
try:
home = Path(os.environ["XDG_HOME"])
except KeyError:
home = Path.home()
return [
{"path_specs": home / f"{tool}.ini", "ac_parser": "ini", "keys": [tool]},
{"path_specs": home / f".{tool}", "ac_parser": "ini", "keys": [tool]},
{"path_specs": home / f".{tool}.ini", "ac_parser": "ini", "keys": [tool]},
{
"path_specs": home / ".config" / f"{tool}.ini",
"ac_parser": "ini",
"keys": [tool],
},
{
"path_specs": home / ".config" / f".{tool}",
"ac_parser": "ini",
"keys": [tool],
},
{
"path_specs": home / ".config" / f".{tool}.ini",
"ac_parser": "ini",
"keys": [tool],
},
]
def _get_local_path_specs(tool: str, project_home: Union[str, Path]) -> path_spec_type:
"""
Generate a list of standard pathspecs for local, project directory config files.
Args:
tool (str): name of the tool to configure
"""
return [
{
"path_specs": Path(project_home) / f"{tool}.ini",
"ac_parser": "ini",
"keys": [tool],
},
{
"path_specs": Path(project_home) / f".{tool}",
"ac_parser": "ini",
"keys": [tool],
},
{
"path_specs": Path(project_home) / f".{tool}.ini",
"ac_parser": "ini",
"keys": [tool],
},
{
"path_specs": Path(project_home) / f"{tool}.yml",
"ac_parser": "yaml",
"keys": [tool],
},
{
"path_specs": Path(project_home) / f".{tool}.yml",
"ac_parser": "yaml",
"keys": [tool],
},
{
"path_specs": Path(project_home) / f"{tool}.toml",
"ac_parser": "toml",
"keys": [tool],
},
{
"path_specs": Path(project_home) / f".{tool}.toml",
"ac_parser": "toml",
"keys": [tool],
},
{
"path_specs": Path(project_home) / "pyproject.toml",
"ac_parser": "toml",
"keys": ["tool", tool],
},
{
"path_specs": Path(project_home) / "setup.cfg",
"ac_parser": "ini",
"keys": [f"tool.{tool}"],
},
]
def _get_attrs(attrs: list, config: Dict) -> Dict:
"""Get nested config data from a list of keys.
specifically written for pyproject.toml which needs to get `tool` then `<tool>`
"""
for attr in attrs:
config = config[attr]
return config
def _load_files(config_path_specs: path_spec_type) -> Dict:
"""Use anyconfig to load config files stopping at the first one that exists.
config_path_specs (list): a list of pathspecs and keys to load
"""
for file in config_path_specs:
if file["path_specs"].exists():
config = anyconfig.load(**file)
else:
# ignore missing files
continue
try:
return _get_attrs(file["keys"], config)
except KeyError:
# ignore incorrect keys
continue
return {}
def _load_env(tool: str) -> Dict:
"""Load config from environment variables.
Args:
tool (str): name of the tool to configure
"""
vars = [var for var in os.environ.keys() if var.startswith(tool.upper())]
return {
var.lower().strip(tool.lower()).strip("_").strip("-"): os.environ[var]
for var in vars
}
def load(tool: str, project_home: Union[Path, str] = ".", overrides: Dict = {}) -> Dict:
"""Load tool config from standard config files.
Resolution Order
* First global file with a tool key
* First local file with a tool key
* Environment variables prefixed with `TOOL`
* Overrides
Args:
tool (str): name of the tool to configure
"""
global_config = _load_files(_get_global_path_specs(tool))
local_config = _load_files(_get_local_path_specs(tool, project_home))
env_config = _load_env(tool)
return {**global_config, **local_config, **env_config, **overrides}

View file

@ -0,0 +1,18 @@
Screen {
align: center middle;
layers: main footer;
}
Sidebar {
height: 100vh;
width: auto;
min-width: 20;
background: $secondary-background-darken-2;
dock: left;
margin-right: 1;
layer: main;
}
Footer {
layer: footer;
}

View file

@ -0,0 +1,62 @@
from pathlib import Path
from textual.app import App, ComposeResult
from textual.containers import Container
from textual.css.query import NoMatches
from textual.widgets import Footer, Static
from marvin_sw_text_adventure.config import config
config["tui"] = {}
config["tui"]["bindings"] = {}
class Sidebar(Static):
def compose(self) -> ComposeResult:
yield Container(
Static("sidebar"),
id="sidebar",
)
class Tui(App):
"""A Textual app to manage requests."""
CSS_PATH = Path("__file__").parent / "app.css"
BINDINGS = [tuple(b.values()) for b in config["tui"]["bindings"]]
def compose(self) -> ComposeResult:
"""Create child widgets for the app."""
yield Container(Static("hello world"))
yield Footer()
def action_toggle_dark(self) -> None:
"""An action to toggle dark mode."""
self.dark = not self.dark
def action_toggle_sidebar(self):
try:
self.query_one("PromptSidebar").remove()
except NoMatches:
self.mount(Sidebar())
def run_app():
import os
import sys
from textual.features import parse_features
dev = "--dev" in sys.argv
features = set(parse_features(os.environ.get("TEXTUAL", "")))
if dev:
features.add("debug")
features.add("devtools")
os.environ["TEXTUAL"] = ",".join(sorted(features))
app = Tui()
app.run()
if __name__ == "__main__":
run_app()