diff --git a/50_Horserace/python/horserace.py b/50_Horserace/python/horserace.py new file mode 100644 index 00000000..cf7c266e --- /dev/null +++ b/50_Horserace/python/horserace.py @@ -0,0 +1,278 @@ +import random +import math +import time + +def basic_print(*zones, **kwargs): + """Simulates the PRINT command from BASIC to some degree. + Supports `printing zones` if given multiple arguments.""" + + line = "" + if len(zones) == 1: + line = str(zones[0]) + else: + line = "".join(["{:<14}".format(str(zone)) for zone in zones]) + identation = kwargs.get("indent", 0) + end = kwargs.get("end", "\n") + print(" " * identation + line, end=end) + + +def basic_input(prompt, type_conversion=None): + """BASIC INPUT command with optional type conversion""" + + while True: + try: + inp = input(f"{prompt}? ") + if type_conversion is not None: + inp = type_conversion(inp) + break + except ValueError: + basic_print("INVALID INPUT!") + return inp + + +# horse names do not change over the program, therefore making it a global. +# throught the game, the ordering of the horses is used to indentify them +HORSE_NAMES = [ + "JOE MAW", + "L.B.J.", + "MR.WASHBURN", + "MISS KAREN", + "JOLLY", + "HORSE", + "JELLY DO NOT", + "MIDNIGHT" +] + + +def introduction(): + """Print the introduction, and optional the instructions""" + + basic_print("HORSERACE", indent=31) + basic_print("CREATIVE COMPUTING MORRISTOWN, NEW JERSEY", indent=15) + basic_print("\n\n") + basic_print("WELCOME TO SOUTH PORTLAND HIGH RACETRACK") + basic_print(" ...OWNED BY LAURIE CHEVALIER") + y_n = basic_input("DO YOU WANT DIRECTIONS") + + # if no instructions needed, return + if y_n.upper() == "NO": + return + + basic_print("UP TO 10 MAY PLAY. A TABLE OF ODDS WILL BE PRINTED. YOU") + basic_print("MAY BET ANY + AMOUNT UNDER 100000 ON ONE HORSE.") + basic_print("DURING THE RACE, A HORSE WILL BE SHOWN BY ITS") + basic_print("NUMBER. THE HORSES RACE DOWN THE PAPER!") + basic_print("") + + +def setup_players(): + """Gather the number of players and their names""" + + # ensure we get an integer value from the user + number_of_players = basic_input("HOW MANY WANT TO BET", int) + + # for each user query their name and return the list of names + player_names = [] + basic_print("WHEN ? APPEARS,TYPE NAME") + for _ in range(number_of_players): + player_names.append(basic_input("")) + return player_names + + +def setup_horses(): + """Generates random odds for each horse. Returns a list of + odds, indexed by the order of the global HORSE_NAMES.""" + + odds = [random.randrange(1, 10) for _ in HORSE_NAMES] + total = sum(odds) + + # rounding odds to two decimals for nicer output, + # this is not in the origin implementation + return [round(total/odd, 2) for odd in odds] + + +def print_horse_odds(odds): + """Print the odds for each horse""" + + basic_print("") + for i in range(len(HORSE_NAMES)): + basic_print(HORSE_NAMES[i], i, f"{odds[i]}:1") + basic_print("") + + +def get_bets(player_names): + """For each player, get the number of the horse to bet on, + as well as the amount of money to bet""" + + basic_print("--------------------------------------------------") + basic_print("PLACE YOUR BETS...HORSE # THEN AMOUNT") + + bets = [] + for name in player_names: + horse = basic_input(name, int) + amount = None + while amount is None: + amount = basic_input("", float) + if amount < 1 or amount >= 100000: + basic_print(" YOU CAN'T DO THAT!") + amount = None + bets.append((horse, amount)) + + basic_print("") + + return bets + + +def get_distance(odd): + """Advances a horse during one step of the racing simulation. + The amount travelled is random, but scaled by the odds of the horse""" + + d = random.randrange(1,100) + s = math.ceil(odd) + if d < 10: + return 1 + elif d < s + 17: + return 2 + elif d < s + 37: + return 3 + elif d < s + 57: + return 4 + elif d < s + 77: + return 5 + elif d < s + 77: + return 5 + elif d < s + 92: + return 6 + else: + return 7 + + +def print_race_state(total_distance, race_pos): + """Outputs the current state/stop of the race. + Each horse is placed according to the distance they have travelled. In + case some horses travelled the same distance, their numbers are printed + on the same name""" + + # we dont want to modify the `race_pos` list, since we need + # it later. Therefore we generating an interator from the list + race_pos_iter = iter(race_pos) + + # race_pos is stored by last to first horse in the race. + # we get the next horse we need to print out + next_pos = next(race_pos_iter) + + # start line + basic_print("XXXXSTARTXXXX") + + # print all 28 lines/unit of the race course + for l in range(28): + + # ensure we still have a horse to print and if so, check if the + # next horse to print is not the current line + # needs iteration, since multiple horses can share the same line + while next_pos is not None and l == total_distance[next_pos]: + basic_print(f"{next_pos} ", end="") + next_pos = next(race_pos_iter, None) + else: + # if no horses are left to print for this line, print a new line + basic_print("") + + # finish line + basic_print("XXXXFINISHXXXX") + + +def simulate_race(odds): + num_horses = len(HORSE_NAMES) + + # in spirit of the original implementation, using two arrays to + # track the total distance travelled, and create an index from + # race position -> horse index + total_distance = [0] * num_horses + + # race_pos maps from the position in the race, to the index of the horse + # it will later be sorted from last to first horse, based on the + # distance travelled by each horse. + # e.g. race_pos[0] => last horse + # race_pos[-1] => winning horse + race_pos = list(range(num_horses)) + + basic_print("\n1 2 3 4 5 6 7 8") + + while True: + + # advance each horse by a random amount + for i in range(num_horses): + total_distance[i] += get_distance(odds[i]) + + # bubble sort race_pos based on total distance travelled + # in the original implementation, race_pos is reset for each + # simulation step, so we keep this behaviour here + race_pos = list(range(num_horses)) + for l in range(num_horses): + for i in range(num_horses-1-l): + if total_distance[race_pos[i]] < total_distance[race_pos[i+1]]: + continue + race_pos[i], race_pos[i+1] = race_pos[i+1], race_pos[i] + + # print current state of the race + print_race_state(total_distance, race_pos) + + # goal line is defined as 28 units from start + # check if the winning horse is already over the finish line + if total_distance[race_pos[-1]] >= 28: + return race_pos + + # this was not in the original BASIC implementation, but it makes the + # race visualization a nice animation (if the terminal size is set to 31 rows) + time.sleep(1) + + +def print_race_results(race_positions, odds, bets, player_names): + """Print the race results, as well as the winnings of each player""" + + # print the race positions first + basic_print("THE RACE RESULTS ARE:") + position = 1 + for horse_idx in reversed(race_positions): + line = f"{position} PLACE HORSE NO. {horse_idx} AT {odds[horse_idx]}:1" + basic_print("") + basic_print(line) + position += 1 + + # followed by the amount the players won + winning_horse_idx = race_positions[-1] + for idx, name in enumerate(player_names): + (horse, amount) = bets[idx] + if horse == winning_horse_idx: + basic_print("") + basic_print(f"{name} WINS ${amount * odds[winning_horse_idx]}") + + +def main_loop(player_names, horse_odds): + """Main game loop""" + + while True: + print_horse_odds(horse_odds) + bets = get_bets(player_names) + final_race_positions = simulate_race(horse_odds) + print_race_results(final_race_positions, horse_odds, bets, player_names) + + basic_print("DO YOU WANT TO BET ON THE NEXT RACE ?") + one_more = basic_input("YES OR NO") + if one_more.upper() != "YES": + break + + +def main(): + # introduction, player names and horse odds are only generated once + introduction() + player_names = setup_players() + horse_odds = setup_horses() + + # main loop of the game, the player can play multiple races, with the + # same odds + main_loop(player_names, horse_odds) + + +if __name__ == "__main__": + main() \ No newline at end of file