bowling.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592
  1. #!/usr/bin/env python
  2. "haliphax's crappy bowling game"
  3. __author__ = 'haliphax <todd@roadha.us>'
  4. def echo(text, flush=True):
  5. "helper function for output"
  6. from sys import stdout
  7. stdout.write(text.encode('cp437'))
  8. if flush:
  9. stdout.flush()
  10. class Bowling(object):
  11. "class definition"
  12. def __init__(self):
  13. "initialize the pin positions"
  14. from blessed import Terminal
  15. # setup
  16. self.term = Terminal()
  17. if self.term.kind.startswith('ansi'):
  18. # monkey-patch move_x
  19. def term_move_x(xpos):
  20. return '\x1b[{xpos}G'.format(xpos=xpos + 1)
  21. self.term.move_x = term_move_x
  22. # pin positions
  23. self.pinpos = []
  24. xloc = 0
  25. yloc = 0
  26. self.pinpos.append([xloc + 3, yloc + 9])
  27. for i in range(2, 3 + 1):
  28. self.pinpos.append([xloc + 2, yloc + 7 + (i - 2) * 4])
  29. for i in range(4, 6 + 1):
  30. self.pinpos.append([xloc + 1, yloc + 5 + (i - 4) * 4])
  31. for i in range(7, 10 + 1):
  32. self.pinpos.append([xloc, yloc + 3 + (i - 7) * 4])
  33. def draw_lane(self):
  34. "draw the lane"
  35. xloc = 0
  36. yloc = 0
  37. term = self.term
  38. gutter = term.black_on_white(chr(178).decode('cp437')) # gutter char
  39. # lane
  40. for i in range(0, 22 + 1):
  41. echo(term.move(xloc + i, yloc))
  42. echo(gutter)
  43. echo(term.on_yellow(' ' * 17))
  44. echo(gutter)
  45. # bowler's deck
  46. echo(term.move(xloc + 23, yloc))
  47. echo(term.on_yellow(' ' * 19))
  48. # lane markers
  49. echo(term.move(xloc + 5, yloc + 3))
  50. echo(term.black_on_yellow('^ ^ ^ ^ ^'))
  51. echo(term.move(xloc + 17, yloc + 9))
  52. echo(term.black_on_yellow('^'))
  53. echo(term.move(xloc + 18, yloc + 6))
  54. echo(term.black_on_yellow('^ ^'))
  55. echo(term.move(xloc + 19, yloc + 3))
  56. echo(term.black_on_yellow('^ ^'))
  57. def draw_pins(self, pins):
  58. "draw the pins"
  59. term = self.term
  60. for i in pins:
  61. echo(term.move(self.pinpos[i - 1][0], self.pinpos[i - 1][1]))
  62. echo(term.bold_white_on_yellow(chr(173).decode('cp437')))
  63. def move_bowler(self, spot):
  64. "move the bowler"
  65. term = self.term
  66. xloc = 23
  67. yloc = 0
  68. bowler = term.blue_on_yellow(chr(234).decode('cp437')) # bowler char
  69. # initial position
  70. echo(term.move(xloc, yloc + spot + 1) + bowler)
  71. done = False
  72. while not done:
  73. oldspot = spot
  74. with term.cbreak():
  75. inp = term.inkey(timeout=0.25)
  76. if (inp in [u'4', u'a', u'h'] or inp.code in [term.KEY_LEFT,]) \
  77. and spot > 0:
  78. spot -= 1
  79. elif (inp in [u'6', u'd', u'l'] or inp.code in [term.KEY_RIGHT,]) \
  80. and spot < 16:
  81. spot += 1
  82. elif inp == u'q' or inp.code in [term.KEY_ESCAPE,]:
  83. return False
  84. elif inp in [u' ', u'\n', u'\r']:
  85. done = True
  86. if inp and spot != oldspot:
  87. echo(''.join([term.move(xloc, yloc + 1 + oldspot),
  88. term.on_yellow(' '), term.move_x(yloc + 1 + spot),
  89. bowler]))
  90. return spot
  91. def power_swingbar(self):
  92. "power swing bar"
  93. term = self.term
  94. stages = [
  95. (25, term.bold_blue),
  96. (50, term.bold_green),
  97. (75, term.bold_yellow),
  98. (100, term.bold_red),
  99. ]
  100. return self.__hswingbar(1, 20, 35, stages, 'WEAK', 'STRONG', False,
  101. True)
  102. def hook_swingbar(self):
  103. "hook swing bar"
  104. term = self.term
  105. stages = [
  106. (10, term.bold_red),
  107. (25, term.bold_yellow),
  108. (40, term.bold_green),
  109. (60, term.bold_blue),
  110. (76, term.bold_green),
  111. (91, term.bold_yellow),
  112. (100, term.bold_red),
  113. ]
  114. return self.__hswingbar(4, 20, 35, stages, 'LEFT', 'RIGHT', True, True)
  115. def __hswingbar(self, xloc, yloc, width=35, stages=None, ltxt='', rtxt='',
  116. wait=False, bounce=False):
  117. "helper for creating horizontal swing bars"
  118. if stages is None:
  119. raise Exception('stages not provided')
  120. width = 35
  121. llen = len(ltxt)
  122. rlen = len(rtxt)
  123. term = self.term
  124. bar = chr(178).decode('cp437') # bar char
  125. widthpct = float(width) / 100
  126. val = 0
  127. count = 0
  128. pct = None
  129. inp = None
  130. # draw the frame
  131. echo(term.move(xloc, yloc))
  132. echo(term.bold(''.join([
  133. chr(213), ltxt, (chr(205) * (width / 2 - llen)), chr(209),
  134. (chr(205) * (width / 2 - rlen)), rtxt, chr(184)
  135. ]).decode('cp437')))
  136. echo(term.move(xloc + 1, yloc))
  137. echo(term.bold(''.join([chr(179), (' ' * width), chr(179)
  138. ]).decode('cp437')))
  139. echo(term.move(xloc + 2, yloc))
  140. echo(term.bold(''.join([chr(212), (chr(205) * (width / 2)), chr(207),
  141. (chr(205) * (width / 2)), chr(190)]).decode('cp437')))
  142. # swallow anything in the input buffer
  143. term.inkey(timeout=0.001)
  144. # are we waiting for input first?
  145. if wait:
  146. with term.cbreak():
  147. while inp not in [u' ', u'\n', u'\r']:
  148. inp = term.inkey(timeout=0.25)
  149. inp = None
  150. direction = 1
  151. echo(term.move(xloc + 1, yloc + 1))
  152. # bar animation
  153. while inp not in [u' ', u'\n', u'\r']:
  154. count += direction
  155. with term.cbreak():
  156. inp = term.inkey(timeout=0.001)
  157. # pct has changed enough to move the bar
  158. if int(widthpct * count / 10) != val:
  159. val += direction
  160. pct = int(float(val) / width * 100)
  161. # moving right (+1) or left (-1)?
  162. if direction == 1:
  163. # display bar in appropriate color for stage
  164. for threshold, func in stages:
  165. if pct > threshold:
  166. continue
  167. echo(func(bar))
  168. break
  169. else:
  170. # erase the bar
  171. echo(term.move_left + ' ' + term.move_left)
  172. # hit the edge; bounce or stop?
  173. if pct >= 100:
  174. if bounce:
  175. direction = -1
  176. else:
  177. break
  178. elif pct == 0:
  179. break
  180. return float(widthpct * count / 10 / width * 100) - 1
  181. def bowl(self, pins, spot, power, hook):
  182. "throw the ball"
  183. import random
  184. from time import sleep
  185. random.seed()
  186. term = self.term
  187. xloc = 23
  188. yloc = 0
  189. hookabs = abs(hook - 50) # absolute value of hook pct
  190. collided = list() # list of pins to track action for
  191. lasty = None
  192. i = 1
  193. while i <= xloc or len(collided) > 0:
  194. # apply power and hook
  195. offset = float(pow(i, 1.3 + hookabs / 50)) / power
  196. # straight throw adjustment
  197. if hookabs < 2:
  198. offset = 0
  199. # dampen hooks <18
  200. elif hookabs < 18:
  201. offset = offset * 0.5
  202. # dampen hooks <35
  203. elif hookabs < 35:
  204. offset = offset * 0.75
  205. # if it's a left hook, offset is negative
  206. if hook < 51:
  207. offset = offset * -1
  208. intoffset = int(offset)
  209. myx = xloc - i
  210. myy = max(0, min(yloc + spot + 1 + intoffset, yloc + 18))
  211. realy = max(0, min(yloc + spot + 1 + offset, yloc + 18))
  212. # gutter ball?
  213. edge = spot + intoffset + 1 <= 0 or spot + intoffset >= 17
  214. delay = 0.15
  215. # draw the ball; use a while loop with breaks to avoid indent hell
  216. while 1:
  217. if xloc - i < 0:
  218. break
  219. if myx >= 0 and myy >= 0:
  220. echo(term.move(myx, myy))
  221. if edge:
  222. echo(term.black_on_white('o')) # in the gutter
  223. break
  224. echo(term.black_on_yellow('o')) # in the lane
  225. # no need to check for collision early (pins are far away, duh)
  226. if i <= 18:
  227. break
  228. # do we have enough power to knock a pin over?
  229. if power <= 1:
  230. break
  231. # did we collide with a pin?
  232. for j in pins:
  233. px = self.pinpos[j - 1][0]
  234. py = self.pinpos[j - 1][1]
  235. # check proximity of ball for pin
  236. if px == myx and (py == myy or
  237. abs(py - (yloc + spot + 1 + offset)) < 2):
  238. # @TODO better calculation!
  239. # at what angle are we sending the pin?
  240. if realy - lasty == 0:
  241. slope = 0.25
  242. else:
  243. slope = realy - lasty
  244. # reverse if the pin is opposite the ball
  245. if ((offset >= 0 and realy > lasty)
  246. or (offset < 0 and realy < lasty)):
  247. slope *= -1
  248. # add a bit of randomness to the pin scatter
  249. slope *= (1 + (0.01 * random.randint(0, 15)))
  250. # increase pin action distance per row
  251. slope += 1.5 * (3 - myx)
  252. # add to list of pins to track action for
  253. collided.append([px, py, slope, power])
  254. # remove pin
  255. pins = [x for x in pins if x != j]
  256. # prevent delay in ball anim frame
  257. delay = 0
  258. echo(term.move(px, py))
  259. # show fall-down animation
  260. for k in ['-', '\\', '|', '/', '-']:
  261. echo(term.bold_white_on_yellow(k))
  262. with term.cbreak():
  263. sleep(0.03)
  264. echo(term.move_left)
  265. # erase the pin
  266. echo(term.on_yellow(' '))
  267. # reduce ball's remaining power
  268. power = power * 0.85
  269. # can't collide with more than 1 pin per anim frame
  270. break
  271. break
  272. if delay > 0:
  273. with term.cbreak():
  274. sleep(delay)
  275. # remember where we were this anim frame for calculating slope
  276. lasty = realy
  277. # replace ball with gutter, lane, or streak
  278. if xloc - i >= 0:
  279. echo(term.move(myx, myy))
  280. if edge:
  281. echo(term.black_on_white(chr(178).decode('cp437'))) # gutter
  282. else:
  283. if random.randint(1, 5) <= 1:
  284. echo(term.bold_black_on_yellow('|')) # streak
  285. else:
  286. echo(term.on_yellow(' ')) # lane
  287. # pin action
  288. for j in range(len(collided)):
  289. action = collided.pop()
  290. pinx = action[0]
  291. piny = action[1]
  292. pinslope = action[2]
  293. pinmom = action[3]
  294. # decrease momentum
  295. pinmom *= 0.85
  296. rpiny = pinslope * (1 + (pinmom / 100)) + piny
  297. piny = int(rpiny)
  298. # do we have enough momentum to knock over a pin?
  299. if pinmom > 10:
  300. # see if we've hit another pin
  301. for k in pins:
  302. opx = self.pinpos[k - 1][0]
  303. opy = self.pinpos[k - 1][1]
  304. # @TODO don't just check proximity; check path, too
  305. if abs(opx - pinx) < 2 and (opy == piny
  306. or abs(opy - rpiny) < 2):
  307. # hit; add to pin action tracker with random scatter
  308. scatter = float(random.randint(85, 115)) / 100
  309. if random.randint(1, 5) <= 2:
  310. scatter *= -1
  311. collided.append([opx, opy, pinslope * scatter,
  312. pinmom])
  313. # remove pin from play
  314. pins = [x for x in pins if x != k]
  315. # skip ball animation delay
  316. delay = 0
  317. echo(term.move(opx, opy))
  318. # show pin knock-over animation
  319. for l in ['-', '\\', '|', '/', '-']:
  320. echo(term.bold_white_on_yellow(l))
  321. with term.cbreak():
  322. sleep(0.03)
  323. echo(term.move_left)
  324. # clean up
  325. echo(term.on_yellow(' '))
  326. pinx -= 1
  327. # only continue to track pin if it's in play
  328. if not (pinx < xloc - 23 or piny <= 0 or piny >= 17):
  329. collided.append([pinx, piny, pinslope, pinmom])
  330. i += 1
  331. return pins
  332. def run(self):
  333. "main method; let's bowl!"
  334. from time import sleep
  335. # intro
  336. term = self.term
  337. echo(term.clear)
  338. echo(u'\r\n'.join((
  339. term.bold(__doc__),
  340. term.bold('=' + ('-' * (len(__doc__) - 2)) + '='),
  341. '',
  342. 'Move the bowler with 4, LEFT ARROW, H, 6, RIGHT ARROW, or L.',
  343. 'Press Q/ESC while moving the bowler to quit early.',
  344. 'Press SPACE/ENTER to begin filling up your power bar, '
  345. 'and again to stop.',
  346. 'Press SPACE/ENTER to begin filling up your hook bar, '
  347. 'and again to stop.',
  348. 'The stronger your throw, the less effect hook will have',
  349. '...but you get more pin action.',
  350. '',
  351. 'Good luck!',
  352. '',
  353. term.bold_green('[Press any key to continue]')
  354. )))
  355. with term.cbreak():
  356. term.inkey()
  357. echo(term.clear)
  358. # main loop
  359. framenum = 1
  360. frames = []
  361. framescore = []
  362. message = None
  363. def new_frame(pins, frames, framescore):
  364. pins = range(1, 11)
  365. frames.append(framescore)
  366. framescore = list()
  367. return pins, frames, framescore
  368. while framenum <= 11:
  369. pins = range(1, 11)
  370. spot = 8
  371. # @TODO better (i.e., actual bowling) scorekeeping
  372. # show the previous frame's score
  373. if framenum > 1:
  374. echo(''.join([term.move(6 + framenum, 22),
  375. 'Frame {frame}: {score}' \
  376. .format(frame=framenum - 1,
  377. score=frames[framenum - 2])]))
  378. # draw the lane
  379. echo(''.join([term.move(23, 22), term.normal, term.clear_eol]))
  380. self.draw_lane()
  381. if framenum == 11:
  382. break
  383. for i in range(0, 3):
  384. if message:
  385. echo(''.join([term.move(21, 22), term.normal,
  386. term.clear_eol, term.move_x(22), message]))
  387. message = None
  388. if len(pins):
  389. echo(''.join([term.move(23, 22), term.normal,
  390. term.clear_eol, term.move_x(22),
  391. 'Frame {0}, Throw {1}'.format(framenum,
  392. i + 1)]))
  393. # show the pins
  394. self.draw_pins(pins)
  395. # move the bowler
  396. spot = self.move_bowler(spot)
  397. if spot is False:
  398. return
  399. # power
  400. power = self.power_swingbar()
  401. # hook
  402. hook = self.hook_swingbar()
  403. # throw the ball
  404. hadpins = len(pins)
  405. pins = self.bowl(pins, spot, power, hook)
  406. framescore.append(hadpins - len(pins))
  407. pinsleft = len(pins)
  408. echo(''.join([term.move(22, 24), term.normal, term.clear_eol]))
  409. # do they get another throw?
  410. if framenum == 10:
  411. if i == 0:
  412. if pinsleft:
  413. message = 'Pick up the spare!'
  414. continue
  415. else:
  416. message = 'Bonus throw!'
  417. pins, frames, framescore = new_frame(pins, frames,
  418. framescore)
  419. continue
  420. elif i == 1:
  421. if pinsleft:
  422. if framescore[0] == 10:
  423. message = 'Bonus throw!'
  424. pins, frames, framescore = new_frame(pins,
  425. frames,
  426. framescore)
  427. continue
  428. else:
  429. pins = []
  430. else:
  431. message = 'Bonus throw!'
  432. pins, frames, framescore = new_frame(pins, frames,
  433. framescore)
  434. continue
  435. else:
  436. pins = []
  437. framenum = 11
  438. elif i == 0:
  439. if pinsleft:
  440. message = 'Pick up the spare!'
  441. else:
  442. message = 'Nice strike!'
  443. elif i == 1:
  444. if pinsleft:
  445. pins = []
  446. message = "You'll get 'em next time!"
  447. else:
  448. message = 'Nice spare!'
  449. if not pinsleft:
  450. # new frame
  451. if framenum < 10:
  452. pins, frames, framescore = new_frame(pins, frames,
  453. framescore)
  454. else:
  455. pins = []
  456. framenum += 1
  457. break
  458. # exit
  459. self.show_scores(frames)
  460. with term.cbreak():
  461. term.inkey(5)
  462. echo(term.normal)
  463. echo(term.move(term.height - 1, 0))
  464. # fire it up, holmes
  465. b = Bowling()
  466. with b.term.hidden_cursor():
  467. b.run()