mirror of
https://we.phorge.it/source/arcanist.git
synced 2024-11-08 16:02:39 +01:00
e9241dcb90
Summary: Fixes T8693. Ref T13098. On a 30x15 terminal, the we can only fit "Score: X/12 * Deaths: Y" on the top line if both `X` and `Y` are less than 10, so they can render with a single character. As soon as the player breaks more than 9 blocks or dies more than 9 times, we need an extra character to render the score. This causes an off-screen write to curses and crashes. Raise the minimum requirement to 32 columns so we can render "12/12" and up to "99" deaths. Then, change the display logic to show "99" if you die more than 99 times. (At this resolution we always generate a board with 12 blocks, even if the terminal is very very tall, so we don't need to deal with a case where the "Score" might read "101/200".) Test Plan: - Beat the game on a 32x15 terminal. - Changed logic to award me 1000 deaths per actual death. - Died on a 32x15 terminal. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13098, T8693 Differential Revision: https://secure.phabricator.com/D20085
228 lines
6.7 KiB
Python
Executable file
228 lines
6.7 KiB
Python
Executable file
#!/usr/bin/env python2
|
|
import sys
|
|
import time
|
|
import select
|
|
import curses
|
|
from curses import wrapper
|
|
|
|
entities = []
|
|
grid = []
|
|
|
|
class Wall:
|
|
def collide(self, ball):
|
|
return False
|
|
|
|
class Block:
|
|
killed = 0
|
|
total = 0
|
|
|
|
def __init__(self, x, y, w, h, c):
|
|
self.x = x
|
|
self.y = y
|
|
self.w = w
|
|
self.h = h
|
|
self.fmt = curses.A_BOLD | curses.color_pair(c)
|
|
self.alive = True
|
|
for i in range(self.x, self.x + self.w):
|
|
for j in range(self.y, self.y + self.h):
|
|
grid[j + 1][i + 1] = self
|
|
Block.total += 1
|
|
|
|
def collide(self, ball):
|
|
self.alive = False
|
|
for i in range(self.x, self.x + self.w):
|
|
for j in range(self.y, self.y + self.h):
|
|
grid[j + 1][i + 1] = None
|
|
Block.killed += 1
|
|
return False
|
|
|
|
def tick(self, win):
|
|
if self.alive:
|
|
for i in range(self.x, self.x + self.w):
|
|
for j in range(self.y, self.y + self.h):
|
|
win.addch(j, i, curses.ACS_BLOCK, self.fmt)
|
|
return self.alive
|
|
|
|
class Ball:
|
|
alive = False
|
|
killed = 0
|
|
|
|
def __init__(self, x, y, vx, vy):
|
|
self.x = x
|
|
self.y = y
|
|
self.vx = vx
|
|
self.vy = vy
|
|
Ball.alive = True
|
|
|
|
def collide(self, ball):
|
|
return True
|
|
|
|
def encounter(self, dx, dy):
|
|
ent = grid[self.y + dy + 1][self.x + dx + 1]
|
|
if ent and not ent.collide(self):
|
|
self.vx -= 2 * dx
|
|
self.vy -= 2 * dy
|
|
return ent
|
|
|
|
def tick(self, win):
|
|
while self.y < ship.y:
|
|
if self.encounter((self.vx + self.vy) / 2, (self.vy - self.vx) / 2):
|
|
continue
|
|
if self.encounter((self.vx - self.vy) / 2, (self.vy + self.vx) / 2):
|
|
continue
|
|
if self.encounter(self.vx, self.vy):
|
|
continue
|
|
break
|
|
self.x += self.vx
|
|
self.y += self.vy
|
|
try:
|
|
win.addch(self.y, self.x, 'O')
|
|
except curses.error:
|
|
Ball.alive = False
|
|
Ball.killed += 1
|
|
return Ball.alive
|
|
|
|
class Ship:
|
|
def __init__(self, x, y):
|
|
self.x = x
|
|
self.y = y
|
|
self.hw = 10
|
|
self.v = 4
|
|
self.last = 1
|
|
self.update()
|
|
|
|
def update(self):
|
|
grid[self.y + 1] = (
|
|
[ None ] * (self.x - self.hw + 1) +
|
|
[ self ] * (self.hw * 2 + 1) +
|
|
[ None ] * (width - self.x - self.hw)
|
|
)
|
|
|
|
def collide(self, ball):
|
|
ball.vy = -1
|
|
if ball.x > self.x + self.hw / 2:
|
|
ball.vx = 1
|
|
elif ball.x < self.x - self.hw / 2:
|
|
ball.vx = -1
|
|
return True
|
|
|
|
def shift(self, i):
|
|
self.last = i
|
|
self.x += self.v * i
|
|
if self.x - self.hw < 0:
|
|
self.x = self.hw
|
|
elif self.x + self.hw >= width:
|
|
self.x = width - self.hw - 1
|
|
self.update()
|
|
|
|
def spawn(self):
|
|
if not Ball.alive:
|
|
entities.append(Ball(self.x, self.y - 1, self.last, -1))
|
|
|
|
def tick(self, win):
|
|
if not Ball.alive:
|
|
win.addch(self.y - 1, self.x, 'O')
|
|
win.addch(self.y, self.x - self.hw, curses.ACS_LTEE)
|
|
for i in range(-self.hw + 1, self.hw):
|
|
win.addch(curses.ACS_HLINE)
|
|
win.addch(curses.ACS_RTEE)
|
|
return True
|
|
|
|
class PowerOverwhelmingException(Exception):
|
|
pass
|
|
|
|
def main(stdscr):
|
|
global height, width, ship
|
|
|
|
for i in range(1, 8):
|
|
curses.init_pair(i, i, 0)
|
|
curses.curs_set(0)
|
|
curses.raw()
|
|
|
|
height, width = stdscr.getmaxyx()
|
|
|
|
if height < 15 or width < 32:
|
|
raise PowerOverwhelmingException(
|
|
'Your computer is not powerful enough to run "arc anoid". '
|
|
'It must support at least 32 columns and 15 rows of next-gen '
|
|
'full-color 3D graphics.')
|
|
|
|
status = curses.newwin(1, width, 0, 0)
|
|
height -= 1
|
|
game = curses.newwin(height, width, 1, 0)
|
|
game.nodelay(1)
|
|
game.keypad(1)
|
|
|
|
grid[:] = [ [ None for x in range(width + 2) ] for y in range(height + 2) ]
|
|
wall = Wall()
|
|
for x in range(width + 2):
|
|
grid[0][x] = wall
|
|
for y in range(height + 2):
|
|
grid[y][0] = grid[y][-1] = wall
|
|
ship = Ship(width / 2, height - 5)
|
|
entities.append(ship)
|
|
|
|
colors = [ 1, 3, 2, 6, 4, 5 ]
|
|
h = height / 10
|
|
for x in range(1, width / 7 - 1):
|
|
for y in range(1, 7):
|
|
entities.append(Block(x * 7,
|
|
y * h + x / 2 % 2,
|
|
7,
|
|
h,
|
|
colors[y - 1]))
|
|
|
|
while True:
|
|
while select.select([ sys.stdin ], [], [], 0)[0]:
|
|
key = game.getch()
|
|
if key == curses.KEY_LEFT or key == ord('a') or key == ord('A'):
|
|
ship.shift(-1)
|
|
elif key == curses.KEY_RIGHT or key == ord('d') or key == ord('D'):
|
|
ship.shift(1)
|
|
elif key == ord(' '):
|
|
ship.spawn()
|
|
elif key == 0x1b or key == 3 or key == ord('q') or key == ord('Q'):
|
|
return
|
|
|
|
game.resize(height, width)
|
|
game.erase()
|
|
entities[:] = [ ent for ent in entities if ent.tick(game) ]
|
|
|
|
status.hline(0, 0, curses.ACS_HLINE, width)
|
|
status.addch(0, 2, curses.ACS_RTEE)
|
|
status.addstr(' SCORE: ', curses.A_BOLD | curses.color_pair(4))
|
|
status.addstr('%s/%s ' % (Block.killed, Block.total), curses.A_BOLD)
|
|
status.addch(curses.ACS_VLINE)
|
|
status.addstr(' DEATHS: ', curses.A_BOLD | curses.color_pair(4))
|
|
|
|
# See T8693. At the minimum display size, we only have room to render
|
|
# two characters for the death count, so just display "99" if the
|
|
# player has more than 99 deaths.
|
|
display_deaths = Ball.killed
|
|
if (display_deaths > 99):
|
|
display_deaths = 99
|
|
|
|
status.addstr('%s ' % display_deaths, curses.A_BOLD)
|
|
status.addch(curses.ACS_LTEE)
|
|
|
|
if Block.killed == Block.total:
|
|
message = ' A WINNER IS YOU!! '
|
|
i = int(time.time() / 0.8)
|
|
for x in range(width):
|
|
for y in range(6):
|
|
game.addch(height / 2 + y - 3 + (x / 8 + i) % 2, x,
|
|
curses.ACS_BLOCK,
|
|
curses.A_BOLD | curses.color_pair(colors[y]))
|
|
game.addstr(height / 2, (width - len(message)) / 2, message,
|
|
curses.A_BOLD | curses.color_pair(7))
|
|
|
|
game.refresh()
|
|
status.refresh()
|
|
time.sleep(0.05)
|
|
|
|
try:
|
|
curses.wrapper(main)
|
|
print ('You destroyed %s blocks out of %s with %s deaths.' %
|
|
(Block.killed, Block.total, Ball.killed))
|
|
except PowerOverwhelmingException as e:
|
|
print (e)
|