Skip to content

Commit 5b37935

Browse files
authored
Merge pull request #11 from cpp-gamedev/issue-8
Issue 8
2 parents 60db065 + 4941cae commit 5b37935

File tree

8 files changed

+155
-74
lines changed

8 files changed

+155
-74
lines changed

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -366,5 +366,6 @@ FodyWeavers.xsd
366366
# Python - virtual environment
367367
venv/
368368

369-
# project-specific directories
370-
assets/
369+
# project-specific files and directories
370+
assets/
371+
manifest.json

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,19 @@ If this is your first time using a python script, use
3030
```bash
3131
$ python -m venv venv/
3232
$ source venv/Scripts/activate
33-
$ pip install -r requirements.txt
33+
$ python -m pip install --upgrade pip
34+
$ pip install -r requirements.txt --only-binary all
3435
```
3536

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

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

4748
You can also use the `--name` option for identifying a new pokemon. Repeat both

gen_data.py

Lines changed: 80 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -16,22 +16,33 @@
1616
ascii image. In both instances, a ID may be used as primary argument instead.
1717
"""
1818

19+
import argparse
1920
import json
21+
import os
22+
from collections import Counter
2023
from pathlib import Path
24+
from pprint import pprint
2125
from random import randint, random
2226
from typing import List
2327
from urllib.parse import urljoin
2428

25-
import click
2629
import colorama
2730
import requests
28-
from click import style
2931
from colorama import Fore, Style
32+
from colorama.ansi import clear_screen
3033
from PIL import Image, ImageOps
3134
from rich.console import Console
3235
from rich.progress import track
3336

34-
#region Image Processing
37+
__version__ = "0.0.1"
38+
assets = Path('assets/')
39+
assets.mkdir(parents=True, exist_ok=True)
40+
ASSETS = assets
41+
BASE_API = "https://pokeapi.co/api/v2/pokemon/"
42+
SPRITE_API = "https://pokeres.bastionbot.org/images/pokemon/"
43+
CONSOLE = Console()
44+
45+
#region image processing
3546

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

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

7788
#endregion
7889

79-
CONTEXT_SETTINGS = dict(max_content_width=120)
80-
81-
@click.group(invoke_without_command=True, context_settings=CONTEXT_SETTINGS, help=style("PKMN utility tool.", fg='bright_magenta'))
82-
@click.option('--verbose', is_flag=True, default=False, help=style("Enable verbose terminal output.", fg='bright_yellow'))
83-
@click.version_option(version='0.0.1', prog_name="get-data", help=style("Show the version and exit.", fg='bright_yellow'))
84-
@click.pass_context
85-
def cli(ctx, verbose):
86-
ctx.ensure_object(dict)
87-
assets = Path('assets/')
88-
assets.mkdir(parents=True, exist_ok=True)
89-
ctx.obj['ASSETS'] = assets
90-
ctx.obj['BASE_API'] = "https://pokeapi.co/api/v2/pokemon/"
91-
ctx.obj['SPRITE_API'] = "https://pokeres.bastionbot.org/images/pokemon/"
92-
ctx.obj['CONSOLE'] = Console()
93-
ctx.obj['VERBOSE'] = verbose
94-
95-
@cli.command(context_settings=CONTEXT_SETTINGS, help=style("Create a new PKMN data file.", fg='bright_green'))
96-
@click.option('--name', type=click.STRING, help=style("Name of a pokemon (English).", fg='bright_yellow'))
97-
@click.option('--id', type=click.STRING, help=style("A pokemon ID.", fg='bright_yellow'))
98-
@click.pass_context
99-
def make(ctx, name, id):
90+
def req_pkmn_data(id_: int, verbose: bool) -> None:
10091
result = None
101-
query = name or id
102-
console = ctx.obj['CONSOLE']
10392

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

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

136-
if ctx.obj['VERBOSE']:
137-
console.print(result)
138-
click.echo('\n')
125+
if verbose:
126+
pprint(result)
139127

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

142-
@cli.command(context_settings=CONTEXT_SETTINGS, help=style("Create an ASCII image.", fg='bright_green'))
143-
@click.option('--name', type=click.STRING, help=style("Name of a pokemon (English).", fg='bright_yellow'))
144-
@click.option('--id', type=click.STRING, help=style("A pokemon ID.", fg='bright_yellow'))
145-
@click.option('--mirror/--no-mirror', is_flag=True, default=False, help=style("Mirror image (Player).", fg='bright_yellow'))
146-
@click.pass_context
147-
def ascii(ctx, name, id, mirror):
148-
query = name or id
149-
130+
def gen_sprite(id_: int, mirror: bool, verbose: bool) -> None:
150131
colorama.init(autoreset=False)
151132

152-
# the base api only contains very small sprites,
153-
# but there's another API which provides higher
154-
# quality sprites which are only searchable by id
155-
with ctx.obj['CONSOLE'].status('Creating new ASCII image . . .', spinner='dots3') as _:
156-
if name:
157-
query = requests.get(urljoin(ctx.obj['BASE_API'], query)).json()['id']
158-
159-
# first find and download the pokemon sprite
160-
filename = f"{query}.png"
161-
image_path = ctx.obj['ASSETS'].joinpath(filename)
162-
response = requests.get(urljoin(ctx.obj['SPRITE_API'], filename), stream=True)
133+
with CONSOLE.status('Creating new ASCII image . . .', spinner='dots3') as _:
134+
filename = f"{id_}.png"
135+
image_path = ASSETS.joinpath(filename)
136+
response = requests.get(urljoin(SPRITE_API, filename), stream=True)
163137
with open(image_path, mode='wb') as file_handler:
164138
for chunk in response.iter_content(1024):
165139
file_handler.write(chunk)
166140

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

172-
# cleanup
173145
image_path.unlink(missing_ok=True)
174146

175-
if ctx.obj['VERBOSE']:
176-
click.echo(f"\n{''.join(ascii_art)}")
147+
if verbose:
148+
print(f"\n{''.join(ascii_art)}")
149+
150+
print(f"{Fore.YELLOW}Done! A new ASCII image was created in {str(ASSETS)!r}.{Style.RESET_ALL}")
151+
152+
def check_manifest(verbose: bool) -> None:
153+
extensions = ['.txt', '.json']
154+
155+
files = list(filter(
156+
lambda file: file.suffix in extensions and file.stem.isnumeric(),
157+
[file for file in ASSETS.glob(r'**/*')]
158+
))
159+
160+
ids = list(map(lambda file: int(file.stem), files))
161+
duplicates = [id_ for id_, count in Counter(ids).items() if count > 1]
162+
163+
manifest = {
164+
'files': list(map(str, files)),
165+
'duplicates': duplicates,
166+
'game_ready': len(duplicates) >= 2,
167+
}
168+
169+
with open("manifest.json", mode='w', encoding='utf-8') as file_handler:
170+
json.dump(manifest, file_handler)
171+
172+
if verbose: pprint(manifest)
173+
174+
def main():
175+
parser = argparse.ArgumentParser()
176+
177+
parser.add_argument('--version', action='version', version=f"%(prog)s {__version__}")
178+
parser.add_argument('--verbose', action='store_true', help="increase output verbosity")
179+
180+
subparser = parser.add_subparsers(dest='command')
181+
182+
make_parser = subparser.add_parser('make', help="create new pkmn data")
183+
make_parser.add_argument('--id', type=int, nargs='+', required=True, help="one or more pokemon id")
184+
make_parser.add_argument('--mirror', action='store_true', help="mirror sprite")
185+
186+
manifest_parser = subparser.add_parser('manifest', help="validate manifest")
187+
188+
args = parser.parse_args()
189+
190+
if args.command == 'make':
191+
for id_ in args.id:
192+
req_pkmn_data(id_, verbose=False)
193+
gen_sprite(id_, args.mirror, args.verbose)
194+
elif args.command == 'manifest':
195+
check_manifest(args.verbose)
196+
else:
197+
raise NotImplementedError()
177198

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

180200
if __name__ == '__main__':
181-
try:
182-
cli(obj={})
183-
except KeyboardInterrupt:
184-
pass
201+
main()

requirements.txt

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
click==7.1.2
21
colorama==0.4.4
3-
pillow==8.1.2
2+
pillow==8.2.0
43
requests==2.25.1
5-
rich==9.13.0
4+
rich==10.2.2

src/anim.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#include <filesystem>
12
#include <iostream>
23
#include <string>
34
#include <vector>
@@ -27,3 +28,14 @@ std::vector<std::string> gen_healthbar(std::string name, int level, int hp, int
2728

2829
return {label, progressbar, hitpoints};
2930
}
31+
32+
std::vector<std::string> load_sprite(int id, const std::filesystem::path& assets)
33+
{
34+
for (const auto& file : std::filesystem::directory_iterator(assets))
35+
{
36+
if (file.path().extension() == ".txt" && std::stoi(file.path().stem().string()) == id)
37+
{
38+
return read_file(file);
39+
}
40+
}
41+
}

src/anim.hpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@
33
#include<string>
44
#include<vector>
55

6-
std::vector<std::string> gen_healthbar(std::string name, int level, int hp, int max_hp);
6+
std::vector<std::string> gen_healthbar(std::string name, int level, int hp, int max_hp);
7+
8+
std::vector<std::string> load_sprite(int id, const std::filesystem::path& assets);

src/utils.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
#include <algorithm>
22
#include <chrono>
3+
#include <fstream>
4+
#include <filesystem>
35
#include <iostream>
46
#include <random>
57
#include <string>
@@ -40,3 +42,43 @@ std::string style(std::string text, Color fore, Color back = Color::BLACK)
4042

4143
return ansi_text.append(kt::format_str("{}\033[0m", text));
4244
}
45+
46+
std::filesystem::path find_upwards(std::string dir_name, int max_depth = 10)
47+
{
48+
auto path = std::filesystem::current_path() / std::filesystem::path(dir_name);
49+
50+
while (!std::filesystem::exists(path) && max_depth > 0)
51+
{
52+
max_depth -= 1;
53+
path = path.parent_path().parent_path() / path.stem();
54+
}
55+
56+
return (max_depth > 0) ? path : std::filesystem::path();
57+
}
58+
59+
bool validate_asset_dir(const std::filesystem::path& asset_dir)
60+
{
61+
// check manifest here
62+
return true;
63+
}
64+
65+
std::vector<std::string> read_file(const std::filesystem::path& path)
66+
{
67+
std::string line{};
68+
std::vector<std::string> lines{};
69+
std::ifstream file{path.string()};
70+
71+
if (std::filesystem::exists(path))
72+
{
73+
while (getline(file, line))
74+
{
75+
lines.push_back(line + '\n');
76+
}
77+
}
78+
else
79+
{
80+
std::cerr << "File not found: " << path.string() << '\n';
81+
}
82+
83+
return lines;
84+
}

src/utils.hpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22

33
#include <algorithm>
4+
#include <filesystem>
45
#include <iostream>
56
#include <random>
67
#include <type_traits>
@@ -64,3 +65,9 @@ enum class Color
6465
};
6566

6667
std::string style(std::string text, Color fore, Color back = Color::BLACK);
68+
69+
std::filesystem::path find_upwards(std::string dir_name, int max_depth = 10);
70+
71+
bool validate_asset_dir(const std::filesystem::path& asset_dir);
72+
73+
std::vector<std::string> read_file(const std::filesystem::path& path);

0 commit comments

Comments
 (0)