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
================================================
```
_ _
/\ (_(_)
/ \ ___ ___ _ _ _ __ __ _ ___ ___ _ __
/ /\ \ / __|/ __| | | | '__/ _` |/ __/ _ | '__|
/ ____ \\__ | (__| | | | | | (_| | (_| __| |
/_/ \_|___/\___|_|_| |_| \__,_|\___\___|_|
```

[](https://travis-ci.com/UpGado/ascii_racer)

[](https://pepy.tech/project/asciiracer)
[](https://opensource.org/licenses/MIT)
An endless racing game that runs in the terminal. 100% Python.

## 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"'
]
)