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