Repository: UpGado/ascii_racer Branch: master Commit: 36ab0185399d Files: 17 Total size: 20.6 KB Directory structure: gitextract_36pgqf48/ ├── .gitignore ├── .replit ├── .travis.yml ├── LICENSE ├── README.md ├── asciiracer/ │ ├── __init__.py │ ├── __main__.py │ ├── ascii_factory.py │ ├── config.py │ ├── environment.py │ ├── game.py │ ├── hud.py │ ├── mechanics.py │ ├── misc.py │ └── tests/ │ └── test_general.py ├── requirements.txt └── setup.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ ================================================ FILE: .replit ================================================ language = "python3" run = "python -m asciiracer" ================================================ FILE: .travis.yml ================================================ language: python python: - "3.6" - "3.7" - "nightly" install: - pip install -r requirements.txt - pip install flake8 before_script: - flake8 . script: - pytest deploy: provider: pypi user: "UpGado" password: secure: bDLod+Mf1eV5e1uE39qtrjR3OCp5NlsnXa+GdhtbRi6tgkgb63foVAB/z0F2kKxzTHWgT6Zl+IIH9m4bZPvpblITKeLke16gG6s1qoXPkEIYInBTOYhJxe9noHvox6mD9dcffX3MZv5O0UOYnujOm0Mb7iIASJzInL8o54ALF0jar8/MLcJPVE+xGYIIgperUhgZPHcAEvpYUZTGsJ4EAHiTnPaq4cFw7FGf7KJJXKZ1aiuI0nu1xJFSvVwrCKd0S96y3+2+xe/DocHap7ZX8fau5CwtQ4CSbXew7dv+U2mYv3hdxxxZh0if10IgIn2iNrw6Tomps98XLuzm5aPDX7eKBeSJdwrZ6rDh/SUODOr34yGSZoXE3eUThqRaSsy+JGwUYleG4HlxiR/KhdD+H1avrnOMQavP9FY3g93BGmnvtBPfcHg6gn46gxV1Z9zOvSRjlDn0erQ/UEZtCiy6to8OoRdXudxYffhfNcv9MlWyR+jPB9wqmJbGdM3Ao0WHwLFYFUNo1Z1/38rnGQwtdErwTU41GsnEYhgKD1Z38NRKnXnseYtGDvy66dugI2ukkxc5LK1V8Hu6r6WHeObQdUlMBAGZ4GOli0YAh5zi8RXcW9FR8xVeeYOD8/p5T4RQh5QMBMsdmkalnPzYf783oPS0YE/Pp0yY1tG9UxjsND0= on: tags: true python: "3.7" distributions: "sdist bdist_wheel" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Ahmed Gado Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ ``` _ _ /\ (_(_) / \ ___ ___ _ _ _ __ __ _ ___ ___ _ __ / /\ \ / __|/ __| | | | '__/ _` |/ __/ _ | '__| / ____ \\__ | (__| | | | | | (_| | (_| __| | /_/ \_|___/\___|_|_| |_| \__,_|\___\___|_| ``` ![PyPI](https://img.shields.io/pypi/v/asciiracer?color=success&label=pypi%20package) [![Build Status](https://travis-ci.com/UpGado/ascii_racer.svg?branch=master)](https://travis-ci.com/UpGado/ascii_racer) ![GitHub last commit](https://img.shields.io/github/last-commit/UpGado/ascii_racer) [![Downloads](https://pepy.tech/badge/asciiracer)](https://pepy.tech/project/asciiracer) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) An endless racing game that runs in the terminal. 100% Python.

ascii-racer

## Instructions Collect as many alcoholic drinks as possible, while avoiding the `Beer` drinks. The game is only key-based. | Keys | Role | |------|-------------| | a | Move Left | | d | Move Right | | w | Accelerate | | s | Decelerate | | q | Quit game | ### Installation > ```diff > + Please report issues if you try to install and run into problems! > ``` Make sure you are running at least Python 3.6.0 Install using pip: ```bash pip3 install asciiracer ``` or clone the repository and install manually: ```bash $ git clone https://github.com/UpGado/ascii_racer.git $ cd ascii_racer && python3 setup.py install ``` ### Start Game To start the game, run either: ```bash $ asciiracer $ python -m asciiracer ``` ### Scoring There are four different types of drinks that you can collect on the racetrack. * Vodka - 10 Points * Gin - 5 Points * $ - 1 Point * Beer - Negative 20 points ### Contributions > ```diff > + If you think this is cool, fork it and make it cooler! > ``` This might be great practice if you want to learn Python, and you can personally reach out to me if you have any questions about the *simple but elegant* code base. #### Possible Improvements - Color support. - Curvy roads and more interesting tracks. - Multiplayer/Competitive racing. - *Your* creative idea. If you encounter any problem or have any suggestions, please [open an issue](https://github.com/UpGado/ascii_racer/issues/new) or [send a PR](https://github.com/UpGado/ascii_racer/pulls). ================================================ FILE: asciiracer/__init__.py ================================================ ================================================ FILE: asciiracer/__main__.py ================================================ from . import game def main(): game.run() main() ================================================ FILE: asciiracer/ascii_factory.py ================================================ # All digits are 6 characters wide and 4 high nums = { 0: ['██████', '█ █', '█ █', '██████'], 1: [' █ ', ' █ ', ' █ ', ' ▉ '], 2: ['██████', ' █', '██████', '█▄▄▄▄▄'], 3: ['██████', ' █', '▀▀▀▀▀█', '▄▄▄▄▄█'], 4: ['█ █', '█ █', '█▄▄▄▄█', ' █'], 5: ['██████', '█ ', '▀▀▀▀▀█', '▄▄▄▄▄█'], 6: ['█▀▀▀▀█', '█ ', '█▀▀▀▀█', '█▄▄▄▄█'], 7: ['██████', ' ▗█▛', ' ▟█▛ ', '▄██▛ '], 8: ['█▀▀▀▀█', '█ █', '█▀▀▀▀█', '█▄▄▄▄█'], 9: ['█▀▀▀▀█', '█▄▄▄▄█', ' █', ' █'], } def num2str(num): assert(0 <= num and num <= 99) r_digit = num % 10 l_digit = (num - r_digit)/10 l_digit, r_digit = [nums[_] for _ in [l_digit, r_digit]] string = [] for l_line, r_line in zip(l_digit, r_digit): string.append(' '.join([l_line, r_line])) return string ================================================ FILE: asciiracer/config.py ================================================ from .misc import get_terminal_size # # Definitions: # - [X]_STICKY_TIME: amount of time a key press of action [X] # sticks in the game GAME_SIZE = get_terminal_size() FPS = 60 # Car movement SPEED_INCREMENT = 1 SPEED_DECREMENT = -1 BASE_SPEED = 5 MAX_SPEED = 99 SPEED_STICKY_TIME = 0.2 STEERING_STICKY_TIME = 0.5 STEERING_STEP = 0.06 # Environment HORIZON = 0.5 # how far from top? TRACK_SLOPE = 0.7 # x = x0 - slope*y DEBRIS_SPEED_MULTIPLIER = 1.0 MAX_NUM_DEBRIS = 20 # Cars MAX_NUM_CARS = 4 ================================================ FILE: asciiracer/environment.py ================================================ import random from collections import namedtuple from .config import HORIZON, TRACK_SLOPE, DEBRIS_SPEED_MULTIPLIER, \ MAX_NUM_DEBRIS from .misc import linear_interpolate Sprite = namedtuple('Sprite', ['attrs', 'current_coords']) def init(screen): global width, height, horizon_y, left_track, right_track height, width = screen.getmaxyx() horizon_y = int(HORIZON*height) left_track = (int(3*width/16), '▞', TRACK_SLOPE) right_track = (int(13*width/16), '▚', -TRACK_SLOPE) def in_range(y, x): return 0 <= y and y <= height - 1 and \ 0 <= x and x <= width - 1 def draw_background(screen, state): global width, height background = ' ' for y in range(height): for x in range(width-1): screen.addstr(y, x, background) def draw_statusbar(screen, state): status = '|'.join([f"Time: {state['time']:.2f} seconds", f"Score: {state['score']}"]) screen.addstr(0, 0, status) def draw_tracks(screen, state): global left_track, right_track, height, horizon_y for (x0, character, slope) in [left_track, right_track]: for y in range(horizon_y, height): x = x0+int(slope*(height-1-y)) if y <= horizon_y + 5: c = character character = '$' screen.addstr(y, x, character) if y <= horizon_y + 5: character = c def spawn_debris(state, x_ranges): debris_list = [[u'/\\', u'\\/'], ['*'], ['#']] return spawn_sprite(state, x_ranges, debris_list, DEBRIS_SPEED_MULTIPLIER) def spawn_money(state, x_ranges): def martini_glass(ch): return [r'╲___╱', f" ╲{ch}╱ ", r' ╿ ', r' ┴ '] def beer_can(): return [r'┌-/-┐', r'| |', r'|BUD|', r'| |', r'└---┘'] def dollar_bill(): return [r' ', r'┌---┐', r'|$1$|', r'└---┘', r' '] money_list = [(martini_glass('V'), 10), (dollar_bill(), 1), (martini_glass('G'), 5), (beer_can(), -20), (beer_can(), -20), (beer_can(), -20)] return spawn_sprite(state, x_ranges, money_list, 1) def spawn_sprite(state, x_ranges, sprites, speed_multiplier): sprite_design = random.choice(sprites) y0 = horizon_y x_range = random.choice(x_ranges) x0 = random.randint(*x_range) t0 = state['time'] new_sprite = Sprite((sprite_design, y0, x0, t0, speed_multiplier), None) return new_sprite def draw_debris(screen, state): top_track_offset = int(horizon_y*TRACK_SLOPE) - 2 x_ranges = [(0, left_track[0]+top_track_offset), (right_track[0]-top_track_offset, width-1)] draw_sprite(screen, state, 'debris', MAX_NUM_DEBRIS, x_ranges, spawn_debris) def draw_money(screen, state): top_track_offset = int(horizon_y*TRACK_SLOPE) + 2 x_ranges = [(left_track[0]+top_track_offset, right_track[0]-top_track_offset)] draw_sprite(screen, state, 'money', 1, x_ranges, spawn_money) def draw_sprite(screen, state, key, max_num, x_ranges, spawn_func): num_missing_sprites = max_num - len(state[key]) if num_missing_sprites > 0: for _ in range(num_missing_sprites): state[key].append(spawn_func(state, x_ranges)) draw_parallax(state[key], screen, state) def draw_parallax(sprites, screen, state): for s, sprite_tuple in enumerate(sprites): sprite, y0, x0, t0, speed_multiplier = sprite_tuple.attrs if type(sprite) is tuple: sprite_design = sprite[0] else: sprite_design = sprite speed = state['speed']*speed_multiplier step = parallax_slope(x0) y = y0 + int(speed*(state['time']-t0)) x = x0 + int((y0-y)*step) if in_range(y+len(sprite_design), x): for i, line in enumerate(sprite_design): screen.addstr(y+i, x, line) sprites[s] = Sprite((sprite, y0, x0, t0, speed_multiplier), ((y, y+i), (x, x+len(line)))) else: sprites.remove(sprite_tuple) def draw_horizon(screen, state): for x in range(width): screen.addstr(horizon_y, x, '-') def draw_car(screen, state): car = [r' ____________ ', r' / \ ', r' ▉▉| RrrrR |▉▉ ', r' ▉▉| CA R R |▉▉ ', r' ▉▉ \____________/ ▉▉ '] car_width = len(car[0]) offset = 2 # offset from track x0 = left_track[0]+car_width/2+offset x1 = right_track[0]-car_width/2-offset car_center_x = linear_interpolate(-1, x0, 1, x1, state['car_x']) start_x = int(car_center_x - car_width / 2) for offset, line in enumerate(reversed(car)): y = height-1-offset x = start_x + len(line) screen.addstr(y, start_x, line) y_coords = (height-1-offset, height-1) x_coords = (start_x, x) state['car'] = Sprite(None, (y_coords, x_coords)) def parallax_slope(x0): # using top end of tracks as reference top_track_offset = int(horizon_y*TRACK_SLOPE) x_range = (left_track[0]+top_track_offset, right_track[0]-top_track_offset) return linear_interpolate(x_range[0], TRACK_SLOPE, x_range[1], -TRACK_SLOPE, x0) ================================================ FILE: asciiracer/game.py ================================================ import curses from . import environment from .environment import draw_background, draw_tracks, draw_statusbar, \ draw_debris, draw_horizon, draw_car, draw_money from . import hud from .hud import draw_hud from .mechanics import update_state from .config import GAME_SIZE, FPS, BASE_SPEED from .misc import limit_fps SCENE = [draw_statusbar, draw_hud, draw_horizon, draw_tracks, draw_debris, draw_car, draw_money, draw_background] state = {'frames': 0, 'time': 0.0, # seconds 'speed': BASE_SPEED, # coord per frame 'car': None, 'car_x': 0, # range -1:1 'car_steer_tuple': None, 'car_speed_tuple': None, 'debris': [], # debris objects drawn in scene 'money': [], # money objects drawn in scene 'score': 0, 'pdb': False} # for testing @limit_fps(fps=FPS) def draw_scene(screen): for draw_element in reversed(SCENE): draw_element(screen, state) screen.refresh() def main(screen): screen.resize(*GAME_SIZE) screen.nodelay(True) environment.init(screen) hud.init(screen) while True: draw_scene(screen) key = screen.getch() if key == ord('q'): break elif key == ord('p'): state['pdb'] = True else: update_state(key, state) state['frames'] += 1 state['time'] += 1/FPS screen.clear() screen.getkey() def run(): curses.wrapper(main) ================================================ FILE: asciiracer/hud.py ================================================ from .ascii_factory import num2str def init(screen): global width, height height, width = screen.getmaxyx() def draw_speedmeter(screen, state): margin_y, margin_x = 4, 4 hud = ['▛▀▀▀▀▀▀▀▀▀▀▀▀▀▜', '▍ ▐', '▍ ▐', '▍ ▐', '▍ ▐', '▙▃▃▃▃▃▃▃▃▃▃▃▃▃▟', '▍ MPH ▐', '▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀'] hud_width = len(hud[0]) speed = num2str(state['speed']) for l, (hud_line, speed_line) in enumerate(zip(hud[1:-1], speed)): hud[l+1] = hud_line[0] + speed_line + hud_line[-1] x0 = width - margin_x - hud_width y0 = margin_y for y, line in enumerate(hud): screen.addstr(y0+y, x0, line) def draw_hud(screen, state): draw_speedmeter(screen, state) ================================================ FILE: asciiracer/mechanics.py ================================================ from .config import SPEED_INCREMENT, SPEED_DECREMENT, BASE_SPEED, \ STEERING_STEP, MAX_SPEED, \ STEERING_STICKY_TIME, SPEED_STICKY_TIME from .misc import make_in_range, rectangle_overlap def update_state(key, state): steer_tuple = state['car_steer_tuple'] speed_tuple = state['car_speed_tuple'] # respond to keys if key in {ord('w'), ord('s')}: direction = 1 if key == ord('w') else -1 if speed_tuple is None or speed_tuple[1] != direction: state['car_speed_tuple'] = (state['time'], direction) elif key in {ord('d'), ord('a')}: direction = 1 if key == ord('d') else -1 if steer_tuple is None or steer_tuple[1] != direction: state['car_steer_tuple'] = (state['time'], direction) elif key == -1: # no key pressed pass if steer_tuple is not None: update_steering(state, steer_tuple) if speed_tuple is not None: update_speed(state, speed_tuple) collect_money(state) def collect_money(state): c_ys, c_xs = state['car'].current_coords for money_object in state['money']: ys, xs = money_object.current_coords if rectangle_overlap(*c_ys, *c_xs, *ys, *xs): (_, score), *args = money_object.attrs state['score'] += score state['money'].remove(money_object) def update_steering(state, steer_tuple): t0, direction = steer_tuple elapsed_time = state['time'] - t0 if elapsed_time > STEERING_STICKY_TIME: state['car_steer_tuple'] = None else: new_car_x = state['car_x'] + direction*STEERING_STEP state['car_x'] = make_in_range(new_car_x, -1, 1) def update_speed(state, speed_tuple): t0, direction = speed_tuple if state['time'] - t0 > SPEED_STICKY_TIME: state['car_speed_tuple'] = None else: change = SPEED_INCREMENT if direction == 1 \ else SPEED_DECREMENT new_car_speed = state['speed'] + change state['speed'] = make_in_range(new_car_speed, BASE_SPEED, MAX_SPEED) ================================================ FILE: asciiracer/misc.py ================================================ import time from time import sleep import os import sys def limit_fps(fps): delay = 1/fps def run_fps_capped(func): def run(*args, **kwargs): start_time = time.time() func(*args, **kwargs) elapsed_time = time.time() - start_time sleep_time = delay-elapsed_time if sleep_time >= 0: sleep(sleep_time) return run return run_fps_capped def linear_interpolate(x1, y1, x2, y2, x3): y3 = y1 + (x3-x1)*(y2-y1)/(x2-x1) return y3 def make_in_range(x, x_min, x_max): x = min(x, x_max) x = max(x_min, x) return x def rectangle_overlap(r1_y1, r1_y2, r1_x1, r1_x2, r2_y1, r2_y2, r2_x1, r2_x2): if r2_x2 < r1_x1 or r2_x1 > r1_x2: return False elif r2_y2 < r1_y1 or r2_y1 > r1_y2: return False else: return True def get_terminal_size(): if sys.platform == 'win32': return _get_terminal_size_windows() else: return _get_terminal_size_unix() def _get_terminal_size_windows(): # http://code.activestate.com/recipes/440694-determine-size-of-console-window-on-windows/ from ctypes import windll, create_string_buffer # stdin handle is -10 # stdout handle is -11 # stderr handle is -12 h = windll.kernel32.GetStdHandle(-12) csbi = create_string_buffer(22) res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi) if res: import struct (_, _, _, _, _, left, top, right, bottom, *_) = struct.unpack("hhhhHhhhhhh", csbi.raw) sizex = right - left + 1 sizey = bottom - top + 1 else: sizex, sizey = 80, 25 # can't determine actual size return (sizey, sizex) def _get_terminal_size_unix(): return tuple(int(i) for i in os.popen('stty size', 'r').read().split()) ================================================ FILE: asciiracer/tests/test_general.py ================================================ def func(x): return x + 1 def test_answer(): assert func(3) == 4 ================================================ FILE: requirements.txt ================================================ ================================================ FILE: setup.py ================================================ import setuptools with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( name='asciiracer', version='1.0.3', python_requires='>=3.6.0', author='Ahmed Gado', author_email='ahmedehabg@gmail.com', description='A racing game that runs in terminal', long_description=long_description, long_description_content_type="text/markdown", url='https://github.com/UpGado/ascii_racer', packages=setuptools.find_packages(), classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], entry_points={ 'console_scripts': [ 'asciiracer = asciiracer.__main__:main' ] }, install_requires=[ 'windows-curses >= 2.0;platform_system=="Windows"' ] )