diff --git a/main.py b/main.py index 8c9a122..4a5efca 100644 --- a/main.py +++ b/main.py @@ -1,24 +1,6 @@ """ 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. """ @@ -32,12 +14,13 @@ 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} + 'Industrial Synthesis': {'input': {'Coal': 4, 'Copper': 4}, 'output': {'Conductor': 10}, 'turns': 3}, + 'Manual Synthesis': {'input': {'Coal': 1, 'Copper': 1}, 'output': {'Conductor': 2}, 'turns': 2} } def main(): + """Initializes the game environment, runs the game loop.""" console = Console() competitors_ids = ["Player", "RationalAI", "RiskTakingAI"] market = Market() @@ -52,7 +35,7 @@ def main(): market.companies = {company.player_id: company for company in ai_competitors + [player]} # Game loop - for turn in range(1, 11): + for turn in range(1, 27): console.print(f"\n--- Turn {turn} ---\n", style="grey50") market.update_market() @@ -67,6 +50,7 @@ def main(): def load_json(filename): + """Loads and returns data from a JSON file specified by the filename.""" try: with open(filename) as file: return json.load(file) @@ -76,41 +60,88 @@ def load_json(filename): class Market: + """ + Represents the game's market, managing product prices, stock ledger, and economic indicators. + + Attributes: + current_turn (int): Counter for the game turn. + companies (dict): Stores company objects with their IDs. + products (dict): Current prices of products. + starting_prices (dict): Initial prices of products for comparison. + events (list): Potential market events affecting prices. + adjust_prices (function): Adjusts product prices based on trends. + event_effects (dict): Effects of market events on prices. + stock_ledger (dict): Tracks stock ownership. + inflation_rate (float): Inflation rate. + unemployment_rate (float): Unemployment rate. + gdp (float): Gross Domestic Product value. + + The market is updated every turn. + """ def __init__(self): self.current_turn = 0 self.companies = {} - self.products = {'A': 10.0, 'B': 15.0, 'C': 20.0} + self.products = {'Coal': 10.0, 'Copper': 15.0, 'Conductor': 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.inflation_rate = 0.05 self.unemployment_rate = 0.05 self.gdp = 500000 + self.trade_volume = 0 + self.total_bought = {'Coal': 0, 'Copper': 0, 'Conductor': 0} + self.total_sold = {'Coal': 0, 'Copper': 0, 'Conductor': 0} + self.previous_prices = self.products.copy() def update_stock_ledger(self, company_id, owner_id, amount): + """Updates the stock ledger for a given company and owner based on the transaction 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): + """Returns the number of stocks owned by a given owner for a specified company.""" return self.stock_ledger.get((company_id, owner_id), 0) def get_stock_price(self, company_id): + """Calculates and returns the current stock price for a specified company.""" 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) + self.previous_prices = self.products.copy() + # Adjust prices based on demand + for product in self.products.keys(): + demand_factor = self.total_bought[product] - self.total_sold[product] + self.products[product] *= (1 + demand_factor * 0.05) + self.reset_trade_volumes() + + # Update GDP + self.gdp += self.trade_volume * 0.1 # Example formula + self.trade_volume = 0 + + # Update inflation rate + self.inflation_rate += (self.gdp / 500000 - 1) * 0.05 # Example adjustment + + # Handle market events 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 reset_trade_volumes(self): + for product in self.total_bought.keys(): + self.total_bought[product] = 0 + self.total_sold[product] = 0 + + def record_trade(self, value): + self.trade_volume += value + def adjust_prices(self): + """Adjusts product prices in the market based on inflation and random fluctuations.""" for product, price in self.products.items(): inflation_adjustment = price * self.inflation_rate fluctuation = random.uniform(-0.03, 0.03) @@ -118,16 +149,19 @@ class Market: return self.products def handle_competitor_event(self, effect): + """Handles market events related to competitors, adjusting product prices accordingly.""" 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): + """Updates key economic indicators like inflation and unemployment rates.""" 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): + """Calculates and returns final scores for each company based on value and stock ownership.""" final_scores = {} for company_id, company in self.companies.items(): final_score = company.value @@ -149,12 +183,24 @@ class Market: class Company: - """Represents a company within the market, managing finances, inventory, and stock transactions.""" + """ + Base class for a company in the game, handling inventory, stock, and financial transactions. + Attributes: + player_id (str): Unique identifier for the company. + cash (float): Available cash for transactions. + inventory (dict): Current inventory of products. + crafting_queue (list): Queue of products being crafted. + own_stock_ownership (dict): Ownership percentage of own stocks. + stock_holdings (dict): Holdings of other companies' stocks. + total_shares (int): Total shares available in the company. + _market (Market): Reference to the game's market. + _debug (bool): Flag for enabling debug mode. + """ 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.inventory = {'Coal': 0, 'Copper': 0, 'Conductor': 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"])} @@ -216,14 +262,24 @@ class Company: 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 + + if buying: + if self.cash >= total_cost: + self.cash -= total_cost + self.inventory[product] += quantity + market.total_bought[product] += quantity # Record the purchase in the market + market.record_trade(total_cost) # Update the market's trade volume + else: + print("Insufficient funds to complete purchase.") + else: + if self.inventory[product] >= quantity: + self.cash += total_cost + self.inventory[product] -= quantity + market.total_sold[product] += quantity # Record the sale in the market + market.record_trade(total_cost) # Update the market's trade volume + else: + print("Insufficient inventory to complete sale.") def crafting_decision(self): """Displays crafting options and handles the user's crafting choice.""" @@ -252,7 +308,7 @@ class Company: 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) + available_shares = self._calculate_available_shares() if amount > available_shares: return f"Not enough available shares to buy. Available: {available_shares}" @@ -260,10 +316,17 @@ class Company: return "Insufficient funds to buy stocks." # Update stock ownership - if is_ai: - self.stock_holdings[company_id] += amount # Increase AI's stock holdings + if company_id == self.player_id: + # Buying own company's stock + self.own_stock_ownership[self.player_id] += amount else: - self.own_stock_ownership[company_id] += amount # Increase player's own stock holdings + # Buying another company's stock + if is_ai: + # For AI + self.stock_holdings[company_id] += amount + else: + # For player + self.stock_holdings[company_id] += amount self.cash -= total_value # Deduct the cost from the buyer's cash return f"Bought {amount} stocks of {company_id}." @@ -283,7 +346,7 @@ class Company: 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): + def _calculate_available_shares(self): """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 @@ -309,33 +372,54 @@ class Company: 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) + + # Cash Row 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()])) + + # Inventory Row + inventory_display = ', '.join([f"[bold]{item}: {quantity}" for item, quantity in self.inventory.items()]) + status_table.add_row("Inventory", inventory_display) + + # Market Prices Row with Color Coding and Emojis + price_info = [] + for product, price in market.products.items(): + price_change = price - market.previous_prices.get(product, price) + if price_change > 0: + price_info.append(f"[green]{product}: {price:.2f} € :arrow_up_small:") # Green color and up emoji + elif price_change < 0: + price_info.append(f"[red]{product}: {price:.2f} € :arrow_down_small:") # Red color and down emoji + else: + price_info.append(f"{product}: {price:.2f} €") # Default color (no change) + status_table.add_row("Market Prices", ', '.join(price_info)) + + # Shareholders Row shareholders = ', '.join( [f"[bold]{company}[/]: {ownership} shares" for company, ownership in self.own_stock_ownership.items()]) status_table.add_row("Your Shareholders", shareholders) + + # Investments Row 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) + + # Action Choices 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 Selection action_choice = Prompt.ask("Choose your action", default="4") selected_action = actions.get(action_choice, None) @@ -422,11 +506,17 @@ class Company: 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): + """ + AI Company. Inherits from the Company class and adds AI-specific decision-making based on risk tolerance. + + Attributes: + risk_tolerance (float): A value representing the AI's willingness to take risks, influencing its decisions. + actions_history (list): Records the history of actions taken by the AI. + average_prices (dict): Tracks the average market prices of products for strategic decision-making. + + Methods provide the AI's logic for crafting, trading, stock transactions, and handling market events. + """ def __init__(self, player_id, competitors_ids, market, risk_tolerance): super().__init__(player_id, competitors_ids, market) self.risk_tolerance = risk_tolerance @@ -434,70 +524,119 @@ class AICompany(Company): 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 + """Updates the average prices of products in the market for AI decision-making purposes.""" + 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) + self.high_risk_decision(market) else: # Low-Risk AI - self.low_risk_decision(market, competitors) + self.low_risk_decision(market) - def low_risk_decision(self, market, competitors): + def low_risk_decision(self, market): + """ + Defines the decision-making process for a low-risk AI player. + """ if market.current_turn == 1: - self.buy_product('A', self.cash / 3) + self.buy_product('Coal', self.cash / 3) elif market.current_turn == 2: - self.buy_product('B', min(self.inventory['A'], self.cash / market.products['B'])) + self.buy_product('Copper', min(self.inventory['Coal'], self.cash / market.products['Copper'])) elif market.current_turn >= 3: - self.buy_and_craft(market) + self.buy_and_craft() - def high_risk_decision(self, market, competitors): + def high_risk_decision(self, market): + """ + Defines the decision-making process for a high-risk AI player. + """ if market.current_turn == 1: self.sell_own_shares(market) - self.buy_product('A', self.cash / 2) - self.buy_product('B', self.cash) + self.buy_product('Coal', self.cash / 2) + self.buy_product('Copper', self.cash) else: - if self.should_craft(market): - self.buy_and_craft(market) + if self.should_craft(): + self.buy_and_craft() if self.should_sell_products(market): self.sell_high_value_products(market) + if market.current_turn > 6: + self.buy_stocks_strategy() def buy_product(self, product, budget): + """ + Buys a specific quantity of a product for the AI company. + """ 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 buy_stocks_strategy(self): + own_stock_left = 100 - self.own_stock_ownership[self.player_id] + if own_stock_left > 0: + stock_price = self._market.get_stock_price(self.player_id) + amount_to_buy = min(own_stock_left, int(self.cash / stock_price)) + if amount_to_buy > 0: + action = f"{self.player_id} is buying its own stock. Amount to buy: {amount_to_buy}" + self.actions_history.append(action) + self.trade_stock('buy', self._market, self.player_id, amount_to_buy, is_ai=True) + return + + for company_id in self.stock_holdings: + if company_id != self.player_id: + stock_price = self._market.get_stock_price(company_id) + if stock_price <= self.cash: + amount_to_buy = int(self.cash / stock_price) + print(f"{self.player_id} is buying {company_id}'s stock. Amount to buy: {amount_to_buy}") + self.trade_stock('buy', self._market, company_id, amount_to_buy, is_ai=True) + return + def sell_own_shares(self, market): + """ + Sells a portion of the AI company's own shares. + """ 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_craft(self): + """ + Determines if the AI should craft products based on inventory and market conditions. + """ + return all(self.inventory[product] >= qty for product, qty in crafting_recipes['Manual Synthesis']['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) + """ + Decides if the AI should sell products based on market prices. + """ + 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): + """ + Decides if the AI should sell products based on market prices. + """ 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'] + def buy_and_craft(self): + """ + Executes buying of resources and crafting of products for the AI. + """ + chosen_recipe = crafting_recipes['Manual Synthesis'] 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.craft_product('Manual Synthesis') + print(f"Crafting using Manual Synthesis") + action = "Crafted products using Manual Synthesis" self.actions_history.append(action) else: - print("Not enough resources to craft using recipe2") + print("Not enough resources to craft using Manual Synthesis") def print_ai_actions(ai_competitors): @@ -509,17 +648,16 @@ def print_ai_actions(ai_competitors): # List all actions for idx, action in enumerate(ai.actions_history, 1): - panel_text.append(f"{idx}. Action: {action}\n", style="bold cyan") + panel_text.append(f"{idx}. Action: {action}\n", style="grey50") # 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") + panel_text.append(f"\nCurrent Cash: {ai.cash:.2f} €\n", style="bold blue") + panel_text.append(f"Inventory: {inventory_text}", style="bold cyan") # 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)