Skip to content

Commit 60db065

Browse files
authored
Merge pull request #7 from cpp-gamedev/pydev
Data Collection & ASCII Image
2 parents fad8b0f + 554febd commit 60db065

File tree

4 files changed

+227
-2
lines changed

4 files changed

+227
-2
lines changed

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,4 +361,10 @@ MigrationBackup/
361361
.ionide/
362362

363363
# Fody - auto-generated XML schema
364-
FodyWeavers.xsd
364+
FodyWeavers.xsd
365+
366+
# Python - virtual environment
367+
venv/
368+
369+
# project-specific directories
370+
assets/

README.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
<a alt="CMake Version" title="CMake Version">
1414
<img src="https://img.shields.io/badge/CMake-3.18+-blue">
1515
</a>
16+
<a alt="Python Version" title="Python Version">
17+
<img src="https://img.shields.io/badge/Python-3.8%20|%203.9-blue">
18+
</a>
1619
<a href="https://www.gnu.org/licenses/gpl-3.0.en.html" alt="License" title="License">
1720
<img src="https://img.shields.io/badge/License-GPLv3-blue.svg">
1821
</a>
@@ -22,7 +25,34 @@
2225

2326
### Generating new Pokemon
2427

25-
TODO
28+
If this is your first time using a python script, use
29+
30+
```bash
31+
$ python -m venv venv/
32+
$ source venv/Scripts/activate
33+
$ pip install -r requirements.txt
34+
```
35+
36+
to install the dependencies in a virtual environment. Note that this script
37+
assumes that it is being run from the project's root directory. After that
38+
you should be able to use this script:
39+
40+
```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
45+
```
46+
47+
You can also use the `--name` option for identifying a new pokemon. Repeat both
48+
steps - you need at least two pokemon to play this game. In case of doubt, use
49+
50+
```bash
51+
$ python gen_data.py --help
52+
```
53+
54+
to get more help. Once you've obtained these files, build the project to play
55+
this game. Use the `--mirror` flag for your own pokemon (recommended).
2656

2757
### On Visual Studio (Windows)
2858

gen_data.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env python3
2+
3+
"""
4+
PKMN Utility Tool (C) hentai-chan <[email protected]>
5+
6+
Description
7+
-----------
8+
This script uses the Pokemon API to create a JSON file for the pkmn game.
9+
10+
Usage
11+
-----
12+
Run 'python gen_data.py make --name <pokemon>' from the project's root directory
13+
to create a new JSON file for pkmn. Append the '--help' option for more help.
14+
15+
Additionally, use `python gen_data.py ascii --name <pokemon>` to create a new
16+
ascii image. In both instances, a ID may be used as primary argument instead.
17+
"""
18+
19+
import json
20+
from pathlib import Path
21+
from random import randint, random
22+
from typing import List
23+
from urllib.parse import urljoin
24+
25+
import click
26+
import colorama
27+
import requests
28+
from click import style
29+
from colorama import Fore, Style
30+
from PIL import Image, ImageOps
31+
from rich.console import Console
32+
from rich.progress import track
33+
34+
#region Image Processing
35+
36+
CHARS = [' ', '.', 'o', 'v', '@', '#', 'W']
37+
38+
def resize_image(img: Image, new_width=20) -> Image:
39+
width, height = img.size
40+
aspect_ratio = height / float(width)
41+
return img.resize((new_width, int(aspect_ratio * new_width)), Image.BILINEAR)
42+
43+
def detect_color(c: str, r:int, g:int, b:int) -> str:
44+
# thresholds were determined experimentally
45+
if sum([r, g, b]) > 550:
46+
return ''.join((Fore.WHITE, c, Style.RESET_ALL))
47+
elif r + g > 250 and b < 100:
48+
return ''.join((Fore.YELLOW, c, Style.RESET_ALL))
49+
elif r + b > 250 and g < 100:
50+
return ''.join((Fore.MAGENTA, c, Style.RESET_ALL))
51+
elif g + b > 250 and r < 100:
52+
return ''.join((Fore.CYAN, c, Style.RESET_ALL))
53+
elif max(r, g, b) == r:
54+
return ''.join((Fore.RED, c, Style.RESET_ALL))
55+
elif max(r, g, b) == g:
56+
return ''.join((Fore.GREEN, c, Style.RESET_ALL))
57+
else:
58+
return ''.join((Fore.BLUE, c, Style.RESET_ALL))
59+
60+
def img2ascii(image, width=20, mirror_image=False) -> List[str]:
61+
image = resize_image(image, new_width=width).convert('RGB')
62+
63+
if mirror_image:
64+
image = ImageOps.mirror(image)
65+
66+
data = image.load()
67+
68+
ascii_art = []
69+
for w in range(width):
70+
for h in (height:=range(width)):
71+
r, g, b = data[h, w]
72+
char = int(r/3 + g/3 + b/3)
73+
ascii_art.append(detect_color(CHARS[int(char * len(CHARS) // 256)], r, g, b))
74+
ascii_art.append('\n')
75+
return ascii_art
76+
77+
#endregion
78+
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):
100+
result = None
101+
query = name or id
102+
console = ctx.obj['CONSOLE']
103+
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()
107+
level = randint(30, 60)
108+
result = {
109+
'id': response['id'],
110+
'name': response['name'],
111+
'level': level,
112+
'hp': int(2 * level * (0.8 + 0.4 * random())), # formula for hp is a rough estimation
113+
'atk': 1,
114+
'def': 1,
115+
}
116+
117+
# the base api does not provide detailed information about moves,
118+
# so we need to make more calls to the api (+1 per move)
119+
moves = {}
120+
endpoints = [move['move']['url'] for move in response['moves']]
121+
for index, endpoint in track(enumerate(endpoints), "Sending Requests", total=len(endpoints), transient=True):
122+
move_response = requests.get(endpoint).json()
123+
moves[index] = {
124+
'name': move_response['names'][7]['name'],
125+
'accuracy': int(move_response['accuracy']) if move_response['accuracy'] is not None else None,
126+
'effect_changes': move_response['effect_changes'],
127+
'effect_chance': int(move_response['effect_chance']) if move_response['effect_chance'] is not None else None,
128+
'power': int(move_response['power']) if move_response['power'] is not None else None,
129+
'flavor_text_entries': [entry['flavor_text'] for entry in move_response['flavor_text_entries'] if entry['language']['name'] == 'en']
130+
}
131+
result['moves'] = moves
132+
133+
with open(ctx.obj['ASSETS'].joinpath(f"{result['id']}.json"), mode='w', encoding='utf-8') as file_handler:
134+
json.dump(result, file_handler)
135+
136+
if ctx.obj['VERBOSE']:
137+
console.print(result)
138+
click.echo('\n')
139+
140+
click.secho(f"Done! A new JSON file was created in '{ctx.obj['ASSETS']}/'.", fg='bright_yellow')
141+
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+
150+
colorama.init(autoreset=False)
151+
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)
163+
with open(image_path, mode='wb') as file_handler:
164+
for chunk in response.iter_content(1024):
165+
file_handler.write(chunk)
166+
167+
# then generate the ascii image and store the result in assets/{id}.txt
168+
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:
170+
file_handler.writelines(ascii_art)
171+
172+
# cleanup
173+
image_path.unlink(missing_ok=True)
174+
175+
if ctx.obj['VERBOSE']:
176+
click.echo(f"\n{''.join(ascii_art)}")
177+
178+
click.secho(f"Done! A new ASCII image was created in '{ctx.obj['ASSETS']}/'.", fg='bright_yellow')
179+
180+
if __name__ == '__main__':
181+
try:
182+
cli(obj={})
183+
except KeyboardInterrupt:
184+
pass

requirements.txt

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

0 commit comments

Comments
 (0)