Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -366,5 +366,6 @@ FodyWeavers.xsd
# Python - virtual environment
venv/

# project-specific directories
assets/
# project-specific files and directories
assets/
manifest.json
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,19 @@ If this is your first time using a python script, use
```bash
$ python -m venv venv/
$ source venv/Scripts/activate
$ pip install -r requirements.txt
$ python -m pip install --upgrade pip
$ pip install -r requirements.txt --only-binary all
```

to install the dependencies in a virtual environment. Note that this script
assumes that it is being run from the project's root directory. After that
you should be able to use this script:

```bash
$ # creates a new JSON file in assets/
$ python gen_data.py make --id 1
$ # creates a new ascii image as txt file in assets/ and prints a preview
$ python gen_data.py --verbose ascii --id 1 --mirror
$ # creates two new pkmn data sets (sprite + data)
$ python gen_data.py --verbose make --id 1 4
$ # create manifest.json
$ python gen_data.py manifest
```

You can also use the `--name` option for identifying a new pokemon. Repeat both
Expand Down
143 changes: 80 additions & 63 deletions gen_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,33 @@
ascii image. In both instances, a ID may be used as primary argument instead.
"""

import argparse
import json
import os
from collections import Counter
from pathlib import Path
from pprint import pprint
from random import randint, random
from typing import List
from urllib.parse import urljoin

import click
import colorama
import requests
from click import style
from colorama import Fore, Style
from colorama.ansi import clear_screen
from PIL import Image, ImageOps
from rich.console import Console
from rich.progress import track

#region Image Processing
__version__ = "0.0.1"
assets = Path('assets/')
assets.mkdir(parents=True, exist_ok=True)
ASSETS = assets
BASE_API = "https://pokeapi.co/api/v2/pokemon/"
SPRITE_API = "https://pokeres.bastionbot.org/images/pokemon/"
CONSOLE = Console()

#region image processing

CHARS = [' ', '.', 'o', 'v', '@', '#', 'W']

Expand Down Expand Up @@ -76,34 +87,12 @@ def img2ascii(image, width=20, mirror_image=False) -> List[str]:

#endregion

CONTEXT_SETTINGS = dict(max_content_width=120)

@click.group(invoke_without_command=True, context_settings=CONTEXT_SETTINGS, help=style("PKMN utility tool.", fg='bright_magenta'))
@click.option('--verbose', is_flag=True, default=False, help=style("Enable verbose terminal output.", fg='bright_yellow'))
@click.version_option(version='0.0.1', prog_name="get-data", help=style("Show the version and exit.", fg='bright_yellow'))
@click.pass_context
def cli(ctx, verbose):
ctx.ensure_object(dict)
assets = Path('assets/')
assets.mkdir(parents=True, exist_ok=True)
ctx.obj['ASSETS'] = assets
ctx.obj['BASE_API'] = "https://pokeapi.co/api/v2/pokemon/"
ctx.obj['SPRITE_API'] = "https://pokeres.bastionbot.org/images/pokemon/"
ctx.obj['CONSOLE'] = Console()
ctx.obj['VERBOSE'] = verbose

@cli.command(context_settings=CONTEXT_SETTINGS, help=style("Create a new PKMN data file.", fg='bright_green'))
@click.option('--name', type=click.STRING, help=style("Name of a pokemon (English).", fg='bright_yellow'))
@click.option('--id', type=click.STRING, help=style("A pokemon ID.", fg='bright_yellow'))
@click.pass_context
def make(ctx, name, id):
def req_pkmn_data(id_: int, verbose: bool) -> None:
result = None
query = name or id
console = ctx.obj['CONSOLE']

# make result is stored in assets/{id}.json
with console.status('Making initial request . . .', spinner='dots3') as _:
response = requests.get(urljoin(ctx.obj['BASE_API'], query)).json()
# result is stored in assets/{id}.json
with CONSOLE.status('Making initial request . . .', spinner='dots3') as _:
response = requests.get(urljoin(BASE_API, str(id_))).json()
level = randint(30, 60)
result = {
'id': response['id'],
Expand All @@ -130,55 +119,83 @@ def make(ctx, name, id):
}
result['moves'] = moves

with open(ctx.obj['ASSETS'].joinpath(f"{result['id']}.json"), mode='w', encoding='utf-8') as file_handler:
with open(ASSETS.joinpath(f"{result['id']}.json"), mode='w', encoding='utf-8') as file_handler:
json.dump(result, file_handler)

if ctx.obj['VERBOSE']:
console.print(result)
click.echo('\n')
if verbose:
pprint(result)

click.secho(f"Done! A new JSON file was created in '{ctx.obj['ASSETS']}/'.", fg='bright_yellow')
print(f"{Fore.YELLOW}Done! A new JSON file was created in {str(ASSETS)!r}.{Style.RESET_ALL}")

@cli.command(context_settings=CONTEXT_SETTINGS, help=style("Create an ASCII image.", fg='bright_green'))
@click.option('--name', type=click.STRING, help=style("Name of a pokemon (English).", fg='bright_yellow'))
@click.option('--id', type=click.STRING, help=style("A pokemon ID.", fg='bright_yellow'))
@click.option('--mirror/--no-mirror', is_flag=True, default=False, help=style("Mirror image (Player).", fg='bright_yellow'))
@click.pass_context
def ascii(ctx, name, id, mirror):
query = name or id

def gen_sprite(id_: int, mirror: bool, verbose: bool) -> None:
colorama.init(autoreset=False)

# the base api only contains very small sprites,
# but there's another API which provides higher
# quality sprites which are only searchable by id
with ctx.obj['CONSOLE'].status('Creating new ASCII image . . .', spinner='dots3') as _:
if name:
query = requests.get(urljoin(ctx.obj['BASE_API'], query)).json()['id']

# first find and download the pokemon sprite
filename = f"{query}.png"
image_path = ctx.obj['ASSETS'].joinpath(filename)
response = requests.get(urljoin(ctx.obj['SPRITE_API'], filename), stream=True)
with CONSOLE.status('Creating new ASCII image . . .', spinner='dots3') as _:
filename = f"{id_}.png"
image_path = ASSETS.joinpath(filename)
response = requests.get(urljoin(SPRITE_API, filename), stream=True)
with open(image_path, mode='wb') as file_handler:
for chunk in response.iter_content(1024):
file_handler.write(chunk)

# then generate the ascii image and store the result in assets/{id}.txt
ascii_art = img2ascii(Image.open(image_path), width=20, mirror_image=mirror)
with open(ctx.obj['ASSETS'].joinpath(f"{query}.txt"), mode='w', encoding='utf-8') as file_handler:
with open(ASSETS.joinpath(f"{id_}.txt"), mode='w', encoding='utf-8') as file_handler:
file_handler.writelines(ascii_art)

# cleanup
image_path.unlink(missing_ok=True)

if ctx.obj['VERBOSE']:
click.echo(f"\n{''.join(ascii_art)}")
if verbose:
print(f"\n{''.join(ascii_art)}")

print(f"{Fore.YELLOW}Done! A new ASCII image was created in {str(ASSETS)!r}.{Style.RESET_ALL}")

def check_manifest(verbose: bool) -> None:
extensions = ['.txt', '.json']

files = list(filter(
lambda file: file.suffix in extensions and file.stem.isnumeric(),
[file for file in ASSETS.glob(r'**/*')]
))

ids = list(map(lambda file: int(file.stem), files))
duplicates = [id_ for id_, count in Counter(ids).items() if count > 1]

manifest = {
'files': list(map(str, files)),
'duplicates': duplicates,
'game_ready': len(duplicates) >= 2,
}

with open("manifest.json", mode='w', encoding='utf-8') as file_handler:
json.dump(manifest, file_handler)

if verbose: pprint(manifest)

def main():
parser = argparse.ArgumentParser()

parser.add_argument('--version', action='version', version=f"%(prog)s {__version__}")
parser.add_argument('--verbose', action='store_true', help="increase output verbosity")

subparser = parser.add_subparsers(dest='command')

make_parser = subparser.add_parser('make', help="create new pkmn data")
make_parser.add_argument('--id', type=int, nargs='+', required=True, help="one or more pokemon id")
make_parser.add_argument('--mirror', action='store_true', help="mirror sprite")

manifest_parser = subparser.add_parser('manifest', help="validate manifest")

args = parser.parse_args()

if args.command == 'make':
for id_ in args.id:
req_pkmn_data(id_, verbose=False)
gen_sprite(id_, args.mirror, args.verbose)
elif args.command == 'manifest':
check_manifest(args.verbose)
else:
raise NotImplementedError()

click.secho(f"Done! A new ASCII image was created in '{ctx.obj['ASSETS']}/'.", fg='bright_yellow')

if __name__ == '__main__':
try:
cli(obj={})
except KeyboardInterrupt:
pass
main()
5 changes: 2 additions & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
click==7.1.2
colorama==0.4.4
pillow==8.1.2
pillow==8.2.0
requests==2.25.1
rich==9.13.0
rich==10.2.2
12 changes: 12 additions & 0 deletions src/anim.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include <filesystem>
#include <iostream>
#include <string>
#include <vector>
Expand Down Expand Up @@ -27,3 +28,14 @@ std::vector<std::string> gen_healthbar(std::string name, int level, int hp, int

return {label, progressbar, hitpoints};
}

std::vector<std::string> load_sprite(int id, const std::filesystem::path& assets)
{
for (const auto& file : std::filesystem::directory_iterator(assets))
{
if (file.path().extension() == ".txt" && std::stoi(file.path().stem().string()) == id)
{
return read_file(file);
}
}
}
4 changes: 3 additions & 1 deletion src/anim.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@
#include<string>
#include<vector>

std::vector<std::string> gen_healthbar(std::string name, int level, int hp, int max_hp);
std::vector<std::string> gen_healthbar(std::string name, int level, int hp, int max_hp);

std::vector<std::string> load_sprite(int id, const std::filesystem::path& assets);
42 changes: 42 additions & 0 deletions src/utils.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include <algorithm>
#include <chrono>
#include <fstream>
#include <filesystem>
#include <iostream>
#include <random>
#include <string>
Expand Down Expand Up @@ -40,3 +42,43 @@ std::string style(std::string text, Color fore, Color back = Color::BLACK)

return ansi_text.append(kt::format_str("{}\033[0m", text));
}

std::filesystem::path find_upwards(std::string dir_name, int max_depth = 10)
{
auto path = std::filesystem::current_path() / std::filesystem::path(dir_name);

while (!std::filesystem::exists(path) && max_depth > 0)
{
max_depth -= 1;
path = path.parent_path().parent_path() / path.stem();
}

return (max_depth > 0) ? path : std::filesystem::path();
}

bool validate_asset_dir(const std::filesystem::path& asset_dir)
{
// check manifest here
return true;
}

std::vector<std::string> read_file(const std::filesystem::path& path)
{
std::string line{};
std::vector<std::string> lines{};
std::ifstream file{path.string()};

if (std::filesystem::exists(path))
{
while (getline(file, line))
{
lines.push_back(line + '\n');
}
}
else
{
std::cerr << "File not found: " << path.string() << '\n';
}

return lines;
}
7 changes: 7 additions & 0 deletions src/utils.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <algorithm>
#include <filesystem>
#include <iostream>
#include <random>
#include <type_traits>
Expand Down Expand Up @@ -64,3 +65,9 @@ enum class Color
};

std::string style(std::string text, Color fore, Color back = Color::BLACK);

std::filesystem::path find_upwards(std::string dir_name, int max_depth = 10);

bool validate_asset_dir(const std::filesystem::path& asset_dir);

std::vector<std::string> read_file(const std::filesystem::path& path);