""" A simple business simulation game with a focus on market dynamics, crafting and trading. Key features: - Market simulation: Manage dynamic product prices, economic factors and random events. - Business management: Allows players to craft products, trade and manage inventory. - AI Competitors: Includes RationalAI and RiskTakingAI, each with unique strategies. - Crafting System: Allows the creation of products using specific recipes. - Stock Market: Facilitates stock trading for players and AI, affecting financial strategies. - Turn-based gameplay: Includes crafting, trading and stock decisions per turn. Goals: - Engage players in strategic economic decisions in a compact, interactive format. - Utilises a 'rich' library for enhanced console performance and interaction. How to play: 1. Start the game by running the main function. 2. Decide on crafting, trading and stock transactions. 3. Compete against the AI with different strategies. 4. Play through a set number of rounds, resulting in a final score. Note: Requires 'rich' and 'json' libraries for functionality. """ import json import random from rich.console import Console from rich.table import Table from rich.prompt import Prompt from rich import box from rich.panel import Panel from rich.text import Text crafting_recipes = { 'recipe1': {'input': {'A': 4, 'B': 4}, 'output': {'C': 10}, 'turns': 3}, 'recipe2': {'input': {'A': 1, 'B': 1}, 'output': {'C': 2}, 'turns': 2} } def main(): console = Console() competitors_ids = ["Player", "RationalAI", "RiskTakingAI"] market = Market() # Initialise the player player = Company("Player", [cid for cid in competitors_ids if cid != "Player"], market) # Initialise AI competitors rational_ai = AICompany("RationalAI", competitors_ids, market, risk_tolerance=0.3) risk_taking_ai = AICompany("RiskTakingAI", competitors_ids, market, risk_tolerance=0.7) ai_competitors = [rational_ai, risk_taking_ai] market.companies = {company.player_id: company for company in ai_competitors + [player]} # Game loop for turn in range(1, 11): console.print(f"\n--- Turn {turn} ---\n", style="grey50") market.update_market() for company in [player] + ai_competitors: company.update_crafting() company.make_decision(market, ai_competitors) print_ai_actions(ai_competitors) final_scores = market.calculate_final_scores() display_final_scores(console, final_scores, market) def load_json(filename): try: with open(filename) as file: return json.load(file) except FileNotFoundError: print(f"Error: The file {filename} was not found.") exit(1) class Market: def __init__(self): self.current_turn = 0 self.companies = {} self.products = {'A': 10.0, 'B': 15.0, 'C': 20.0} self.starting_prices = self.products.copy() self.events = load_json('market_events.json')["events"] self.adjust_prices = lambda adjustment: {k: max(1, v + adjustment) for k, v in self.products.items()} self.event_effects = {"double": lambda x: x * 2, "halve": lambda x: x / 2, "increase": lambda x: x + 3.0, "decrease": lambda x: max(1, x - 3.0)} self.stock_ledger = {} self.inflation_rate = 0.02 self.unemployment_rate = 0.05 self.gdp = 500000 def update_stock_ledger(self, company_id, owner_id, amount): self.stock_ledger[company_id, owner_id] = self.stock_ledger.get((company_id, owner_id), 0) + amount def get_stock_ownership(self, company_id, owner_id): return self.stock_ledger.get((company_id, owner_id), 0) def get_stock_price(self, company_id): return round(self.companies[company_id].value / 100.0, 2) def update_market(self): self.current_turn += 1 trend_adjustment = random.choice([2, -2, 0]) self.products = self.adjust_prices(trend_adjustment) event = random.choices(self.events, weights=[e["probability"] for e in self.events])[0] if event["effect"] in ["new_competitor", "exit_competitor"]: self.handle_competitor_event(event["effect"]) else: self.products = {k: self.event_effects[event["effect"]](v) for k, v in self.products.items()} self.update_economic_indicators() def adjust_prices(self): for product, price in self.products.items(): inflation_adjustment = price * self.inflation_rate fluctuation = random.uniform(-0.03, 0.03) self.products[product] = round(max(1.5, price + inflation_adjustment + fluctuation), 2) return self.products def handle_competitor_event(self, effect): adjustment = float(random.randint(1, 3)) self.products = {k: max(1.0, v - adjustment) if effect == "new_competitor" else v + adjustment for k, v in self.products.items()} def update_economic_indicators(self): self.inflation_rate = max(0.5, self.inflation_rate + random.uniform(-0.01, 0.01)) self.unemployment_rate = min(max(self.unemployment_rate + random.uniform(-0.005, 0.005), 0), 1) self.gdp += self.gdp * (random.uniform(-0.01, 0.03) + self.inflation_rate) def calculate_final_scores(self): final_scores = {} for company_id, company in self.companies.items(): final_score = company.value majority_owner = max(company.own_stock_ownership, key=lambda owner: (company.own_stock_ownership[owner], owner)) majority_percentage = company.own_stock_ownership[majority_owner] is_major_owner = majority_percentage >= 51 if company_id not in final_scores: final_scores[company_id] = {'score': 0, 'note': '', 'majority_owner': majority_owner} for owner_id, percentage in company.own_stock_ownership.items(): if percentage > 20: final_scores[owner_id] = final_scores.get(owner_id, {'score': 0, 'note': '', 'majority_owner': ''}) final_scores[owner_id]['score'] += final_score * (percentage / 100) if not is_major_owner: remaining_score = final_score - sum( final_scores.get(owner, {'score': 0})['score'] for owner in company.own_stock_ownership) final_scores[company_id]['score'] += remaining_score return final_scores class Company: """Represents a company within the market, managing finances, inventory, and stock transactions.""" def __init__(self, player_id, competitors_ids, market=None, debug=False): self.player_id = player_id self.cash = 500.0 self.inventory = {'A': 0, 'B': 0, 'C': 0} self.crafting_queue = [] self.own_stock_ownership = {cid: 51 if cid == player_id else 0 for cid in [player_id] + competitors_ids} self.stock_holdings = {cid: 0 for cid in set([player_id] + competitors_ids + ["RationalAI", "RiskTakingAI"])} self.total_shares = 100 self._market = market self._debug = debug @property def value(self): """Calculates the total value of the company, combining cash and the market value of its inventory.""" return self.cash + sum(self.inventory[product] * price for product, price in self._market.products.items()) def craft_product(self, recipe_key): """Processes crafting of a product based on the chosen recipe.""" if self._debug: print(f"Inventory before crafting: {self.inventory}") recipe = crafting_recipes[recipe_key] if all(self.inventory[product] >= quantity for product, quantity in recipe['input'].items()): self.crafting_queue.append({'recipe': recipe, 'turns_remaining': recipe['turns']}) self._update_inventory(recipe['input'], decrease=True) print("Crafting order placed.") else: print("Not enough resources to craft.") if self._debug: print(f"Inventory after crafting: {self.inventory}") def _update_inventory(self, items, decrease=False): """Updates the inventory based on the given items.""" if self._debug: print(f"Inventory before update: {self.inventory}") for product, quantity in items.items(): if decrease: self.inventory[product] -= quantity else: self.inventory[product] += quantity if self._debug: print(f"Inventory after update: {self.inventory}") def update_crafting(self): """Updates the crafting queue, completing orders as their turns conclude.""" if self._debug: print(f"Crafting queue before update: {self.crafting_queue}") completed_orders = [] for order in self.crafting_queue: if self._debug: print(f"Processing order: {order}") order['turns_remaining'] -= 1 if order['turns_remaining'] == 0: if self._debug: print(f"Completing order: {order}") self._update_inventory(order['recipe']['output']) if self._debug: print(f"Inventory after completing order: {self.inventory}") completed_orders.append(order) for order in completed_orders: self.crafting_queue.remove(order) if self._debug: print(f"Crafting queue after update: {self.crafting_queue}") def trade_product(self, market, product, quantity, buying=True): """Executes a product trade transaction based on the specified parameters.""" total_cost = market.products[product] * quantity if buying and self.cash >= total_cost: self.cash -= total_cost self.inventory[product] += quantity elif not buying and self.inventory[product] >= quantity: self.cash += total_cost self.inventory[product] -= quantity def crafting_decision(self): """Displays crafting options and handles the user's crafting choice.""" print("\nCrafting Decision") recipe_keys = list(crafting_recipes.keys()) print("\nAvailable Recipes:") for idx, recipe in enumerate(recipe_keys, 1): print(f" {idx}: {recipe}") recipe_choice = self.get_user_choice(len(recipe_keys), "Choose a recipe to craft: ") self.craft_product(recipe_keys[recipe_choice - 1]) def trade_stock(self, action, market, company_id, amount, is_ai=False): """Executes a stock trade action, buying or selling as specified.""" if company_id not in market.companies and company_id != self.player_id: return "Company not found in the market." stock_price = market.get_stock_price(company_id) total_value = stock_price * amount if action == 'buy': return self._buy_stock(company_id, amount, total_value, is_ai) elif action == 'sell': return self._sell_stock(company_id, amount, total_value, is_ai) else: return "Invalid stock action." def _buy_stock(self, company_id, amount, total_value, is_ai): """Handles the buying of stocks for the specified company.""" available_shares = self._calculate_available_shares(company_id) if amount > available_shares: return f"Not enough available shares to buy. Available: {available_shares}" if self.cash < total_value: return "Insufficient funds to buy stocks." # Update stock ownership if is_ai: self.stock_holdings[company_id] += amount # Increase AI's stock holdings else: self.own_stock_ownership[company_id] += amount # Increase player's own stock holdings self.cash -= total_value # Deduct the cost from the buyer's cash return f"Bought {amount} stocks of {company_id}." def _sell_stock(self, company_id, amount, total_value, is_ai): """Handles the selling of stocks for the specified company.""" # Check if the seller has enough stocks to sell if self._get_stock_ownership(company_id) < amount: return "Not enough stocks to sell." # Update stock ownership if is_ai: self.stock_holdings[company_id] -= amount # Decrease AI's stock holdings else: self.own_stock_ownership[company_id] -= amount # Decrease player's own stock holdings self.cash += total_value # Add the proceeds to the seller's cash return f"Sold {amount} stocks of {company_id}." def _calculate_available_shares(self, company_id): """Calculates the number of available shares for a given company.""" total_owned = sum(self.stock_holdings.values()) + sum(self.own_stock_ownership.values()) return self.total_shares - total_owned def _get_stock_ownership(self, company_id): """Retrieves the stock ownership amount for a given company.""" if company_id == self.player_id: return self.own_stock_ownership[self.player_id] return self.stock_holdings.get(company_id, 0) def _update_stock_ownership(self, company_id, amount, total_value, buying): """Updates the stock ownership details after a buy or sell action.""" if company_id == self.player_id: if buying: self.own_stock_ownership[self.player_id] += amount else: self.own_stock_ownership[self.player_id] -= amount else: if buying: self.stock_holdings[company_id] += amount else: self.stock_holdings[company_id] -= amount self.cash += -total_value if buying else total_value def make_decision(self, market, competitors): """Presents decision options to the user and processes the chosen action.""" console = Console() status_table = Table(title=f"[bold cyan]{self.player_id}'s Turn - Turn {market.current_turn}", box=box.ROUNDED) status_table.add_column("Category", style="bold cyan") status_table.add_column("Details") status_table.add_row("Cash", f"[bold blue]{self.cash:.2f} €") status_table.add_row("Inventory", ', '.join( [f"[bold]{item}: {quantity}" for item, quantity in self.inventory.items()])) status_table.add_row("Market Prices", ', '.join( [f"[bold blue]{product}: {price:.2f} €" for product, price in market.products.items()])) shareholders = ', '.join( [f"[bold]{company}[/]: {ownership} shares" for company, ownership in self.own_stock_ownership.items()]) status_table.add_row("Your Shareholders", shareholders) investments = ', '.join( [f"[bold]{company}[/]: {holding} shares" for company, holding in self.stock_holdings.items() if holding > 0]) status_table.add_row("Your Investments", investments) console.print(status_table) actions = { "1": "Trade Products", "2": "Craft", "3": "Trade Stocks", "4": "Skip Turn" } choices_display = "\n".join([f"{key}: {value}" for key, value in actions.items()]) console.print(f"Available Actions:\n{choices_display}", style="bold") action_choice = Prompt.ask("Choose your action", default="4") selected_action = actions.get(action_choice, None) if selected_action == "Trade Products": self.trade_products_decision(market) elif selected_action == "Craft": self.crafting_decision() elif selected_action == "Trade Stocks": stock_actions = ["Buy", "Sell"] stock_action_choice = Prompt.ask("Choose stock action", choices=stock_actions, default=stock_actions[0]) self.trade_stocks_decision(stock_action_choice.lower(), market, competitors) elif selected_action == "Skip Turn": pass # Skip turn else: console.print("[bold red]Invalid choice. Please enter a valid option.") def trade_products_decision(self, market): """Handles the decision-making process for trading products.""" print("\nProduct Trading Decision") products = list(market.products.keys()) print("\nAvailable Products:") for idx, product in enumerate(products, 1): print(f" {idx}: {product} - Price: {market.products[product]:.2f} €") product_choice = self.get_user_choice(len(products), "Choose a product to trade: ") product = products[product_choice - 1] quantity = self.get_valid_input("Enter the quantity to trade: ", int, "Quantity must be positive.", lambda x: x > 0) self.get_user_choice(2, "Choose trade type (1: Buy, 2: Sell): ") self.trade_product(market, product, quantity) def trade_stocks_decision(self, action, market, competitors): """Facilitates the decision-making process for stock trading actions.""" print("\nStock Trading Decision") if action == 'buy': print("Available companies to buy stocks from:") elif action == 'sell': print("Your stock holdings:") companies = competitors + [self] for idx, company in enumerate(companies, 1): company_id = company.player_id stock_info = f" {idx}: {company_id} - Current stock price: {market.get_stock_price(company_id)}" if action == 'sell' and self.stock_holdings.get(company_id, 0) > 0: stock_info += f", Owned: {self.stock_holdings[company_id]}" print(stock_info) company_choice = self.get_user_choice(len(companies), "Enter the company number to trade stocks: ") company_id = companies[company_choice - 1].player_id amount = self.get_valid_input("Enter the amount of stocks to trade: ", int, "Stock amount must be positive.", lambda x: x > 0) self.trade_stock(action, market, company_id, amount) @staticmethod def get_user_choice(num_options, prompt): """Prompts the user for a choice and validates the input.""" choice = 0 while choice < 1 or choice > num_options: try: choice = int(input(prompt)) if choice < 1 or choice > num_options: raise ValueError except ValueError: print(f"Please enter a number between 1 and {num_options}.") return choice @staticmethod def get_valid_input(prompt, input_type, error_message, validation_func=lambda x: True): """Requests and validates user input based on specified criteria.""" while True: try: value = input_type(input(prompt)) if not validation_func(value): raise ValueError return value except ValueError: print(error_message) def is_market_boom(self): """Checks if the market is booming.""" return all(price > 20 for price in self._market.products.values()) def is_market_bust(market): return all(price < 0.75 * market.starting_prices[product] for product, price in market.products.items()) class AICompany(Company): def __init__(self, player_id, competitors_ids, market, risk_tolerance): super().__init__(player_id, competitors_ids, market) self.risk_tolerance = risk_tolerance self.actions_history = [] self.average_prices = {product: market.products[product] for product in market.products} def update_average_prices(self): self.average_prices = {product: (self.average_prices[product] * (self._market.current_turn - 1) + price) / self._market.current_turn for product, price in self._market.products.items()} def make_decision(self, market, competitors): if self.risk_tolerance > 0.5: # High-Risk AI self.high_risk_decision(market, competitors) else: # Low-Risk AI self.low_risk_decision(market, competitors) def low_risk_decision(self, market, competitors): if market.current_turn == 1: self.buy_product('A', self.cash / 3) elif market.current_turn == 2: self.buy_product('B', min(self.inventory['A'], self.cash / market.products['B'])) elif market.current_turn >= 3: self.buy_and_craft(market) def high_risk_decision(self, market, competitors): if market.current_turn == 1: self.sell_own_shares(market) self.buy_product('A', self.cash / 2) self.buy_product('B', self.cash) else: if self.should_craft(market): self.buy_and_craft(market) if self.should_sell_products(market): self.sell_high_value_products(market) def buy_product(self, product, budget): quantity = int(budget // self._market.products[product]) if quantity > 0: self.trade_product(self._market, product, quantity) action = f"Bought {quantity} of {product}" self.actions_history.append(action) def sell_own_shares(self, market): amount_to_sell = int(self.own_stock_ownership[self.player_id] * 0.25) # Sell 25% of own shares if amount_to_sell > 0: self.trade_stock('sell', market, self.player_id, amount_to_sell, is_ai=True) action = f"Sold {amount_to_sell} of own shares" self.actions_history.append(action) def should_craft(self, market): return all(self.inventory[product] >= qty for product, qty in crafting_recipes['recipe2']['input'].items()) def should_sell_products(self, market): return any(market.products[product] >= 2 * self.average_prices[product] for product in self.inventory if self.inventory[product] > 0) def sell_high_value_products(self, market): for product, quantity in self.inventory.items(): if quantity > 0 and market.products[product] >= 2 * self.average_prices[product]: self.trade_product(market, product, quantity, buying=False) action = f"Sold high value products" self.actions_history.append(action) def buy_and_craft(self, market): chosen_recipe = crafting_recipes['recipe2'] if all(self.inventory[product] >= qty for product, qty in chosen_recipe['input'].items()): self.craft_product('recipe2') print(f"Crafting using recipe2") action = "Crafted products using recipe2" self.actions_history.append(action) else: print("Not enough resources to craft using recipe2") def print_ai_actions(ai_competitors): """Displays the actions history, current cash, and inventory of AI competitors.""" console = Console() for ai in ai_competitors: panel_text = Text() inventory_text = ', '.join([f"{product}: {quantity}" for product, quantity in ai.inventory.items()]) # List all actions for idx, action in enumerate(ai.actions_history, 1): panel_text.append(f"{idx}. Action: {action}\n", style="bold cyan") # Append current cash and inventory to the text panel_text.append(f"\nCurrent Cash: {ai.cash:.2f} €\n", style="bold yellow") panel_text.append(f"Inventory: {inventory_text}", style="bold magenta") # Display in a panel console.print(Panel(panel_text, title=f"[bold]{ai.player_id}'s Status", border_style="grey50")) def display_final_scores(console, final_scores, market): """Displays the final scores in a styled table.""" score_table = Table(header_style="bold cyan", box=box.DOUBLE_EDGE) score_table.add_column("Company", style="bold") score_table.add_column("Final Score", justify="right") score_table.add_column("Majority Owner", style="bold") score_table.add_column("Ownership Percentage", justify="right", style="dim") for company_id, data in sorted(final_scores.items(), key=lambda item: item[1]['score'], reverse=True): ownership_percentage = f"{data['score'] / market.companies[company_id].value * 100:.2f}%" score_table.add_row(company_id, f"{data['score']:.0f}", data['majority_owner'], ownership_percentage) console.print(score_table) if __name__ == "__main__": main()