1#!/usr/local/bin/python3.8
2import sys
3import time
4import select
5import curses
6from curses import wrapper
7
8entities = []
9grid = []
10
11class Wall:
12    def collide(self, ball):
13        return False
14
15class Block:
16    killed = 0
17    total = 0
18
19    def __init__(self, x, y, w, h, c):
20        self.x = int(round(x))
21        self.y = int(round(y))
22        self.w = int(round(w))
23        self.h = int(round(h))
24        self.fmt = curses.A_BOLD | curses.color_pair(c)
25        self.alive = True
26        for i in range(self.x, self.x + self.w):
27            for j in range(self.y, self.y + self.h):
28                grid[j + 1][i + 1] = self
29        Block.total += 1
30
31    def collide(self, ball):
32        self.alive = False
33        for i in range(self.x, self.x + self.w):
34            for j in range(self.y, self.y + self.h):
35                grid[j + 1][i + 1] = None
36        Block.killed += 1
37        return False
38
39    def tick(self, win):
40        if self.alive:
41            for i in range(self.x, self.x + self.w):
42                for j in range(self.y, self.y + self.h):
43                    win.addch(j, i, curses.ACS_BLOCK, self.fmt)
44        return self.alive
45
46class Ball:
47    alive = False
48    killed = 0
49
50    def __init__(self, x, y, vx, vy):
51        self.x = int(round(x))
52        self.y = int(round(y))
53        self.vx = vx
54        self.vy = vy
55        Ball.alive = True
56
57    def collide(self, ball):
58        return True
59
60    def encounter(self, dx, dy):
61        dx = int(round(dx))
62        dy = int(round(dy))
63        ent = grid[self.y + dy + 1][self.x + dx + 1]
64        if ent and not ent.collide(self):
65            self.vx -= 2 * dx
66            self.vy -= 2 * dy
67        return ent
68
69    def tick(self, win):
70        while self.y < ship.y:
71            if self.encounter((self.vx + self.vy) / 2, (self.vy - self.vx) / 2):
72                continue
73            if self.encounter((self.vx - self.vy) / 2, (self.vy + self.vx) / 2):
74                continue
75            if self.encounter(self.vx, self.vy):
76                continue
77            break
78        self.x += self.vx
79        self.y += self.vy
80        try:
81            win.addch(self.y, self.x, 'O')
82        except curses.error:
83            Ball.alive = False
84            Ball.killed += 1
85        return Ball.alive
86
87class Ship:
88    def __init__(self, x, y):
89        self.x = int(round(x))
90        self.y = int(round(y))
91        self.hw = 10
92        self.v = 4
93        self.last = 1
94        self.update()
95
96    def update(self):
97        grid[self.y + 1] = (
98            [ None ] * (self.x - self.hw + 1) +
99            [ self ] * (self.hw * 2 + 1) +
100            [ None ] * (width - self.x - self.hw)
101        )
102
103    def collide(self, ball):
104        ball.vy = -1
105        if ball.x > self.x + self.hw / 2:
106            ball.vx = 1
107        elif ball.x < self.x - self.hw / 2:
108            ball.vx = -1
109        return True
110
111    def shift(self, i):
112        self.last = i
113        self.x += self.v * i
114        if self.x - self.hw < 0:
115            self.x = self.hw
116        elif self.x + self.hw >= width:
117            self.x = width - self.hw - 1
118        self.update()
119
120    def spawn(self):
121        if not Ball.alive:
122            entities.append(Ball(self.x, self.y - 1, self.last, -1))
123
124    def tick(self, win):
125        if not Ball.alive:
126            win.addch(self.y - 1, self.x, 'O')
127        win.addch(self.y, self.x - self.hw, curses.ACS_LTEE)
128        for i in range(-self.hw + 1, self.hw):
129            win.addch(curses.ACS_HLINE)
130        win.addch(curses.ACS_RTEE)
131        return True
132
133class PowerOverwhelmingException(Exception):
134    pass
135
136def main(stdscr):
137    global height, width, ship
138
139    for i in range(1, 8):
140        curses.init_pair(i, i, 0)
141    curses.curs_set(0)
142    curses.raw()
143
144    height, width = stdscr.getmaxyx()
145
146    if height < 15 or width < 32:
147        raise PowerOverwhelmingException(
148            'Your computer is not powerful enough to run "arc anoid". '
149            'It must support at least 32 columns and 15 rows of next-gen '
150            'full-color 3D graphics.')
151
152    status = curses.newwin(1, width, 0, 0)
153    height -= 1
154    game = curses.newwin(height, width, 1, 0)
155    game.nodelay(1)
156    game.keypad(1)
157
158    grid[:] = [ [ None for x in range(width + 2) ] for y in range(height + 2) ]
159    wall = Wall()
160    for x in range(width + 2):
161        grid[0][x] = wall
162    for y in range(height + 2):
163        grid[y][0] = grid[y][-1] = wall
164    ship = Ship(width / 2, height - 5)
165    entities.append(ship)
166
167    colors = [ 1, 3, 2, 6, 4, 5 ]
168    h = height / 10
169    for x in range(1, int(width / 7) - 1):
170        for y in range(1, 7):
171            entities.append(Block(x * 7,
172                                  y * h + x / 2 % 2,
173                                  7,
174                                  h,
175                                  colors[y - 1]))
176
177    while True:
178        while select.select([ sys.stdin ], [], [], 0)[0]:
179            key = game.getch()
180            if key == curses.KEY_LEFT or key == ord('a') or key == ord('A'):
181                ship.shift(-1)
182            elif key == curses.KEY_RIGHT or key == ord('d') or key == ord('D'):
183                ship.shift(1)
184            elif key == ord(' '):
185                ship.spawn()
186            elif key == 0x1b or key == 3 or key == ord('q') or key == ord('Q'):
187                return
188
189        game.resize(height, width)
190        game.erase()
191        entities[:] = [ ent for ent in entities if ent.tick(game) ]
192
193        status.hline(0, 0, curses.ACS_HLINE, width)
194        status.addch(0, 2, curses.ACS_RTEE)
195        status.addstr(' SCORE: ', curses.A_BOLD | curses.color_pair(4))
196        status.addstr('%s/%s ' % (Block.killed, Block.total), curses.A_BOLD)
197        status.addch(curses.ACS_VLINE)
198        status.addstr(' DEATHS: ', curses.A_BOLD | curses.color_pair(4))
199
200        # See T8693. At the minimum display size, we only have room to render
201        # two characters for the death count, so just display "99" if the
202        # player has more than 99 deaths.
203        display_deaths = Ball.killed
204        if (display_deaths > 99):
205            display_deaths = 99
206
207        status.addstr('%s ' % display_deaths, curses.A_BOLD)
208        status.addch(curses.ACS_LTEE)
209
210        if Block.killed == Block.total:
211            message = ' A WINNER IS YOU!! '
212            i = int(time.time() / 0.8)
213            for x in range(width):
214                for y in range(6):
215                    game.addch(height / 2 + y - 3 + (x / 8 + i) % 2, x,
216                               curses.ACS_BLOCK,
217                               curses.A_BOLD | curses.color_pair(colors[y]))
218            game.addstr(height / 2, (width - len(message)) / 2, message,
219                           curses.A_BOLD | curses.color_pair(7))
220
221        game.refresh()
222        status.refresh()
223        time.sleep(0.05)
224
225try:
226    curses.wrapper(main)
227    print ('You destroyed %s blocks out of %s with %s deaths.' %
228        (Block.killed, Block.total, Ball.killed))
229except PowerOverwhelmingException as e:
230    print (e)
231