diff --git a/84_Super_Star_Trek/python/superstartrek.py b/84_Super_Star_Trek/python/superstartrek.py index 2dddef87..40ccb9f6 100644 --- a/84_Super_Star_Trek/python/superstartrek.py +++ b/84_Super_Star_Trek/python/superstartrek.py @@ -15,13 +15,79 @@ Python translation by Jack Boyce - February 2021 import random +import sys +from dataclasses import dataclass +from enum import Enum from math import sqrt -from typing import Any, Callable, Dict, List, Tuple +from typing import Callable, Dict, Final, List, Optional, Tuple + + +def get_user_float(prompt: str) -> float: + """Get input from user and return it.""" + while True: + answer = input(prompt) + try: + answer_float = float(answer) + return answer_float + except ValueError: + pass + + +class Entity(Enum): + klingon = "+K+" + ship = "<*>" + empty = "***" + starbase = ">!<" + star = " * " + void = " " + + +@dataclass +class Point: + x: int + y: int + + def __str__(self) -> str: + return f"{self.x + 1} , {self.y + 1}" + + +@dataclass +class Position: + """ + Every quadrant has 8 sectors + + Hence the position could also be represented as: + x = quadrant.x * 8 + sector.x + y = quadrant.y * 8 + sector.y + """ + + quadrant: Point + sector: Point + + +@dataclass +class QuadrantData: + klingons: int + bases: int + stars: int + + def num(self) -> int: + return 100 * self.klingons + 10 * self.bases + self.stars + + +@dataclass +class KlingonShip: + sector: Point + shield: float class Ship: - def __init__(self, energy: int = 3000, shields: int = 0, torpedoes: int = 10): - self.energy: int = energy + energy_capacity: int = 3000 + torpedo_capacity: int = 10 + + def __init__(self) -> None: + self.position = Position(Point(fnr(), fnr()), Point(fnr(), fnr())) + self.energy: int = Ship.energy_capacity self.devices: Tuple[str, ...] = ( "WARP ENGINES", "SHORT RANGE SENSORS", @@ -32,13 +98,15 @@ class Ship: "SHIELD CONTROL", "LIBRARY-COMPUTER", ) - self.damage_stats: List[float] = [0] * len( - self.devices - ) # damage stats for devices - self.shields = shields - self.torpedoes = torpedoes + self.damage_stats: List[float] = [0] * len(self.devices) + self.shields = 0 + self.torpedoes = Ship.torpedo_capacity self.docked: bool = False # true when docked at starbase + def refill(self) -> None: + self.energy = Ship.energy_capacity + self.torpedoes = Ship.torpedo_capacity + def maneuver_energy(self, n: int) -> None: """Deduct the energy for navigation from energy/shields.""" self.energy -= n + 10 @@ -78,714 +146,973 @@ class Ship: self.energy += self.shields - x self.shields = x print("DEFLECTOR CONTROL ROOM REPORT:") - print(f" 'SHIELDS NOW AT {ship.shields} UNITS PER YOUR COMMAND.'") + print(f" 'SHIELDS NOW AT {self.shields} UNITS PER YOUR COMMAND.'") + + +class Quadrant: + def __init__( + self, + point: Point, # position of the quadrant + population: QuadrantData, + ship_position: Position, + ) -> None: + """Populate quadrant map""" + assert 0 <= point.x <= 7 and 0 <= point.y <= 7 + self.name = Quadrant.quadrant_name(point.x, point.y, False) + + self.nb_klingons = population.klingons + self.nb_bases = population.bases + self.nb_stars = population.stars + + # extra delay in repairs at base + self.delay_in_repairs_at_base: float = 0.5 * random.random() + + # Klingons in current quadrant + self.klingon_ships: List[KlingonShip] = [] + + # Initialize empty: save what is at which position + self.data = [[Entity.void for _ in range(8)] for _ in range(8)] + + self.populate_quadrant(ship_position) + + @classmethod + def quadrant_name(cls, row: int, col: int, region_only: bool = False) -> str: + """Return quadrant name visible on scans, etc.""" + region1 = [ + "ANTARES", + "RIGEL", + "PROCYON", + "VEGA", + "CANOPUS", + "ALTAIR", + "SAGITTARIUS", + "POLLUX", + ] + region2 = [ + "SIRIUS", + "DENEB", + "CAPELLA", + "BETELGEUSE", + "ALDEBARAN", + "REGULUS", + "ARCTURUS", + "SPICA", + ] + modifier = ["I", "II", "III", "IV"] + + quadrant = region1[row] if col < 4 else region2[row] + + if not region_only: + quadrant += " " + modifier[col % 4] + + return quadrant + + def set_value(self, x: float, y: float, entity: Entity) -> None: + self.data[round(x)][round(y)] = entity + + def get_value(self, x: float, y: float) -> Entity: + return self.data[round(x)][round(y)] + + def find_empty_place(self) -> Tuple[int, int]: + """Find an empty location in the current quadrant.""" + while True: + row, col = fnr(), fnr() + if self.get_value(row, col) == Entity.void: + return row, col + + def populate_quadrant(self, ship_position: Position) -> None: + self.set_value(ship_position.sector.x, ship_position.sector.y, Entity.ship) + for _ in range(self.nb_klingons): + x, y = self.find_empty_place() + self.set_value(x, y, Entity.klingon) + self.klingon_ships.append( + KlingonShip( + Point(x, y), klingon_shield_strength * (0.5 + random.random()) + ) + ) + if self.nb_bases > 0: + # Position of starbase in current sector + starbase_x, starbase_y = self.find_empty_place() + self.starbase = Point(starbase_x, starbase_y) + self.set_value(starbase_x, starbase_y, Entity.starbase) + for _ in range(self.nb_stars): + x, y = self.find_empty_place() + self.set_value(x, y, Entity.star) + + def __str__(self) -> str: + quadrant_string = "" + for row in self.data: + for entity in row: + quadrant_string += entity.value + return quadrant_string + + +class World: + def __init__( + self, + total_klingons: int = 0, # Klingons at start of game + bases_in_galaxy: int = 0, + ) -> None: + self.ship = Ship() + self.initial_stardate = 100 * random.randint(20, 39) + self.stardate: float = self.initial_stardate + self.mission_duration = random.randint(25, 34) + + # Enemy + self.remaining_klingons = total_klingons + + # Player starbases + self.bases_in_galaxy = bases_in_galaxy + + self.galaxy_map: List[List[QuadrantData]] = [ + [QuadrantData(0, 0, 0) for _ in range(8)] for _ in range(8) + ] + self.charted_galaxy_map: List[List[QuadrantData]] = [ + [QuadrantData(0, 0, 0) for _ in range(8)] for _ in range(8) + ] + + # initialize contents of galaxy + for x in range(8): + for y in range(8): + r1 = random.random() + + if r1 > 0.98: + quadrant_klingons = 3 + elif r1 > 0.95: + quadrant_klingons = 2 + elif r1 > 0.80: + quadrant_klingons = 1 + else: + quadrant_klingons = 0 + self.remaining_klingons += quadrant_klingons + + quadrant_bases = 0 + if random.random() > 0.96: + quadrant_bases = 1 + self.bases_in_galaxy += 1 + self.galaxy_map[x][y] = QuadrantData( + quadrant_klingons, quadrant_bases, 1 + fnr() + ) + + if self.remaining_klingons > self.mission_duration: + self.mission_duration = self.remaining_klingons + 1 + + if self.bases_in_galaxy == 0: # original has buggy extra code here + self.bases_in_galaxy = 1 + self.galaxy_map[self.ship.position.quadrant.x][ + self.ship.position.quadrant.y + ].bases += 1 + + curr = self.ship.position.quadrant + self.quadrant = Quadrant( + self.ship.position.quadrant, + self.galaxy_map[curr.x][curr.y], + self.ship.position, + ) + + def remaining_time(self) -> float: + return self.initial_stardate + self.mission_duration - self.stardate + + def has_mission_ended(self) -> bool: + return self.remaining_time() < 0 + + +class Game: + """Handle user actions""" + + def __init__(self) -> None: + self.restart = False + self.world = World() + + def startup(self) -> None: + """Initialize the game variables and map, and print startup messages.""" + print( + "\n\n\n\n\n\n\n\n\n\n\n" + " ,------*------,\n" + " ,------------- '--- ------'\n" + " '-------- --' / /\n" + " ,---' '-------/ /--,\n" + " '----------------'\n\n" + " THE USS ENTERPRISE --- NCC-1701\n" + "\n\n\n\n" + ) + self.world = World() + world = self.world + print( + "YOUR ORDERS ARE AS FOLLOWS:\n" + f" DESTROY THE {world.remaining_klingons} KLINGON WARSHIPS WHICH HAVE INVADED\n" + " THE GALAXY BEFORE THEY CAN ATTACK FEDERATION HEADQUARTERS\n" + f" ON STARDATE {world.initial_stardate+world.mission_duration}. " + f" THIS GIVES YOU {world.mission_duration} DAYS. THERE " + f"{'IS' if world.bases_in_galaxy == 1 else 'ARE'}\n" + f" {world.bases_in_galaxy} " + f"STARBASE{'' if world.bases_in_galaxy == 1 else 'S'} IN THE GALAXY FOR " + "RESUPPLYING YOUR SHIP.\n" + ) + + def new_quadrant(self) -> None: + """Enter a new quadrant: populate map and print a short range scan.""" + world = self.world + ship = world.ship + q = ship.position.quadrant + + world.quadrant = Quadrant( + q, + world.galaxy_map[q.x][q.y], + ship.position, + ) + + world.charted_galaxy_map[q.x][q.y] = world.galaxy_map[q.x][q.y] + + if world.stardate == world.initial_stardate: + print("\nYOUR MISSION BEGINS WITH YOUR STARSHIP LOCATED") + print(f"IN THE GALACTIC QUADRANT, '{world.quadrant.name}'.\n") + else: + print(f"\nNOW ENTERING {world.quadrant.name} QUADRANT . . .\n") + + if world.quadrant.nb_klingons != 0: + print("COMBAT AREA CONDITION RED") + if ship.shields <= 200: + print(" SHIELDS DANGEROUSLY LOW") + self.short_range_scan() + + def fnd(self, i: int) -> float: + """Find distance between Enterprise and i'th Klingon warship.""" + ship = self.world.ship.position.sector + klingons = self.world.quadrant.klingon_ships[i].sector + return sqrt((klingons.x - ship.x) ** 2 + (klingons.y - ship.y) ** 2) + + def klingons_fire(self) -> None: + """Process nearby Klingons firing on Enterprise.""" + ship = self.world.ship + + if self.world.quadrant.nb_klingons <= 0: + return + if ship.docked: + print("STARBASE SHIELDS PROTECT THE ENTERPRISE") + return + + for i, klingon_ship in enumerate(self.world.quadrant.klingon_ships): + if klingon_ship.shield <= 0: + continue + + h = int((klingon_ship.shield / self.fnd(i)) * (random.random() + 2)) + ship.shields -= h + klingon_ship.shield /= random.random() + 3 + print(f" {h} UNIT HIT ON ENTERPRISE FROM SECTOR {klingon_ship.sector} ") + if ship.shields <= 0: + self.end_game(won=False, quit=False, enterprise_killed=True) + return + print(f" ") + if h >= 20 and random.random() < 0.60 and h / ship.shields > 0.02: + device = fnr() + ship.damage_stats[device] -= h / ship.shields + 0.5 * random.random() + print( + f"DAMAGE CONTROL REPORTS '{ship.devices[device]} DAMAGED BY THE HIT'" + ) + + def phaser_control(self) -> None: + """Take phaser control input and fire phasers.""" + world = self.world + klingon_ships = world.quadrant.klingon_ships + ship = world.ship + + if ship.damage_stats[3] < 0: + print("PHASERS INOPERATIVE") + return + + if self.world.quadrant.nb_klingons <= 0: + print("SCIENCE OFFICER SPOCK REPORTS 'SENSORS SHOW NO ENEMY SHIPS") + print(" IN THIS QUADRANT'") + return + + if ship.damage_stats[7] < 0: + print("COMPUTER FAILURE HAMPERS ACCURACY") + + print(f"PHASERS LOCKED ON TARGET; ENERGY AVAILABLE = {ship.energy} UNITS") + phaser_firepower: float = 0 + while True: + while True: + units_to_fire = input("NUMBER OF UNITS TO FIRE? ") + if len(units_to_fire) > 0: + phaser_firepower = int(units_to_fire) + break + if phaser_firepower <= 0: + return + if ship.energy >= phaser_firepower: + break + print(f"ENERGY AVAILABLE = {ship.energy} UNITS") + + ship.energy -= phaser_firepower + if ship.damage_stats[7] < 0: # bug in original, was d[6] + phaser_firepower *= random.random() + + phaser_per_klingon = int(phaser_firepower / self.world.quadrant.nb_klingons) + for i, klingon_ship in enumerate(klingon_ships): + if klingon_ship.shield <= 0: + continue + + h = int((phaser_per_klingon / self.fnd(i)) * (random.random() + 2)) + if h <= 0.15 * klingon_ship.shield: + print(f"SENSORS SHOW NO DAMAGE TO ENEMY AT {klingon_ship.sector}") + else: + klingon_ship.shield -= h + print(f" {h} UNIT HIT ON KLINGON AT SECTOR {klingon_ship.sector}") + if klingon_ship.shield <= 0: + print("*** KLINGON DESTROYED ***") + self.world.quadrant.nb_klingons -= 1 + world.remaining_klingons -= 1 + world.quadrant.set_value( + klingon_ship.sector.x, klingon_ship.sector.y, Entity.void + ) + klingon_ship.shield = 0 + world.galaxy_map[ship.position.quadrant.x][ + ship.position.quadrant.y + ].klingons -= 1 + world.charted_galaxy_map[ship.position.quadrant.x][ + ship.position.quadrant.y + ] = world.galaxy_map[ship.position.quadrant.x][ + ship.position.quadrant.y + ] + if world.remaining_klingons <= 0: + self.end_game(won=True, quit=False) + return + else: + print( + f" (SENSORS SHOW {round(klingon_ship.shield,6)} UNITS REMAINING)" + ) + + self.klingons_fire() + + def photon_torpedoes(self) -> None: + """Take photon torpedo input and process firing of torpedoes.""" + world = self.world + klingon_ships = world.quadrant.klingon_ships + ship = world.ship + + if ship.torpedoes <= 0: + print("ALL PHOTON TORPEDOES EXPENDED") + return + if ship.damage_stats[4] < 0: + print("PHOTON TUBES ARE NOT OPERATIONAL") + return + + cd = get_user_float("PHOTON TORPEDO COURSE (1-9)? ") + if cd == 9: + cd = 1 + if cd < 1 or cd >= 9: + print("ENSIGN CHEKOV REPORTS, 'INCORRECT COURSE DATA, SIR!'") + return + + cdi = int(cd) + + # Interpolate direction: + dx = dirs[cdi - 1][0] + (dirs[cdi][0] - dirs[cdi - 1][0]) * (cd - cdi) + dy = dirs[cdi - 1][1] + (dirs[cdi][1] - dirs[cdi - 1][1]) * (cd - cdi) + + ship.energy -= 2 + ship.torpedoes -= 1 + + # Exact position + x: float = ship.position.sector.x + y: float = ship.position.sector.y + + # Rounded position (to coordinates) + torpedo_x, torpedo_y = x, y + print("TORPEDO TRACK:") + while True: + x += dx + y += dy + torpedo_x, torpedo_y = round(x), round(y) + if torpedo_x < 0 or torpedo_x > 7 or torpedo_y < 0 or torpedo_y > 7: + print("TORPEDO MISSED") + self.klingons_fire() + return + print(f" {torpedo_x + 1} , {torpedo_y + 1}") + if world.quadrant.get_value(torpedo_x, torpedo_y) != Entity.void: + break + + if world.quadrant.get_value(torpedo_x, torpedo_y) == Entity.klingon: + print("*** KLINGON DESTROYED ***") + self.world.quadrant.nb_klingons -= 1 + world.remaining_klingons -= 1 + if world.remaining_klingons <= 0: + self.end_game(won=True, quit=False) + return + for klingon_ship in klingon_ships: + if ( + torpedo_x == klingon_ship.sector.x + and torpedo_y == klingon_ship.sector.y + ): + klingon_ship.shield = 0 + elif world.quadrant.get_value(torpedo_x, torpedo_y) == Entity.star: + print(f"STAR AT {torpedo_x + 1} , {torpedo_y + 1} ABSORBED TORPEDO ENERGY.") + self.klingons_fire() + return + elif world.quadrant.get_value(torpedo_x, torpedo_y) == Entity.starbase: + print("*** STARBASE DESTROYED ***") + self.world.quadrant.nb_bases -= 1 + world.bases_in_galaxy -= 1 + if ( + world.bases_in_galaxy == 0 + and world.remaining_klingons + <= world.stardate - world.initial_stardate - world.mission_duration + ): + print("THAT DOES IT, CAPTAIN!! YOU ARE HEREBY RELIEVED OF COMMAND") + print("AND SENTENCED TO 99 STARDATES AT HARD LABOR ON CYGNUS 12!!") + self.end_game(won=False) + return + print("STARFLEET COMMAND REVIEWING YOUR RECORD TO CONSIDER") + print("COURT MARTIAL!") + ship.docked = False + + world.quadrant.set_value(torpedo_x, torpedo_y, Entity.void) + world.galaxy_map[ship.position.quadrant.x][ + ship.position.quadrant.y + ] = QuadrantData( + self.world.quadrant.nb_klingons, + self.world.quadrant.nb_bases, + self.world.quadrant.nb_stars, + ) + world.charted_galaxy_map[ship.position.quadrant.x][ + ship.position.quadrant.y + ] = world.galaxy_map[ship.position.quadrant.x][ship.position.quadrant.y] + self.klingons_fire() def short_range_scan(self) -> None: """Print a short range scan.""" - self.docked = False - for i in (s1 - 1, s1, s1 + 1): - for j in (s2 - 1, s2, s2 + 1): - if 0 <= i <= 7 and 0 <= j <= 7 and compare_marker(i, j, ">!<"): - self.docked = True + self.world.ship.docked = False + ship = self.world.ship + for x in ( + ship.position.sector.x - 1, + ship.position.sector.x, + ship.position.sector.x + 1, + ): + for y in ( + ship.position.sector.y - 1, + ship.position.sector.y, + ship.position.sector.y + 1, + ): + if ( + 0 <= x <= 7 + and 0 <= y <= 7 + and self.world.quadrant.get_value(x, y) == Entity.starbase + ): + ship.docked = True cs = "DOCKED" - self.energy, self.torpedoes = initial_energy, initial_torpedoes + ship.refill() print("SHIELDS DROPPED FOR DOCKING PURPOSES") - self.shields = 0 + ship.shields = 0 break else: continue break else: - if k3 > 0: + if self.world.quadrant.nb_klingons > 0: cs = "*RED*" - elif self.energy < initial_energy * 0.1: + elif ship.energy < Ship.energy_capacity * 0.1: cs = "YELLOW" else: cs = "GREEN" - if self.damage_stats[1] < 0: + if ship.damage_stats[1] < 0: print("\n*** SHORT RANGE SENSORS ARE OUT ***\n") return sep = "---------------------------------" print(sep) - for i in range(8): + for x in range(8): line = "" - for j in range(8): - pos = i * 24 + j * 3 - line = line + " " + quadrant_string[pos : (pos + 3)] + for y in range(8): + line = line + " " + self.world.quadrant.data[x][y].value - if i == 0: - line += f" STARDATE {round(int(current_stardate * 10) * 0.1, 1)}" - elif i == 1: + if x == 0: + line += f" STARDATE {round(int(self.world.stardate * 10) * 0.1, 1)}" + elif x == 1: line += f" CONDITION {cs}" - elif i == 2: - line += f" QUADRANT {q1 + 1} , {q2 + 1}" - elif i == 3: - line += f" SECTOR {s1 + 1} , {s2 + 1}" - elif i == 4: - line += f" PHOTON TORPEDOES {int(self.torpedoes)}" - elif i == 5: - line += f" TOTAL ENERGY {int(self.energy + self.shields)}" - elif i == 6: - line += f" SHIELDS {int(self.shields)}" + elif x == 2: + line += f" QUADRANT {ship.position.quadrant}" + elif x == 3: + line += f" SECTOR {ship.position.sector}" + elif x == 4: + line += f" PHOTON TORPEDOES {int(ship.torpedoes)}" + elif x == 5: + line += f" TOTAL ENERGY {int(ship.energy + ship.shields)}" + elif x == 6: + line += f" SHIELDS {int(ship.shields)}" else: - line += f" KLINGONS REMAINING {total_klingons}" + line += f" KLINGONS REMAINING {self.world.remaining_klingons}" print(line) print(sep) + def long_range_scan(self) -> None: + """Print a long range scan.""" + if self.world.ship.damage_stats[2] < 0: + print("LONG RANGE SENSORS ARE INOPERABLE") + return -# Global variables -restart = False -klingons: List[List[float]] = [ - [0, 0, 0], - [0, 0, 0], - [0, 0, 0], -] # Klingons in current quadrant -c = [ - [0, 1], - [-1, 1], - [-1, 0], - [-1, -1], - [0, -1], - [1, -1], - [1, 0], - [1, 1], - [0, 1], + print(f"LONG RANGE SCAN FOR QUADRANT {self.world.ship.position.quadrant}") + print_scan_results( + self.world.ship.position.quadrant, + self.world.galaxy_map, + self.world.charted_galaxy_map, + ) + + def navigation(self) -> None: + """ + Take navigation input and move the Enterprise. + + 1/8 warp goes 1 sector in the direction dirs[course] + """ + world = self.world + ship = world.ship + + cd = get_user_float("COURSE (1-9)? ") - 1 # Convert to 0-8 + if cd == len(dirs) - 1: + cd = 0 + if cd < 0 or cd >= len(dirs): + print(" LT. SULU REPORTS, 'INCORRECT COURSE DATA, SIR!'") + return + + warp = get_user_float( + f"WARP FACTOR (0-{'0.2' if ship.damage_stats[0] < 0 else '8'})? " + ) + if ship.damage_stats[0] < 0 and warp > 0.2: + print("WARP ENGINES ARE DAMAGED. MAXIMUM SPEED = WARP 0.2") + return + if warp == 0: + return + if warp < 0 or warp > 8: + print( + f" CHIEF ENGINEER SCOTT REPORTS 'THE ENGINES WON'T TAKE WARP {warp}!'" + ) + return + + warp_rounds = round(warp * 8) + if ship.energy < warp_rounds: + print("ENGINEERING REPORTS 'INSUFFICIENT ENERGY AVAILABLE") + print(f" FOR MANEUVERING AT WARP {warp}!'") + if ship.shields >= warp_rounds - ship.energy and ship.damage_stats[6] >= 0: + print( + f"DEFLECTOR CONTROL ROOM ACKNOWLEDGES {ship.shields} UNITS OF ENERGY" + ) + print(" PRESENTLY DEPLOYED TO SHIELDS.") + return + + # klingons move and fire + for klingon_ship in self.world.quadrant.klingon_ships: + if klingon_ship.shield != 0: + world.quadrant.set_value( + klingon_ship.sector.x, klingon_ship.sector.y, Entity.void + ) + ( + klingon_ship.sector.x, + klingon_ship.sector.y, + ) = world.quadrant.find_empty_place() + world.quadrant.set_value( + klingon_ship.sector.x, klingon_ship.sector.y, Entity.klingon + ) + + self.klingons_fire() + + # repair damaged devices and print damage report + line = "" + for i in range(8): + if ship.damage_stats[i] < 0: + ship.damage_stats[i] += min(warp, 1) + if -0.1 < ship.damage_stats[i] < 0: + ship.damage_stats[i] = -0.1 + elif ship.damage_stats[i] >= 0: + if len(line) == 0: + line = "DAMAGE CONTROL REPORT:" + line += f" {ship.devices[i]} REPAIR COMPLETED\n" + if len(line) > 0: + print(line) + if random.random() <= 0.2: + device = fnr() + if random.random() < 0.6: + ship.damage_stats[device] -= random.random() * 5 + 1 + print(f"DAMAGE CONTROL REPORT: {ship.devices[device]} DAMAGED\n") + else: + ship.damage_stats[device] += random.random() * 3 + 1 + print( + f"DAMAGE CONTROL REPORT: {ship.devices[device]} STATE OF REPAIR IMPROVED\n" + ) + + self.move_ship(warp_rounds, cd) + world.stardate += 0.1 * int(10 * warp) if warp < 1 else 1 + if world.has_mission_ended(): + self.end_game(won=False, quit=False) + return + + self.short_range_scan() + + def move_ship(self, warp_rounds: int, cd: float) -> None: + assert cd >= 0 + assert cd < len(dirs) - 1 + # cd is the course data which points to 'dirs' + world = self.world + ship = self.world.ship + world.quadrant.set_value( + int(ship.position.sector.x), int(ship.position.sector.y), Entity.void + ) + cdi = int(cd) + + # Interpolate direction: + dx = dirs[cdi][0] + (dirs[cdi + 1][0] - dirs[cdi][0]) * (cd - cdi) + dy = dirs[cdi][1] + (dirs[cdi + 1][1] - dirs[cdi][1]) * (cd - cdi) + + start_quadrant = Point(ship.position.quadrant.x, ship.position.quadrant.y) + sector_start_x: float = ship.position.sector.x + sector_start_y: float = ship.position.sector.y + + for _ in range(warp_rounds): + ship.position.sector.x += dx # type: ignore + ship.position.sector.y += dy # type: ignore + + if ( + ship.position.sector.x < 0 + or ship.position.sector.x > 7 + or ship.position.sector.y < 0 + or ship.position.sector.y > 7 + ): + # exceeded quadrant limits; calculate final position + sector_start_x += ship.position.quadrant.x * 8 + warp_rounds * dx + sector_start_y += ship.position.quadrant.y * 8 + warp_rounds * dy + ship.position.quadrant.x = int(sector_start_x / 8) + ship.position.quadrant.y = int(sector_start_y / 8) + ship.position.sector.x = int( + sector_start_x - ship.position.quadrant.x * 8 + ) + ship.position.sector.y = int( + sector_start_y - ship.position.quadrant.y * 8 + ) + if ship.position.sector.x < 0: + ship.position.quadrant.x -= 1 + ship.position.sector.x = 7 + if ship.position.sector.y < 0: + ship.position.quadrant.y -= 1 + ship.position.sector.y = 7 + + hit_edge = False + if ship.position.quadrant.x < 0: + hit_edge = True + ship.position.quadrant.x = ship.position.sector.x = 0 + if ship.position.quadrant.x > 7: + hit_edge = True + ship.position.quadrant.x = ship.position.sector.x = 7 + if ship.position.quadrant.y < 0: + hit_edge = True + ship.position.quadrant.y = ship.position.sector.y = 0 + if ship.position.quadrant.y > 7: + hit_edge = True + ship.position.quadrant.y = ship.position.sector.y = 7 + if hit_edge: + print("LT. UHURA REPORTS MESSAGE FROM STARFLEET COMMAND:") + print(" 'PERMISSION TO ATTEMPT CROSSING OF GALACTIC PERIMETER") + print(" IS HEREBY *DENIED*. SHUT DOWN YOUR ENGINES.'") + print("CHIEF ENGINEER SCOTT REPORTS 'WARP ENGINES SHUT DOWN") + print( + f" AT SECTOR {ship.position.sector} OF " + f"QUADRANT {ship.position.quadrant}.'" + ) + if world.has_mission_ended(): + self.end_game(won=False, quit=False) + return + + stayed_in_quadrant = ( + ship.position.quadrant.x == start_quadrant.x + and ship.position.quadrant.y == start_quadrant.y + ) + if stayed_in_quadrant: + break + world.stardate += 1 + ship.maneuver_energy(warp_rounds) + self.new_quadrant() + return + ship_sector = self.world.ship.position.sector + ship_x = int(ship_sector.x) + ship_y = int(ship_sector.y) + if self.world.quadrant.data[ship_x][ship_y] != Entity.void: + ship_sector.x = int(ship_sector.x - dx) + ship_sector.y = int(ship_sector.y - dy) + print( + "WARP ENGINES SHUT DOWN AT SECTOR " + f"{ship_sector} DUE TO BAD NAVIGATION" + ) + break + else: + ship.position.sector.x, ship.position.sector.y = int( + ship.position.sector.x + ), int(ship.position.sector.y) + + world.quadrant.set_value( + int(ship.position.sector.x), int(ship.position.sector.y), Entity.ship + ) + ship.maneuver_energy(warp_rounds) + + def damage_control(self) -> None: + """Print a damage control report.""" + ship = self.world.ship + + if ship.damage_stats[5] < 0: + print("DAMAGE CONTROL REPORT NOT AVAILABLE") + else: + print("\nDEVICE STATE OF REPAIR") + for r1 in range(8): + print( + f"{ship.devices[r1].ljust(26, ' ')}{int(ship.damage_stats[r1] * 100) * 0.01:g}" + ) + print() + + if not ship.docked: + return + + damage_sum = sum(0.1 for i in range(8) if ship.damage_stats[i] < 0) + if damage_sum == 0: + return + + damage_sum += self.world.quadrant.delay_in_repairs_at_base + if damage_sum >= 1: + damage_sum = 0.9 + print("\nTECHNICIANS STANDING BY TO EFFECT REPAIRS TO YOUR SHIP;") + print( + f"ESTIMATED TIME TO REPAIR: {round(0.01 * int(100 * damage_sum), 2)} STARDATES" + ) + if input("WILL YOU AUTHORIZE THE REPAIR ORDER (Y/N)? ").upper().strip() != "Y": + return + + for i in range(8): + if ship.damage_stats[i] < 0: + ship.damage_stats[i] = 0 + self.world.stardate += damage_sum + 0.1 + + def computer(self) -> None: + """Perform the various functions of the library computer.""" + world = self.world + ship = world.ship + + if ship.damage_stats[7] < 0: + print("COMPUTER DISABLED") + return + + while True: + command = input("COMPUTER ACTIVE AND AWAITING COMMAND? ") + if len(command) == 0: + com = 6 + else: + com = int(command) + if com < 0: + return + + print() + + if com in [0, 5]: + if com == 5: + print(" THE GALAXY") + else: + print( + "\n COMPUTER RECORD OF GALAXY FOR " + f"QUADRANT {ship.position.quadrant}\n" + ) + + print(" 1 2 3 4 5 6 7 8") + sep = " ----- ----- ----- ----- ----- ----- ----- -----" + print(sep) + + for i in range(8): + line = " " + str(i + 1) + " " + + if com == 5: + g2s = Quadrant.quadrant_name(i, 0, True) + line += (" " * int(12 - 0.5 * len(g2s))) + g2s + g2s = Quadrant.quadrant_name(i, 4, True) + line += (" " * int(39 - 0.5 * len(g2s) - len(line))) + g2s + else: + for j in range(8): + line += " " + if world.charted_galaxy_map[i][j].num() == 0: + line += "***" + else: + line += str( + world.charted_galaxy_map[i][j].num() + 1000 + )[-3:] + + print(line) + print(sep) + + print() + elif com == 1: + print(" STATUS REPORT:") + print( + f"KLINGON{'S' if world.remaining_klingons > 1 else ''} LEFT: {world.remaining_klingons}" + ) + print( + "MISSION MUST BE COMPLETED IN " + f"{round(0.1 * int(world.remaining_time() * 10), 1)} STARDATES" + ) + + if world.bases_in_galaxy == 0: + print("YOUR STUPIDITY HAS LEFT YOU ON YOUR OWN IN") + print(" THE GALAXY -- YOU HAVE NO STARBASES LEFT!") + else: + print( + f"THE FEDERATION IS MAINTAINING {world.bases_in_galaxy} " + f"STARBASE{'S' if world.bases_in_galaxy > 1 else ''} IN THE GALAXY" + ) + + self.damage_control() + elif com == 2: + if self.world.quadrant.nb_klingons <= 0: + print( + "SCIENCE OFFICER SPOCK REPORTS 'SENSORS SHOW NO ENEMY " + "SHIPS\n" + " IN THIS QUADRANT'" + ) + return + + print( + f"FROM ENTERPRISE TO KLINGON BATTLE CRUISER{'S' if self.world.quadrant.nb_klingons > 1 else ''}" + ) + + for klingon_ship in self.world.quadrant.klingon_ships: + if klingon_ship.shield > 0: + print_direction( + Point(ship.position.sector.x, ship.position.sector.y), + Point( + int(klingon_ship.sector.x), + int(klingon_ship.sector.y), + ), + ) + elif com == 3: + if self.world.quadrant.nb_bases == 0: + print( + "MR. SPOCK REPORTS, 'SENSORS SHOW NO STARBASES IN THIS " + "QUADRANT.'" + ) + return + + print("FROM ENTERPRISE TO STARBASE:") + print_direction( + Point(ship.position.sector.x, ship.position.sector.y), + self.world.quadrant.starbase, + ) + elif com == 4: + print("DIRECTION/DISTANCE CALCULATOR:") + print( + f"YOU ARE AT QUADRANT {ship.position.quadrant} " + f"SECTOR {ship.position.sector}" + ) + print("PLEASE ENTER") + while True: + coordinates = input(" INITIAL COORDINATES (X,Y)? ").split(",") + if len(coordinates) == 2: + from1, from2 = int(coordinates[0]) - 1, int(coordinates[1]) - 1 + if 0 <= from1 <= 7 and 0 <= from2 <= 7: + break + while True: + coordinates = input(" FINAL COORDINATES (X,Y)? ").split(",") + if len(coordinates) == 2: + to1, to2 = int(coordinates[0]) - 1, int(coordinates[1]) - 1 + if 0 <= to1 <= 7 and 0 <= to2 <= 7: + break + print_direction(Point(from1, from2), Point(to1, to2)) + else: + print( + "FUNCTIONS AVAILABLE FROM LIBRARY-COMPUTER:\n" + " 0 = CUMULATIVE GALACTIC RECORD\n" + " 1 = STATUS REPORT\n" + " 2 = PHOTON TORPEDO DATA\n" + " 3 = STARBASE NAV DATA\n" + " 4 = DIRECTION/DISTANCE CALCULATOR\n" + " 5 = GALAXY 'REGION NAME' MAP\n" + ) + + def end_game( + self, won: bool = False, quit: bool = True, enterprise_killed: bool = False + ) -> None: + """Handle end-of-game situations.""" + if won: + print("CONGRATULATIONS, CAPTAIN! THE LAST KLINGON BATTLE CRUISER") + print("MENACING THE FEDERATION HAS BEEN DESTROYED.\n") + print( + f"YOUR EFFICIENCY RATING IS {round(1000 * (self.world.remaining_klingons / (self.world.stardate - self.world.initial_stardate))**2, 4)}\n\n" + ) + else: + if not quit: + if enterprise_killed: + print( + "\nTHE ENTERPRISE HAS BEEN DESTROYED. THE FEDERATION " + "WILL BE CONQUERED." + ) + print(f"IT IS STARDATE {round(self.world.stardate, 1)}") + + print( + f"THERE WERE {self.world.remaining_klingons} KLINGON BATTLE CRUISERS LEFT AT" + ) + print("THE END OF YOUR MISSION.\n\n") + + if self.world.bases_in_galaxy == 0: + sys.exit() + + print("THE FEDERATION IS IN NEED OF A NEW STARSHIP COMMANDER") + print("FOR A SIMILAR MISSION -- IF THERE IS A VOLUNTEER,") + if input("LET HIM STEP FORWARD AND ENTER 'AYE'? ").upper().strip() != "AYE": + sys.exit() + self.restart = True + + +klingon_shield_strength: Final = 200 +# 8 sectors = 1 quadrant +dirs: Final = [ # (down-up, left,right) + [0, 1], # 1: go right (same as #9) + [-1, 1], # 2: go up-right + [-1, 0], # 3: go up (lower x-coordines; north) + [-1, -1], # 4: go up-left (north-west) + [0, -1], # 5: go left (west) + [1, -1], # 6: go down-left (south-west) + [1, 0], # 7: go down (higher x-coordines; south) + [1, 1], # 8: go down-right + [0, 1], # 9: go right (east) ] # vectors in cardinal directions -q1 = s1 = 0 -q2 = s2 = 0 -k3 = b3 = s3 = 0 # Klingons, bases, stars in quad. -b4 = b5 = 0 -quadrant_string = " " * 192 # quadrant string -# set up global game variables -galaxy_map = [[0] * 8 for _ in range(8)] # galaxy map -charted_galaxy_map = [[0] * 8 for _ in range(8)] # charted galaxy map -current_stardate: float -current_stardate = initial_stardate = 100 * random.randint( - 20, 39 -) # stardate (current, initial) -mission_duration = random.randint(25, 34) # mission duration (stardates) -initial_energy = 3000 -initial_torpedoes = 10 -ship = Ship(energy=initial_energy, torpedoes=initial_torpedoes) -total_klingons, bases_in_galaxy = 0, 0 # total Klingons, bases in galaxy -# ^ bug in original, was b9 = 2 -klingon_shield_strength = 200 # avg. Klingon shield strength -current_klingons = total_klingons # Klingons at start of game -delay_in_repairs_at_base = 0.5 * random.random() # extra delay in repairs at base - -# ------------------------------------------------------------------------- -# Utility functions -# ------------------------------------------------------------------------- def fnr() -> int: """Generate a random integer from 0 to 7 inclusive.""" return random.randint(0, 7) -def quadrant_name(row: int, col: int, region_only: bool = False) -> str: - """Return quadrant name visible on scans, etc.""" - region1 = [ - "ANTARES", - "RIGEL", - "PROCYON", - "VEGA", - "CANOPUS", - "ALTAIR", - "SAGITTARIUS", - "POLLUX", - ] - region2 = [ - "SIRIUS", - "DENEB", - "CAPELLA", - "BETELGEUSE", - "ALDEBARAN", - "REGULUS", - "ARCTURUS", - "SPICA", - ] - modifier = ["I", "II", "III", "IV"] - - quadrant = region1[row] if col < 4 else region2[row] - - if not region_only: - quadrant += " " + modifier[col % 4] - - return quadrant - - -def insert_marker(row: float, col: float, marker: str) -> None: - """ - Insert a marker into a given position in the quadrant string `qs`. The - contents of a quadrant (Enterprise, stars, etc.) are stored in `qs`. - """ - global quadrant_string - - if len(marker) != 3: - print("ERROR") - exit() - - pos = round(col) * 3 + round(row) * 24 - quadrant_string = quadrant_string[0:pos] + marker + quadrant_string[(pos + 3) : 192] - - -def compare_marker(row: float, col: float, test_marker: str) -> bool: - """ - Check whether the position in the current quadrant is occupied with a - given marker. - """ - pos = round(col) * 3 + round(row) * 24 - return quadrant_string[pos : (pos + 3)] == test_marker - - -def find_empty_place() -> Tuple[int, int]: - """Find an empty location in the current quadrant.""" - while True: - row, col = fnr(), fnr() - if compare_marker(row, col, " "): - return row, col - - -# ------------------------------------------------------------------------- -# Functions for individual player commands -# ------------------------------------------------------------------------- -def navigation() -> None: - """Take navigation input and move the Enterprise.""" - global ship, klingons, quadrant_string, current_stardate, q1, q2, s1, s2 - - while True: - c1s = input("COURSE (1-9)? ") - if len(c1s) > 0: - c1 = float(c1s) - break - if c1 == 9: - c1 = 1 - if c1 < 1 or c1 >= 9: - print(" LT. SULU REPORTS, 'INCORRECT COURSE DATA, SIR!'") - return - - while True: - warps = input(f"WARP FACTOR (0-{'0.2' if ship.damage_stats[0] < 0 else '8'})? ") - if len(warps) > 0: - warp = float(warps) - break - if ship.damage_stats[0] < 0 and warp > 0.2: - print("WARP ENGINES ARE DAMAGED. MAXIMUM SPEED = WARP 0.2") - return - if warp == 0: - return - if warp < 0 or warp > 8: - print(f" CHIEF ENGINEER SCOTT REPORTS 'THE ENGINES WON'T TAKE WARP {warp}!'") - return - - n = round(warp * 8) - if ship.energy < n: - print("ENGINEERING REPORTS 'INSUFFICIENT ENERGY AVAILABLE") - print(f" FOR MANEUVERING AT WARP {warp}!'") - if ship.shields >= n - ship.energy and ship.damage_stats[6] >= 0: - print(f"DEFLECTOR CONTROL ROOM ACKNOWLEDGES {ship.shields} UNITS OF ENERGY") - print(" PRESENTLY DEPLOYED TO SHIELDS.") - return - - # klingons move and fire - for i in range(3): - if klingons[i][2] != 0: - insert_marker(klingons[i][0], klingons[i][1], " ") - klingons[i][0], klingons[i][1] = find_empty_place() - insert_marker(klingons[i][0], klingons[i][1], "+K+") - - klingons_fire() - - # repair damaged devices and print damage report - line = "" - for i in range(8): - if ship.damage_stats[i] < 0: - ship.damage_stats[i] += min(warp, 1) - if -0.1 < ship.damage_stats[i] < 0: - ship.damage_stats[i] = -0.1 - elif ship.damage_stats[i] >= 0: - if len(line) == 0: - line = "DAMAGE CONTROL REPORT:" - line += " " + ship.devices[i] + " REPAIR COMPLETED\n" - if len(line) > 0: - print(line) - if random.random() <= 0.2: - r1 = fnr() - if random.random() < 0.6: - ship.damage_stats[r1] -= random.random() * 5 + 1 - print(f"DAMAGE CONTROL REPORT: {ship.devices[r1]} DAMAGED\n") - else: - ship.damage_stats[r1] += random.random() * 3 + 1 - print( - f"DAMAGE CONTROL REPORT: {ship.devices[r1]} STATE OF REPAIR IMPROVED\n" - ) - - # begin moving starship - insert_marker(int(s1), int(s2), " ") - ic1 = int(c1) - x1 = c[ic1 - 1][0] + (c[ic1][0] - c[ic1 - 1][0]) * (c1 - ic1) - x2 = c[ic1 - 1][1] + (c[ic1][1] - c[ic1 - 1][1]) * (c1 - ic1) - q1_start, q2_start = q1, q2 - x, y = s1, s2 - - for _ in range(n): - s1 += x1 # type: ignore - s2 += x2 # type: ignore - - if s1 < 0 or s1 > 7 or s2 < 0 or s2 > 7: - # exceeded quadrant limits; calculate final position - x += 8 * q1 + n * x1 # type: ignore - y += 8 * q2 + n * x2 # type: ignore - q1, q2 = int(x / 8), int(y / 8) - s1, s2 = int(x - q1 * 8), int(y - q2 * 8) - if s1 < 0: - q1 -= 1 - s1 = 7 - if s2 < 0: - q2 -= 1 - s2 = 7 - - hit_edge = False - if q1 < 0: - hit_edge = True - q1 = s1 = 0 - if q1 > 7: - hit_edge = True - q1 = s1 = 7 - if q2 < 0: - hit_edge = True - q2 = s2 = 0 - if q2 > 7: - hit_edge = True - q2 = s2 = 7 - if hit_edge: - print("LT. UHURA REPORTS MESSAGE FROM STARFLEET COMMAND:") - print(" 'PERMISSION TO ATTEMPT CROSSING OF GALACTIC PERIMETER") - print(" IS HEREBY *DENIED*. SHUT DOWN YOUR ENGINES.'") - print("CHIEF ENGINEER SCOTT REPORTS 'WARP ENGINES SHUT DOWN") - print( - f" AT SECTOR {s1 + 1} , {s2 + 1} OF QUADRANT " - f"{q1 + 1} , {q2 + 1}.'" - ) - if current_stardate > initial_stardate + mission_duration: - end_game(won=False, quit=False) - return - - if q1 == q1_start and q2 == q2_start: - break - current_stardate += 1 - ship.maneuver_energy(n) - new_quadrant() - return - else: - pos = int(s1) * 24 + int(s2) * 3 - if quadrant_string[pos : (pos + 2)] != " ": - s1, s2 = int(s1 - x1), int(s2 - x2) - print( - "WARP ENGINES SHUT DOWN AT SECTOR " - f"{s1 + 1} , {s2 + 1} DUE TO BAD NAVAGATION" - ) - break - else: - s1, s2 = int(s1), int(s2) - - insert_marker(int(s1), int(s2), "<*>") - ship.maneuver_energy(n) - - current_stardate += 0.1 * int(10 * warp) if warp < 1 else 1 - if current_stardate > initial_stardate + mission_duration: - end_game(won=False, quit=False) - return - - ship.short_range_scan() - - -def long_range_scan() -> None: - """Print a long range scan.""" - global charted_galaxy_map, galaxy_map - - if ship.damage_stats[2] < 0: - print("LONG RANGE SENSORS ARE INOPERABLE") - return - - print(f"LONG RANGE SCAN FOR QUADRANT {q1 + 1} , {q2 + 1}") - print_scan_results(q1, q2, galaxy_map, charted_galaxy_map) - - def print_scan_results( - q1: int, q2: int, g: List[List[Any]], z: List[List[Any]] + quadrant: Point, + galaxy_map: List[List[QuadrantData]], + charted_galaxy_map: List[List[QuadrantData]], ) -> None: sep = "-------------------" print(sep) - for i in (q1 - 1, q1, q1 + 1): - n = [-1, -2, -3] + for x in (quadrant.x - 1, quadrant.x, quadrant.x + 1): + n: List[Optional[int]] = [None, None, None] - for j in (q2 - 1, q2, q2 + 1): - if 0 <= i <= 7 and 0 <= j <= 7: - n[j - q2 + 1] = g[i][j] - z[i][j] = g[i][j] + # Reveal parts of the current map + for y in (quadrant.y - 1, quadrant.y, quadrant.y + 1): + if 0 <= x <= 7 and 0 <= y <= 7: + n[y - quadrant.y + 1] = galaxy_map[x][y].num() + charted_galaxy_map[x][y] = galaxy_map[x][y] line = ": " - for line_index in range(3): - if n[line_index] < 0: + for line_col in n: + if line_col is None: line += "*** : " else: - line += str(n[line_index] + 1000).rjust(4, " ")[-3:] + " : " + line += str(line_col + 1000).rjust(4, " ")[-3:] + " : " print(line) print(sep) -def phaser_control() -> None: - """Take phaser control input and fire phasers.""" - global ship, klingons, galaxy_map, charted_galaxy_map, k3, total_klingons - - if ship.damage_stats[3] < 0: - print("PHASERS INOPERATIVE") - return - - if k3 <= 0: - print("SCIENCE OFFICER SPOCK REPORTS 'SENSORS SHOW NO ENEMY SHIPS") - print(" IN THIS QUADRANT'") - return - - if ship.damage_stats[7] < 0: - print("COMPUTER FAILURE HAMPERS ACCURACY") - - print(f"PHASERS LOCKED ON TARGET; ENERGY AVAILABLE = {ship.energy} UNITS") - x = 0 - while True: - while True: - units_to_fire = input("NUMBER OF UNITS TO FIRE? ") - if len(units_to_fire) > 0: - x = int(units_to_fire) - break - if x <= 0: - return - if ship.energy >= x: - break - print(f"ENERGY AVAILABLE = {ship.energy} UNITS") - - ship.energy -= x - if ship.damage_stats[7] < 0: # bug in original, was d[6] - x *= random.random() # type: ignore - - h1 = int(x / k3) - for i in range(3): - if klingons[i][2] <= 0: - continue - - h = int((h1 / fnd(i)) * (random.random() + 2)) - if h <= 0.15 * klingons[i][2]: - print( - f"SENSORS SHOW NO DAMAGE TO ENEMY AT {klingons[i][0] + 1} , {klingons[i][1] + 1}" - ) - else: - klingons[i][2] -= h - print( - f" {h} UNIT HIT ON KLINGON AT SECTOR {klingons[i][0] + 1} , {klingons[i][1] + 1}" - ) - if klingons[i][2] <= 0: - print("*** KLINGON DESTROYED ***") - k3 -= 1 - total_klingons -= 1 - insert_marker(klingons[i][0], klingons[i][1], " ") - klingons[i][2] = 0 - galaxy_map[q1][q2] -= 100 - charted_galaxy_map[q1][q2] = galaxy_map[q1][q2] - if total_klingons <= 0: - end_game(won=True, quit=False) - return - else: - print(f" (SENSORS SHOW {round(klingons[i][2],6)} UNITS REMAINING)") - - klingons_fire() - - -def photon_torpedoes() -> None: - """Take photon torpedo input and process firing of torpedoes.""" - global ship, k3, total_klingons, klingons, b3, bases_in_galaxy, galaxy_map, charted_galaxy_map - - if ship.torpedoes <= 0: - print("ALL PHOTON TORPEDOES EXPENDED") - return - if ship.damage_stats[4] < 0: - print("PHOTON TUBES ARE NOT OPERATIONAL") - return - - while True: - torpedo_course = input("PHOTON TORPEDO COURSE (1-9)? ") - if len(torpedo_course) > 0: - c1 = float(torpedo_course) - break - if c1 == 9: - c1 = 1 - if c1 < 1 or c1 >= 9: - print("ENSIGN CHEKOV REPORTS, 'INCORRECT COURSE DATA, SIR!'") - return - - ic1 = int(c1) - x1 = c[ic1 - 1][0] + (c[ic1][0] - c[ic1 - 1][0]) * (c1 - ic1) - ship.energy -= 2 - ship.torpedoes -= 1 - x2 = c[ic1 - 1][1] + (c[ic1][1] - c[ic1 - 1][1]) * (c1 - ic1) - x, y = s1, s2 - x3, y3 = x, y - print("TORPEDO TRACK:") - while True: - x += x1 # type: ignore - y += x2 # type: ignore - x3, y3 = round(x), round(y) - if x3 < 0 or x3 > 7 or y3 < 0 or y3 > 7: - print("TORPEDO MISSED") - klingons_fire() - return - print(f" {x3 + 1} , {y3 + 1}") - if not compare_marker(x3, y3, " "): - break - - if compare_marker(x3, y3, "+K+"): - print("*** KLINGON DESTROYED ***") - k3 -= 1 - total_klingons -= 1 - if total_klingons <= 0: - end_game(won=True, quit=False) - return - for i in range(3): - if x3 == klingons[i][0] and y3 == klingons[i][1]: - klingons[i][2] = 0 - elif compare_marker(x3, y3, " * "): - print(f"STAR AT {x3 + 1} , {y3 + 1} ABSORBED TORPEDO ENERGY.") - klingons_fire() - return - elif compare_marker(x3, y3, ">!<"): - print("*** STARBASE DESTROYED ***") - b3 -= 1 - bases_in_galaxy -= 1 - if ( - bases_in_galaxy == 0 - and total_klingons <= current_stardate - initial_stardate - mission_duration - ): - print("THAT DOES IT, CAPTAIN!! YOU ARE HEREBY RELIEVED OF COMMAND") - print("AND SENTENCED TO 99 STARDATES AT HARD LABOR ON CYGNUS 12!!") - end_game(won=False) - return - else: - print("STARFLEET COMMAND REVIEWING YOUR RECORD TO CONSIDER") - print("COURT MARTIAL!") - ship.docked = False - - insert_marker(x3, y3, " ") - galaxy_map[q1][q2] = k3 * 100 + b3 * 10 + s3 - charted_galaxy_map[q1][q2] = galaxy_map[q1][q2] - klingons_fire() - - -def fnd(i: int) -> float: - """Find distance between Enterprise and i'th Klingon warship.""" - return sqrt((klingons[i][0] - s1) ** 2 + (klingons[i][1] - s2) ** 2) - - -def klingons_fire() -> None: - """Process nearby Klingons firing on Enterprise.""" - global ship, klingons - - if k3 <= 0: - return - if ship.docked: - print("STARBASE SHIELDS PROTECT THE ENTERPRISE") - return - - for i in range(3): - if klingons[i][2] <= 0: - continue - - h = int((klingons[i][2] / fnd(i)) * (random.random() + 2)) - ship.shields -= h - klingons[i][2] /= random.random() + 3 - print( - f" {h} UNIT HIT ON ENTERPRISE FROM SECTOR {klingons[i][0] + 1} , {klingons[i][1] + 1}" - ) - if ship.shields <= 0: - end_game(won=False, quit=False, enterprise_killed=True) - return - print(f" ") - if h >= 20 and random.random() < 0.60 and h / ship.shields > 0.02: - r1 = fnr() - ship.damage_stats[r1] -= h / ship.shields + 0.5 * random.random() - print(f"DAMAGE CONTROL REPORTS '{ship.devices[r1]} DAMAGED BY THE HIT'") - - -def damage_control() -> None: - """Print a damage control report.""" - global ship, current_stardate - - if ship.damage_stats[5] < 0: - print("DAMAGE CONTROL REPORT NOT AVAILABLE") - else: - print("\nDEVICE STATE OF REPAIR") - for r1 in range(8): - print( - f"{ship.devices[r1].ljust(26, ' ')}{int(ship.damage_stats[r1] * 100) * 0.01:g}" - ) - print() - - if not ship.docked: - return - - d3 = sum(0.1 for i in range(8) if ship.damage_stats[i] < 0) - if d3 == 0: - return - - d3 += delay_in_repairs_at_base - if d3 >= 1: - d3 = 0.9 - print("\nTECHNICIANS STANDING BY TO EFFECT REPAIRS TO YOUR SHIP;") - print(f"ESTIMATED TIME TO REPAIR: {round(0.01 * int(100 * d3), 2)} STARDATES") - if input("WILL YOU AUTHORIZE THE REPAIR ORDER (Y/N)? ").upper().strip() != "Y": - return - - for i in range(8): - if ship.damage_stats[i] < 0: - ship.damage_stats[i] = 0 - current_stardate += d3 + 0.1 - - -def computer() -> None: - """Perform the various functions of the library computer.""" - global ship, charted_galaxy_map, total_klingons, initial_stardate, mission_duration, current_stardate, bases_in_galaxy, s1, s2, b4, b5 - - if ship.damage_stats[7] < 0: - print("COMPUTER DISABLED") - return - - while True: - command = input("COMPUTER ACTIVE AND AWAITING COMMAND? ") - if len(command) == 0: - com = 6 - else: - com = int(command) - if com < 0: - return - - print() - - if com == 0 or com == 5: - if com == 5: - print(" THE GALAXY") - else: - print( - "\n COMPUTER RECORD OF GALAXY FOR " - f"QUADRANT {q1 + 1} , {q2 + 1}\n" - ) - - print(" 1 2 3 4 5 6 7 8") - sep = " ----- ----- ----- ----- ----- ----- ----- -----" - print(sep) - - for i in range(8): - line = " " + str(i + 1) + " " - - if com == 5: - g2s = quadrant_name(i, 0, True) - line += (" " * int(12 - 0.5 * len(g2s))) + g2s - g2s = quadrant_name(i, 4, True) - line += (" " * int(39 - 0.5 * len(g2s) - len(line))) + g2s - else: - for j in range(8): - line += " " - if charted_galaxy_map[i][j] == 0: - line += "***" - else: - line += str(charted_galaxy_map[i][j] + 1000)[-3:] - - print(line) - print(sep) - - print() - return - elif com == 1: - print(" STATUS REPORT:") - print(f"KLINGON{'S' if total_klingons > 1 else ''} LEFT: {total_klingons}") - print( - "MISSION MUST BE COMPLETED IN " - f"{round(0.1 * int((initial_stardate+mission_duration-current_stardate) * 10), 1)} STARDATES" - ) - - if bases_in_galaxy == 0: - print("YOUR STUPIDITY HAS LEFT YOU ON YOUR OWN IN") - print(" THE GALAXY -- YOU HAVE NO STARBASES LEFT!") - else: - print( - f"THE FEDERATION IS MAINTAINING {bases_in_galaxy} " - f"STARBASE{'S' if bases_in_galaxy > 1 else ''} IN THE GALAXY" - ) - - damage_control() - return - elif com == 2: - if k3 <= 0: - print( - "SCIENCE OFFICER SPOCK REPORTS 'SENSORS SHOW NO ENEMY " - "SHIPS\n" - " IN THIS QUADRANT'" - ) - return - - print(f"FROM ENTERPRISE TO KLINGON BATTLE CRUISER{'S' if k3 > 1 else ''}") - - for i in range(3): - if klingons[i][2] > 0: - print_direction(s1, s2, klingons[i][0], klingons[i][1]) - return - elif com == 3: - if b3 == 0: - print( - "MR. SPOCK REPORTS, 'SENSORS SHOW NO STARBASES IN THIS " - "QUADRANT.'" - ) - return - - print("FROM ENTERPRISE TO STARBASE:") - print_direction(s1, s2, b4, b5) - return - elif com == 4: - print("DIRECTION/DISTANCE CALCULATOR:") - print(f"YOU ARE AT QUADRANT {q1+1} , {q2+1} SECTOR {s1+1} , {s2+1}") - print("PLEASE ENTER") - while True: - coordinates = input(" INITIAL COORDINATES (X,Y)? ").split(",") - if len(coordinates) == 2: - from1, from2 = int(coordinates[0]) - 1, int(coordinates[1]) - 1 - if 0 <= from1 <= 7 and 0 <= from2 <= 7: - break - while True: - coordinates = input(" FINAL COORDINATES (X,Y)? ").split(",") - if len(coordinates) == 2: - to1, to2 = int(coordinates[0]) - 1, int(coordinates[1]) - 1 - if 0 <= to1 <= 7 and 0 <= to2 <= 7: - break - print_direction(from1, from2, to1, to2) - return - else: - print( - "FUNCTIONS AVAILABLE FROM LIBRARY-COMPUTER:\n" - " 0 = CUMULATIVE GALACTIC RECORD\n" - " 1 = STATUS REPORT\n" - " 2 = PHOTON TORPEDO DATA\n" - " 3 = STARBASE NAV DATA\n" - " 4 = DIRECTION/DISTANCE CALCULATOR\n" - " 5 = GALAXY 'REGION NAME' MAP\n" - ) - - -def print_direction(from1: float, from2: float, to1: float, to2: float) -> None: +def print_direction(source: Point, to: Point) -> None: """Print direction and distance between two locations in the grid.""" - delta1 = -(to1 - from1) # flip so positive is up (heading = 3) - delta2 = to2 - from2 + delta1 = -(to.x - source.x) # flip so positive is up (heading = 3) + delta2 = to.y - source.y if delta2 > 0: if delta1 < 0: @@ -811,202 +1138,26 @@ def print_direction(from1: float, from2: float, to1: float, to2: float) -> None: print(f"DISTANCE = {round(sqrt(delta1 ** 2 + delta2 ** 2), 6)}") -# ------------------------------------------------------------------------- -# Game transitions -# ------------------------------------------------------------------------- -def startup() -> None: - """Initialize the game variables and map, and print startup messages.""" - global galaxy_map, charted_galaxy_map, ship, current_stardate - global initial_stardate, mission_duration - global initial_energy, initial_torpedoes - global ship, total_klingons, bases_in_galaxy, klingon_shield_strength, c - global q1, q2, s1, s2, current_klingons - - print( - "\n\n\n\n\n\n\n\n\n\n\n" - " ,------*------,\n" - " ,------------- '--- ------'\n" - " '-------- --' / /\n" - " ,---' '-------/ /--,\n" - " '----------------'\n\n" - " THE USS ENTERPRISE --- NCC-1701\n" - "\n\n\n\n" - ) - - # set up global game variables - galaxy_map = [[0] * 8 for _ in range(8)] # galaxy map - charted_galaxy_map = [[0] * 8 for _ in range(8)] # charted galaxy map - current_stardate = initial_stardate = 100 * random.randint( - 20, 39 - ) # stardate (current, initial) - mission_duration = random.randint(25, 34) # mission duration (stardates) - initial_energy = 3000 - initial_torpedoes = 10 - ship.damage_stats = [0] * 8 # damage stats for devices - ship.energy = initial_energy - ship.docked = False - ship.torpedoes = initial_torpedoes - ship.shields = 0 # shields - total_klingons, bases_in_galaxy = 0, 0 # total Klingons, bases in galaxy - # ^ bug in original, was b9 = 2 - klingon_shield_strength = 200 # avg. Klingon shield strength - - c = [ - [0, 1], - [-1, 1], - [-1, 0], - [-1, -1], - [0, -1], - [1, -1], - [1, 0], - [1, 1], - [0, 1], - ] # vectors in cardinal directions - - # initialize Enterprise's position - q1, q2 = fnr(), fnr() # Enterprise's quadrant - s1, s2 = fnr(), fnr() # ...and sector - - # initialize contents of galaxy - for i in range(8): - for j in range(8): - k3 = 0 - r1 = random.random() - - if r1 > 0.98: - k3 = 3 - elif r1 > 0.95: - k3 = 2 - elif r1 > 0.80: - k3 = 1 - total_klingons += k3 - - b3 = 0 - if random.random() > 0.96: - b3 = 1 - bases_in_galaxy += 1 - galaxy_map[i][j] = k3 * 100 + b3 * 10 + fnr() + 1 - - if total_klingons > mission_duration: - mission_duration = total_klingons + 1 - - if bases_in_galaxy == 0: # original has buggy extra code here - bases_in_galaxy = 1 - galaxy_map[q1][q2] += 10 - q1, q2 = fnr(), fnr() - - current_klingons = total_klingons # Klingons at start of game - - print( - "YOUR ORDERS ARE AS FOLLOWS:\n" - f" DESTROY THE {total_klingons} KLINGON WARSHIPS WHICH HAVE INVADED\n" - " THE GALAXY BEFORE THEY CAN ATTACK FEDERATION HEADQUARTERS\n" - f" ON STARDATE {initial_stardate+mission_duration}. THIS GIVES YOU {mission_duration} DAYS. THERE " - f"{'IS' if bases_in_galaxy == 1 else 'ARE'}\n" - f" {bases_in_galaxy} STARBASE{'' if bases_in_galaxy == 1 else 'S'} IN THE GALAXY FOR " - "RESUPPLYING YOUR SHIP.\n" - ) - - -def new_quadrant() -> None: - """Enter a new quadrant: populate map and print a short range scan.""" - global charted_galaxy_map, k3, b3, s3, delay_in_repairs_at_base, klingons, quadrant_string, b4, b5 - - k3 = b3 = s3 = 0 # Klingons, bases, stars in quad. - delay_in_repairs_at_base = 0.5 * random.random() # extra delay in repairs at base - charted_galaxy_map[q1][q2] = galaxy_map[q1][q2] - - if 0 <= q1 <= 7 and 0 <= q2 <= 7: - quad = quadrant_name(q1, q2, False) - if current_stardate == initial_stardate: - print("\nYOUR MISSION BEGINS WITH YOUR STARSHIP LOCATED") - print(f"IN THE GALACTIC QUADRANT, '{quad}'.\n") - else: - print(f"\nNOW ENTERING {quad} QUADRANT . . .\n") - - k3 = galaxy_map[q1][q2] // 100 - b3 = galaxy_map[q1][q2] // 10 - 10 * k3 - s3 = galaxy_map[q1][q2] - 100 * k3 - 10 * b3 - - if k3 != 0: - print("COMBAT AREA CONDITION RED") - if ship.shields <= 200: - print(" SHIELDS DANGEROUSLY LOW") - - klingons = [[0, 0, 0], [0, 0, 0], [0, 0, 0]] # Klingons in current quadrant - quadrant_string = " " * 192 # quadrant string - - # build quadrant string - insert_marker(s1, s2, "<*>") - for i in range(k3): - r1, r2 = find_empty_place() - insert_marker(r1, r2, "+K+") - klingons[i] = [r1, r2, klingon_shield_strength * (0.5 + random.random())] - if b3 > 0: - b4, b5 = find_empty_place() # position of starbase (sector) - insert_marker(b4, b5, ">!<") - for _ in range(s3): - r1, r2 = find_empty_place() - insert_marker(r1, r2, " * ") - - ship.short_range_scan() - - -def end_game( - won: bool = False, quit: bool = True, enterprise_killed: bool = False -) -> None: - """Handle end-of-game situations.""" - global restart - - if won: - print("CONGRATULATIONS, CAPTAIN! THE LAST KLINGON BATTLE CRUISER") - print("MENACING THE FEDERATION HAS BEEN DESTROYED.\n") - print( - f"YOUR EFFICIENCY RATING IS {round(1000 * (current_klingons / (current_stardate - initial_stardate))**2, 4)}\n\n" - ) - else: - if not quit: - if enterprise_killed: - print( - "\nTHE ENTERPRISE HAS BEEN DESTROYED. THE FEDERATION " - "WILL BE CONQUERED." - ) - print(f"IT IS STARDATE {round(current_stardate, 1)}") - - print(f"THERE WERE {total_klingons} KLINGON BATTLE CRUISERS LEFT AT") - print("THE END OF YOUR MISSION.\n\n") - - if bases_in_galaxy == 0: - exit() - - print("THE FEDERATION IS IN NEED OF A NEW STARSHIP COMMANDER") - print("FOR A SIMILAR MISSION -- IF THERE IS A VOLUNTEER,") - if input("LET HIM STEP FORWARD AND ENTER 'AYE'? ").upper().strip() != "AYE": - exit() - restart = True - - -# ------------------------------------------------------------------------- -# Entry point and main game loop -# ------------------------------------------------------------------------- def main() -> None: - global restart, ship + game = Game() + world = game.world + ship = world.ship f: Dict[str, Callable[[], None]] = { - "NAV": navigation, - "SRS": ship.short_range_scan, - "LRS": long_range_scan, - "PHA": phaser_control, - "TOR": photon_torpedoes, + "NAV": game.navigation, + "SRS": game.short_range_scan, + "LRS": game.long_range_scan, + "PHA": game.phaser_control, + "TOR": game.photon_torpedoes, "SHE": ship.shield_control, - "DAM": damage_control, - "COM": computer, - "XXX": end_game, + "DAM": game.damage_control, + "COM": game.computer, + "XXX": game.end_game, } while True: - startup() - new_quadrant() + game.startup() + game.new_quadrant() restart = False while not restart: