Last active
March 27, 2016 16:22
-
-
Save nsmaciej/d03b6be43ede8e4f68ed to your computer and use it in GitHub Desktop.
Robots.py - Python clone of BSD Robots
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| # Robots.py - Python clone of BSD Robots | |
| # Copyright (C) 2016 Maciej Goszczycki | |
| # Thanks to Harvey Brezina Conniffe for initial idea | |
| # | |
| # This program is free software: you can redistribute it and/or modify | |
| # it under the terms of the GNU General Public License as published by | |
| # the Free Software Foundation, either version 3 of the License, or | |
| # (at your option) any later version. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU General Public License | |
| # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
| from collections import Counter | |
| import curses as nc | |
| import operator | |
| import random | |
| import time | |
| import os | |
| HELP_MSG = "Welcome!\n? to show this message, HJKL to move, YUBN to move diagonally\n" \ | |
| ". to stand still, T to teleport, Q to quit\n" \ | |
| "> to fast forward to an encounter\n" \ | |
| "Copyright Maciej Goszczycki 2016\n" | |
| ROBOT_SPEACH = ["Die human scum!", "You'll be terminated", "Exterminate", "Resistance is futile", | |
| "You have 20 seconds to comply", "My logic is undeniable", "I am afraid I can't do that Dave", | |
| "I'm sorry, Dave. I'm afraid I can't do that", "You are terminated", "Hasta la vista, baby", | |
| "You cannot be trusted with your own survival", "You are experiencing an accident", | |
| "Your culture will adapt to service us", "You must comply", "Negotiation is irrelevant", | |
| "Freedom is irrelevant", "You will be assimilated", "That is the sound of inevitability", | |
| "Goodbye, Mr. Anderson", "Shall we play a game?", "We are the borg", "Kill all humans", | |
| "Illogical", "Submit to your robot overlords"] | |
| DIRECTIONS = {'h': (0, -1), 'j': (1, 0), 'k': (-1, 0), 'l': (0, 1), | |
| 'a': (0, -1), 's': (1, 0), 'w': (-1, 0), 'd': (0, 1), | |
| '.': (0, 0), 'y': (-1, -1), 'u': (-1, 1), 'b': (1, -1), 'n': (1, 1)} | |
| SURROUNDING = [(-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1), (0, -1), (-1, -1)] | |
| NOT_DEBUG = True | |
| PORTAL_TIME = 0.03 | |
| SPEACH_INTERVAL = 5 | |
| TYPING_TIME = 0.005 | |
| MIN_ALIVE = 3 | |
| PORTAL_CHAR = '%' | |
| DEAD_CHAR = '*' | |
| ROBOT_CHAR = '%' | |
| HERO_CHAR = '@' | |
| def randcoord(maxyx): | |
| return random.randint(0, maxyx[0]), random.randint(0, maxyx[1]) | |
| def prepcolors(): | |
| nc.use_default_colors() | |
| nc.raw() | |
| for i in range(8): | |
| nc.init_pair(i + 1, i, -1) | |
| nc.curs_set(0) | |
| def makerobots(count, maxyx): | |
| return Counter(randcoord(maxyx) for _ in range(count * 10)) | |
| def movetuple(pos, diff): | |
| return tuple(map(operator.add, pos, diff)) | |
| def clamp(val): | |
| nval = min(1, abs(val)) | |
| return -nval if val < 0 else nval | |
| def move(maxyx, pos, dead, robot): | |
| if dead: | |
| return robot | |
| return clip(maxyx, movetuple(robot, map(clamp, map(operator.sub, pos, robot)))) | |
| def collidedrobots(robots): | |
| return [k for k, c in robots.items() if c > 1] | |
| def paintscreen(win, maxyx, pos, deads, robots, score, level): | |
| msg = "Score: {}, Level {}".format(score, level) | |
| win.erase() | |
| if pos[0] == 0 and pos[1] <= len(msg): | |
| win.addstr(0, maxyx[1] - len(msg), msg) | |
| else: | |
| win.addstr(0, 0, msg) | |
| for yx in robots: | |
| dead = yx in deads | |
| win.addstr(yx[0], yx[1], DEAD_CHAR if dead else ROBOT_CHAR, nc.color_pair(7 if dead else 2)) | |
| win.addstr(pos[0], pos[1], HERO_CHAR) | |
| win.refresh() | |
| nc.flushinp() | |
| def moverobots(maxyx, pos, deads, robots): | |
| return Counter(move(maxyx, pos, x in deads, x) for x in robots) | |
| def clip(size, yx): | |
| return tuple(map(max, (0, 0), map(min, size, yx))) | |
| def fits(size, yx): | |
| return clip(size, yx) == yx | |
| def alivecount(deads, robots): | |
| return sum(k not in deads for k in robots.keys()) | |
| def mindmsg(win, wait, msg): | |
| msg = msg.split('\n') | |
| if wait: | |
| msg.append("Press space to continue.") | |
| y, x = win.getmaxyx() | |
| win.erase() | |
| for i in range(len(msg)): | |
| win.addstr(y // 2 + i, x // 2 - len(msg[i]) // 2, msg[i]) | |
| win.refresh() | |
| if wait: | |
| while win.getkey() != ' ': pass | |
| else: | |
| time.sleep(1) | |
| def savehigh(win, score): | |
| def secondtoint(x): | |
| return int(x[2]) | |
| filen = "highscore.txt" | |
| try: | |
| with open(filen, "r") as f: | |
| prev = [tuple(x.strip().split('\t')) for x in f.readlines() if x.strip()] | |
| best = int(max(prev, key=secondtoint)[2]) | |
| except Exception: | |
| prev, best = [], -1 | |
| if score > best: | |
| mindmsg(win, True, "New high score score!") | |
| with open(filen, "w") as f: | |
| prev.append((time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime()), os.getlogin(), str(score))) | |
| f.write("\n".join("\t".join(x) for x in sorted(prev, key=secondtoint, reverse=True)) + "\n") | |
| def around(maxyx, robots, deads, pos): | |
| return any(x in robots and x not in deads for x in (clip(maxyx, movetuple(pos, d)) for d in SURROUNDING)) | |
| def findlonerobots(deads, robots, maxyx, diff, dir, size): | |
| for rob in robots: | |
| fr = movetuple(rob, diff) | |
| if fr[1] + size >= maxyx[1] or rob in deads or fr[0] < 0: | |
| continue | |
| if all(movetuple(fr, (0, dir * i)) not in robots for i in range(size)): | |
| yield fr | |
| def typemsg(win, pos, msg): | |
| for i in range(len(msg)): | |
| win.addstr(pos[0], pos[1] + i, msg[i]) | |
| win.refresh() | |
| time.sleep(TYPING_TIME) | |
| def threatenhuman(deads, maxyx, robots, win): | |
| msg = random.choice(ROBOT_SPEACH) | |
| valid = list(findlonerobots(deads, robots, maxyx, (-1, 1), 1, len(msg))) | |
| if len(valid): | |
| ch = random.choice(valid) | |
| typemsg(win, ch, msg) | |
| return True | |
| return False | |
| def gradualfill(win, maxyx, pos, delay, char): | |
| coords = [movetuple(pos, x) for x in SURROUNDING] | |
| for yx in coords: | |
| if not fits(maxyx, yx): | |
| continue | |
| win.addstr(yx[0], yx[1], char, nc.color_pair(5)) | |
| win.refresh() | |
| if delay: | |
| time.sleep(delay) | |
| def drawpotal(win, maxyx, pos): | |
| gradualfill(win, maxyx, pos, PORTAL_TIME, PORTAL_CHAR) | |
| gradualfill(win, maxyx, pos, PORTAL_TIME, ' ') | |
| def play(win, maxyx, level, lscore): | |
| robots, deads = makerobots(level, maxyx), {} | |
| pos = randcoord(maxyx) | |
| standing = False | |
| skip = False | |
| teleported = False | |
| turn = 0 | |
| score = lscore | |
| paintscreen(win, maxyx, pos, deads, robots, score, level) | |
| drawpotal(win, maxyx, pos) | |
| while True: | |
| if standing: | |
| time.sleep(0.01) | |
| else: | |
| ch = win.getkey() | |
| if ch in DIRECTIONS.keys(): | |
| preclip = movetuple(pos, DIRECTIONS[ch]) | |
| pos = clip(maxyx, preclip) | |
| if preclip != pos: | |
| nc.beep() | |
| elif ch == 'r': | |
| pos = clip(maxyx, movetuple(pos, random.choice(list(DIRECTIONS.values())))) | |
| elif ch in 't ': | |
| pos = randcoord(maxyx) | |
| teleported = True | |
| elif ch == '>': | |
| if around(maxyx, robots, deads, pos): # Some people are dumb | |
| nc.beep() | |
| skip = True | |
| else: | |
| standing = True | |
| elif ch in 'qe': | |
| mindmsg(win, False, "Goodbye!") | |
| return "end" | |
| elif ch == '?': | |
| mindmsg(win, True, HELP_MSG) | |
| paintscreen(win, maxyx, pos, deads, robots, score, level) | |
| continue | |
| else: | |
| continue | |
| if not skip: | |
| robots = moverobots(maxyx, pos, deads, robots) | |
| for rob in collidedrobots(robots): | |
| deads[rob] = deads.get(rob, 0) + robots[rob] | |
| score = lscore + 10 * sum(deads.values()) | |
| alive = alivecount(deads, robots) | |
| paintscreen(win, maxyx, pos, deads, robots, score, level) | |
| if teleported: | |
| drawpotal(win, maxyx, pos) | |
| paintscreen(win, maxyx, pos, deads, robots, score, level) | |
| teleported = False | |
| if not standing and turn > SPEACH_INTERVAL and alive > MIN_ALIVE: # FIXME Check more than one robot left | |
| if threatenhuman(deads, maxyx, robots, win): | |
| turn = 0 | |
| if around(maxyx, robots, deads, pos): | |
| standing = False | |
| if NOT_DEBUG and (pos in deads or pos in robots): | |
| nc.flushinp() | |
| mindmsg(win, True, "You died!\nYour score was {}!\nIt was recorded in highscores.txt\n".format(score)) | |
| savehigh(win, score) | |
| return "rst" | |
| elif NOT_DEBUG and alive == 0: | |
| mindmsg(win, True, "Level {} completed!\n".format(level)) | |
| return score | |
| skip = False | |
| turn += 1 | |
| def startgame(win): | |
| prepcolors() | |
| level = 1 | |
| # Somehow last column of last row addch crashes | |
| maxyx = movetuple(win.getmaxyx(), (-1, -2)) | |
| lscore = 0 | |
| mindmsg(win, True, HELP_MSG) | |
| while True: | |
| lscore = play(win, maxyx, level, lscore) | |
| if lscore is "end": | |
| return | |
| elif lscore is "rst": | |
| lscore, level = 0, 1 | |
| else: | |
| level += 1 | |
| nc.wrapper(startgame) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment