Files
sqlalchemy/examples/space_invaders/space_invaders.py
T
Mike Bayer 1e278de4cc Post black reformatting
Applied on top of a pure run of black -l 79 in
I7eda77fed3d8e73df84b3651fd6cfcfe858d4dc9,  this set of changes
resolves all remaining flake8 conditions for those codes
we have enabled in setup.cfg.

Included are resolutions for all remaining flake8 issues
including shadowed builtins, long lines, import order, unused
imports, duplicate imports, and docstring issues.

Change-Id: I4f72d3ba1380dd601610ff80b8fb06a2aff8b0fe
2019-01-06 18:23:11 -05:00

794 lines
19 KiB
Python

import curses
import logging
import random
import re
import sys
import textwrap
import time
from sqlalchemy import Column
from sqlalchemy import create_engine
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy import Integer
from sqlalchemy import String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.hybrid import hybrid_method
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import joinedload
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Session
_PY3 = sys.version_info > (3, 0)
if _PY3:
xrange = range
logging.basicConfig(
filename="space_invaders.log",
format="%(asctime)s,%(msecs)03d %(levelname)-5.5s %(message)s",
)
logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO)
Base = declarative_base()
WINDOW_LEFT = 10
WINDOW_TOP = 2
WINDOW_WIDTH = 70
WINDOW_HEIGHT = 34
VERT_PADDING = 2
HORIZ_PADDING = 5
ENEMY_VERT_SPACING = 4
MAX_X = WINDOW_WIDTH - HORIZ_PADDING
MAX_Y = WINDOW_HEIGHT - VERT_PADDING
LEFT_KEY = ord("j")
RIGHT_KEY = ord("l")
FIRE_KEY = ord(" ")
PAUSE_KEY = ord("p")
COLOR_MAP = {
"K": curses.COLOR_BLACK,
"B": curses.COLOR_BLUE,
"C": curses.COLOR_CYAN,
"G": curses.COLOR_GREEN,
"M": curses.COLOR_MAGENTA,
"R": curses.COLOR_RED,
"W": curses.COLOR_WHITE,
"Y": curses.COLOR_YELLOW,
}
class Glyph(Base):
"""Describe a "glyph", a graphical element
to be painted on the screen.
"""
__tablename__ = "glyph"
id = Column(Integer, primary_key=True)
name = Column(String)
type = Column(String)
width = Column(Integer)
height = Column(Integer)
data = Column(String)
alt_data = Column(String)
__mapper_args__ = {"polymorphic_on": type}
def __init__(self, name, img, alt=None):
self.name = name
self.data, self.width, self.height = self._encode_glyph(img)
if alt is not None:
self.alt_data, alt_w, alt_h = self._encode_glyph(alt)
def _encode_glyph(self, img):
"""Receive a textual description of the glyph and
encode into a format understood by
GlyphCoordinate.render().
"""
img = re.sub(r"^\n", "", textwrap.dedent(img))
color = "W"
lines = [line.rstrip() for line in img.split("\n")]
data = []
for line in lines:
render_line = []
line = list(line)
while line:
char = line.pop(0)
if char == "#":
color = line.pop(0)
continue
render_line.append((color, char))
data.append(render_line)
width = max([len(rl) for rl in data])
data = "".join(
"".join("%s%s" % (color, char) for color, char in render_line)
+ ("W " * (width - len(render_line)))
for render_line in data
)
return data, width, len(lines)
def glyph_for_state(self, coord, state):
"""Return the appropriate data representation
for this Glyph, based on the current coordinates
and state.
Subclasses may override this to provide animations.
"""
return self.data
class GlyphCoordinate(Base):
"""Describe a glyph rendered at a certain x, y coordinate.
The GlyphCoordinate may also include optional values
such as the tick at time of render, a label, and a
score value.
"""
__tablename__ = "glyph_coordinate"
id = Column(Integer, primary_key=True)
glyph_id = Column(Integer, ForeignKey("glyph.id"))
x = Column(Integer)
y = Column(Integer)
tick = Column(Integer)
label = Column(String)
score = Column(Integer)
glyph = relationship(Glyph, innerjoin=True)
def __init__(
self, session, glyph_name, x, y, tick=None, label=None, score=None
):
self.glyph = session.query(Glyph).filter_by(name=glyph_name).one()
self.x = x
self.y = y
self.tick = tick
self.label = label
self.score = score
session.add(self)
def render(self, window, state):
"""Render the Glyph at this position."""
col = 0
row = 0
glyph = self.glyph
data = glyph.glyph_for_state(self, state)
for color, char in [
(data[i], data[i + 1]) for i in xrange(0, len(data), 2)
]:
x = self.x + col
y = self.y + row
if 0 <= x <= MAX_X and 0 <= y <= MAX_Y:
window.addstr(
y + VERT_PADDING,
x + HORIZ_PADDING,
char,
_COLOR_PAIRS[color],
)
col += 1
if col == glyph.width:
col = 0
row += 1
if self.label:
self._render_label(window, False)
def _render_label(self, window, blank):
label = self.label if not blank else " " * len(self.label)
if self.x + self.width + len(self.label) < MAX_X:
window.addstr(self.y, self.x + self.width, label)
else:
window.addstr(self.y, self.x - len(self.label), label)
def blank(self, window):
"""Render a blank box for this glyph's position and size."""
glyph = self.glyph
x = min(max(self.x, 0), MAX_X)
width = min(glyph.width, MAX_X - x) or 1
for y_a in xrange(self.y, self.y + glyph.height):
y = y_a
window.addstr(y + VERT_PADDING, x + HORIZ_PADDING, " " * width)
if self.label:
self._render_label(window, True)
@hybrid_property
def width(self):
return self.glyph.width
@width.expression
def width(cls):
return Glyph.width
@hybrid_property
def height(self):
return self.glyph.height
@height.expression
def height(cls):
return Glyph.height
@hybrid_property
def bottom_bound(self):
return self.y + self.height >= MAX_Y
@hybrid_property
def top_bound(self):
return self.y <= 0
@hybrid_property
def left_bound(self):
return self.x <= 0
@hybrid_property
def right_bound(self):
return self.x + self.width >= MAX_X
@hybrid_property
def right_edge_bound(self):
return self.x > MAX_X
@hybrid_method
def intersects(self, other):
"""Return True if this GlyphCoordinate intersects with
the given GlyphCoordinate."""
return ~(
(self.x + self.width < other.x) | (self.x > other.x + other.width)
) & ~(
(self.y + self.height < other.y)
| (self.y > other.y + other.height)
)
class EnemyGlyph(Glyph):
"""Describe an enemy."""
__mapper_args__ = {"polymorphic_identity": "enemy"}
class ArmyGlyph(EnemyGlyph):
"""Describe an enemy that's part of the "army". """
__mapper_args__ = {"polymorphic_identity": "army"}
def glyph_for_state(self, coord, state):
if state["flip"]:
return self.alt_data
else:
return self.data
class SaucerGlyph(EnemyGlyph):
"""Describe the enemy saucer flying overhead."""
__mapper_args__ = {"polymorphic_identity": "saucer"}
def glyph_for_state(self, coord, state):
if state["flip"] == 0:
return self.alt_data
else:
return self.data
class MessageGlyph(Glyph):
"""Describe a glyph for displaying a message."""
__mapper_args__ = {"polymorphic_identity": "message"}
class PlayerGlyph(Glyph):
"""Describe a glyph representing the player."""
__mapper_args__ = {"polymorphic_identity": "player"}
class MissileGlyph(Glyph):
"""Describe a glyph representing a missile."""
__mapper_args__ = {"polymorphic_identity": "missile"}
class SplatGlyph(Glyph):
"""Describe a glyph representing a "splat"."""
__mapper_args__ = {"polymorphic_identity": "splat"}
def glyph_for_state(self, coord, state):
age = state["tick"] - coord.tick
if age > 5:
return self.alt_data
else:
return self.data
def init_glyph(session):
"""Create the glyphs used during play."""
enemy1 = ArmyGlyph(
"enemy1",
"""
#W-#B^#R-#B^#W-
#G| |
""",
"""
#W>#B^#R-#B^#W<
#G^ ^
""",
)
enemy2 = ArmyGlyph(
"enemy2",
"""
#W***
#R<#C~~~#R>
""",
"""
#W@@@
#R<#C---#R>
""",
)
enemy3 = ArmyGlyph(
"enemy3",
"""
#Y((--))
#M-~-~-~
""",
"""
#Y[[--]]
#M~-~-~-
""",
)
saucer = SaucerGlyph(
"saucer",
"""#R~#Y^#R~#G<<((=#WOO#G=))>>""",
"""#Y^#R~#Y^#G<<((=#WOO#G=))>>""",
)
splat1 = SplatGlyph(
"splat1",
"""
#WVVVVV
#W> #R*** #W<
#W^^^^^
""",
"""
#M|
#M- #Y+++ #M-
#M|
""",
)
ship = PlayerGlyph(
"ship",
"""
#Y^
#G=====
""",
)
missile = MissileGlyph(
"missile",
"""
|
""",
)
start = MessageGlyph(
"start_message",
"J = move left; L = move right; SPACE = fire\n"
" #GPress any key to start",
)
lose = MessageGlyph("lose_message", "#YY O U L O S E ! ! !")
win = MessageGlyph("win_message", "#RL E V E L C L E A R E D ! ! !")
paused = MessageGlyph(
"pause_message", "#WP A U S E D\n#GPress P to continue"
)
session.add_all(
[
enemy1,
enemy2,
enemy3,
ship,
saucer,
missile,
start,
lose,
win,
paused,
splat1,
]
)
def setup_curses():
"""Setup terminal/curses state."""
window = curses.initscr()
curses.noecho()
window = curses.newwin(
WINDOW_HEIGHT + (VERT_PADDING * 2),
WINDOW_WIDTH + (HORIZ_PADDING * 2),
WINDOW_TOP - VERT_PADDING,
WINDOW_LEFT - HORIZ_PADDING,
)
curses.start_color()
global _COLOR_PAIRS
_COLOR_PAIRS = {}
for i, (k, v) in enumerate(COLOR_MAP.items(), 1):
curses.init_pair(i, v, curses.COLOR_BLACK)
_COLOR_PAIRS[k] = curses.color_pair(i)
return window
def init_positions(session):
"""Establish a new field of play.
This generates GlyphCoordinate objects
and persists them to the database.
"""
# delete all existing coordinates
session.query(GlyphCoordinate).delete()
session.add(
GlyphCoordinate(
session, "ship", WINDOW_WIDTH // 2 - 2, WINDOW_HEIGHT - 4
)
)
arrangement = (
("enemy3", 50),
("enemy2", 25),
("enemy1", 10),
("enemy2", 25),
("enemy1", 10),
)
for (ship_vert, (etype, score)) in zip(
xrange(5, 30, ENEMY_VERT_SPACING), arrangement
):
for ship_horiz in xrange(0, 50, 10):
session.add(
GlyphCoordinate(
session, etype, ship_horiz, ship_vert, score=score
)
)
def draw(session, window, state):
"""Load all current GlyphCoordinate objects from the
database and render.
"""
for gcoord in session.query(GlyphCoordinate).options(joinedload("glyph")):
gcoord.render(window, state)
window.addstr(1, WINDOW_WIDTH - 5, "Score: %.4d" % state["score"])
window.move(0, 0)
window.refresh()
def check_win(session, state):
"""Return the number of army glyphs remaining -
the player wins if this is zero."""
return (
session.query(func.count(GlyphCoordinate.id))
.join(GlyphCoordinate.glyph.of_type(ArmyGlyph))
.scalar()
)
def check_lose(session, state):
"""Return the number of army glyphs either colliding
with the player or hitting the bottom of the screen.
The player loses if this is non-zero."""
player = state["player"]
return (
session.query(GlyphCoordinate)
.join(GlyphCoordinate.glyph.of_type(ArmyGlyph))
.filter(
GlyphCoordinate.intersects(player) | GlyphCoordinate.bottom_bound
)
.count()
)
def render_message(session, window, msg, x, y):
"""Render a message glyph.
Clears the area beneath the message first
and assumes the display will be paused
afterwards.
"""
# create message box
msg = GlyphCoordinate(session, msg, x, y)
# clear existing glyphs which intersect
for gly in (
session.query(GlyphCoordinate)
.join(GlyphCoordinate.glyph)
.filter(GlyphCoordinate.intersects(msg))
):
gly.blank(window)
# render
msg.render(window, {})
window.refresh()
return msg
def win(session, window, state):
"""Handle the win case."""
render_message(session, window, "win_message", 15, 15)
time.sleep(2)
start(session, window, state, True)
def lose(session, window, state):
"""Handle the lose case."""
render_message(session, window, "lose_message", 15, 15)
time.sleep(2)
start(session, window, state)
def pause(session, window, state):
"""Pause the game."""
msg = render_message(session, window, "pause_message", 15, 15)
prompt(window)
msg.blank(window)
session.delete(msg)
def prompt(window):
"""Display a prompt, quashing any keystrokes
which might have remained."""
window.move(0, 0)
window.nodelay(1)
window.getch()
window.nodelay(0)
window.getch()
window.nodelay(1)
def move_army(session, window, state):
"""Update the army position based on the current
size of the field."""
speed = 30 // 25 * state["num_enemies"]
flip = (state["tick"] % speed) == 0
if not flip:
return
else:
state["flip"] = not state["flip"]
x_slide = 1
# get the lower/upper boundaries of the army
# along the X axis.
min_x, max_x = (
session.query(
func.min(GlyphCoordinate.x),
func.max(GlyphCoordinate.x + GlyphCoordinate.width),
)
.join(GlyphCoordinate.glyph.of_type(ArmyGlyph))
.first()
)
if min_x is None or max_x is None:
# no enemies
return
direction = state["army_direction"]
move_y = False
if direction == 0 and max_x + x_slide >= MAX_X:
direction = state["army_direction"] = 1
move_y = True
elif direction == 1 and min_x - x_slide <= 0:
direction = state["army_direction"] = 0
move_y = True
for enemy_g in session.query(GlyphCoordinate).join(
GlyphCoordinate.glyph.of_type(ArmyGlyph)
):
enemy_g.blank(window)
if move_y:
enemy_g.y += 1
elif direction == 0:
enemy_g.x += x_slide
elif direction == 1:
enemy_g.x -= x_slide
def move_player(session, window, state):
"""Receive player input and adjust state."""
ch = window.getch()
if ch not in (LEFT_KEY, RIGHT_KEY, FIRE_KEY, PAUSE_KEY):
return
elif ch == PAUSE_KEY:
pause(session, window, state)
return
player = state["player"]
if ch == RIGHT_KEY and not player.right_bound:
player.blank(window)
player.x += 1
elif ch == LEFT_KEY and not player.left_bound:
player.blank(window)
player.x -= 1
elif ch == FIRE_KEY and state["missile"] is None:
state["missile"] = GlyphCoordinate(
session, "missile", player.x + 3, player.y - 1
)
def move_missile(session, window, state):
"""Update the status of the current missile, if any."""
if state["missile"] is None or state["tick"] % 2 != 0:
return
missile = state["missile"]
# locate enemy glyphs which intersect with the
# missile's current position; i.e. a hit
glyph = (
session.query(GlyphCoordinate)
.join(GlyphCoordinate.glyph.of_type(EnemyGlyph))
.filter(GlyphCoordinate.intersects(missile))
.first()
)
missile.blank(window)
if glyph or missile.top_bound:
# missle is done
session.delete(missile)
state["missile"] = None
if glyph:
# score!
score(session, window, state, glyph)
else:
# move missle up one character.
missile.y -= 1
def move_saucer(session, window, state):
"""Update the status of the saucer."""
saucer_interval = 500
saucer_speed_interval = 4
if state["saucer"] is None and state["tick"] % saucer_interval != 0:
return
if state["saucer"] is None:
state["saucer"] = saucer = GlyphCoordinate(
session, "saucer", -6, 1, score=random.randrange(100, 600, 100)
)
elif state["tick"] % saucer_speed_interval == 0:
saucer = state["saucer"]
saucer.blank(window)
saucer.x += 1
if saucer.right_edge_bound:
session.delete(saucer)
state["saucer"] = None
def update_splat(session, window, state):
"""Render splat animations."""
for splat in session.query(GlyphCoordinate).join(
GlyphCoordinate.glyph.of_type(SplatGlyph)
):
age = state["tick"] - splat.tick
if age > 10:
splat.blank(window)
session.delete(splat)
else:
splat.render(window, state)
def score(session, window, state, glyph):
"""Process a glyph intersecting with a missile."""
glyph.blank(window)
session.delete(glyph)
if state["saucer"] is glyph:
state["saucer"] = None
state["score"] += glyph.score
# render a splat !
GlyphCoordinate(
session,
"splat1",
glyph.x,
glyph.y,
tick=state["tick"],
label=str(glyph.score),
)
def update_state(session, window, state):
"""Update all state for each game tick."""
num_enemies = state["num_enemies"] = check_win(session, state)
if num_enemies == 0:
win(session, window, state)
elif check_lose(session, state):
lose(session, window, state)
else:
# update the tick counter.
state["tick"] += 1
move_player(session, window, state)
move_missile(session, window, state)
move_army(session, window, state)
move_saucer(session, window, state)
update_splat(session, window, state)
def start(session, window, state, continue_=False):
"""Start a new field of play."""
render_message(session, window, "start_message", 15, 20)
prompt(window)
init_positions(session)
player = (
session.query(GlyphCoordinate)
.join(GlyphCoordinate.glyph.of_type(PlayerGlyph))
.one()
)
state.update(
{
"field_pos": 0,
"alt": False,
"tick": 0,
"missile": None,
"saucer": None,
"player": player,
"army_direction": 0,
"flip": False,
}
)
if not continue_:
state["score"] = 0
window.clear()
window.box()
draw(session, window, state)
def main():
"""Initialize the database and establish the game loop."""
e = create_engine("sqlite://")
Base.metadata.create_all(e)
session = Session(e)
init_glyph(session)
session.commit()
window = setup_curses()
state = {}
start(session, window, state)
while True:
update_state(session, window, state)
draw(session, window, state)
time.sleep(0.01)
if __name__ == "__main__":
main()