|
| 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 |
0 commit comments