From 30222c98eefdb11598b265a62db1ea20cc733518 Mon Sep 17 00:00:00 2001 From: isaak Date: Sun, 2 Jun 2024 11:50:47 +0200 Subject: [PATCH] Narrative Scenes added --- main.py | 687 +++++++++++++++---------------------------- narrative_scenes.yml | 443 ++++++++++++++++++++++++++++ 2 files changed, 687 insertions(+), 443 deletions(-) create mode 100644 narrative_scenes.yml diff --git a/main.py b/main.py index a4fafa3..27fb09a 100644 --- a/main.py +++ b/main.py @@ -1,17 +1,7 @@ -""" -A simple business simulation game with a focus on market dynamics, crafting and trading. - -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 +import yaml +from blessed import Terminal crafting_recipes = { 'Industrial Synthesis': {'input': {'Coal': 4, 'Copper': 4}, 'output': {'Conductor': 10}, 'turns': 3}, @@ -21,70 +11,7 @@ crafting_recipes = { } -def main(): - """Initializes the game environment, runs the game loop.""" - 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, 27): - console.print(f"\n--- Turn {turn} ---\n", style="grey50") - market.inflation_rate += market.inflation_change if market.inflation_growing else -market.inflation_change - market.inflation_rate = max(0.0001, min(market.inflation_rate, 0.05)) - market.gdp *= (1 + (market.inflation_rate / 20)) - market.update_market() - market.print_market_events() - - 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): - """Loads and returns data from a JSON file specified by the 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: - """ - 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 = {} @@ -111,22 +38,18 @@ class Market: self.current_turn += 1 self.previous_prices = self.products.copy() - # Update prices based on inflation and random fluctuations for product in self.products: inflation_adjustment = self.inflation_rate - random_fluctuation = random.uniform(-0.01, 0.01) # ±1% fluctuation + random_fluctuation = random.uniform(-0.01, 0.01) price_change_factor = 1 + inflation_adjustment + random_fluctuation self.products[product] *= price_change_factor self.reset_trade_volumes() - # Procedural generation of market trends self.trend_factor = self.generate_market_trend() - # Update economic indicators self.update_economic_indicators() - # Handle market events self.handle_market_events() def update_prices(self): @@ -151,7 +74,6 @@ class Market: def calculate_price_change(self, product): demand_factor = self.total_bought[product] - self.total_sold[product] - # Here you can define how strongly demand affects the price, e.g., 0.05 as in your original code return demand_factor * 0.05 def reset_trade_volumes(self): @@ -163,36 +85,11 @@ class Market: return max(0.5, min(self.trend_factor + trend_change, 1.5)) def update_economic_indicators(self): - """Updates key economic indicators like inflation and unemployment rates.""" - - # Update inflation_rate self.inflation_rate += self.inflation_change if self.inflation_growing else -self.inflation_change - self.inflation_rate = max(0.0001, min(self.inflation_rate, 0.05)) # Clamp the inflation_rate - - # Update GDP - self.gdp *= (1 + (self.inflation_rate / 2)) - - # Update unemployment_rate + self.inflation_rate = max(0.0001, min(self.inflation_rate, 0.05)) + self.gdp *= (1 + (self.inflation_rate / 20)) self.unemployment_rate = min(max(self.unemployment_rate + random.uniform(-0.005, 0.005), 0), 1) - # Optional: Print statements for debugging - print(f"Inflation Rate: {self.inflation_rate}") - print(f"GDP: {self.gdp}") - print(f"Unemployment Rate: {self.unemployment_rate}") - - def update_inflation_rate(self): - lower_bound = 0.0001 - upper_bound = 0.049 - if self.inflation_rate <= lower_bound: - self.inflation_growing = True - elif self.inflation_rate >= upper_bound: - self.inflation_growing = False - self.inflation_rate += self.inflation_change if self.inflation_growing else -self.inflation_change - - def update_unemployment_rate(self): - fluctuation = random.uniform(-0.005, 0.005) - self.unemployment_rate = max(0, min(self.unemployment_rate + fluctuation, 1)) - def handle_market_events(self): event = random.choices(self.events, weights=[e["probability"] for e in self.events])[0] self.last_event_name = event["name"] @@ -205,88 +102,32 @@ class Market: random_resource = random.choice(['Coal', 'Copper']) self.products[random_resource] *= 1.5 - def trade_agreement_effect(self, price): - """Handles the effect of a new trade agreement event.""" - return price * 0.9 - - def innovation_breakthrough_effect(self, price): - """Handles the effect of an innovation breakthrough event.""" - return price * 0.8 - - def recession_effect(self, price): - """Handles the effect of a recession event on product prices.""" - return price * 0.7 - - def print_market_events(self): - """ - Prints the latest market events and key economic indicators in a formatted panel. - """ - # Prepare the console and panel text for printing - console = Console() - event_info = f"Last Market Event: {self.last_event_name}\n" + def print_market_events(self, term): + event_info = f"{term.green}Last Market Event: {self.last_event_name}{term.normal}\n" economic_indicators = ( - f"Inflation Rate: {self.inflation_rate * 100:.2f}%\n" - f"Unemployment Rate: {self.unemployment_rate * 100:.2f}%\n" - f"GDP: {self.gdp:.2f} €" + f"{term.cyan}Inflation Rate: {self.inflation_rate * 100:.2f}%{term.normal}\n" + f"{term.cyan}Unemployment Rate: {self.unemployment_rate * 100:.2f}%{term.normal}\n" + f"{term.cyan}GDP: {self.gdp:.2f} €{term.normal}" ) - - # Create a formatted panel text - panel_text = Text(event_info + economic_indicators) - - # Print the panel with styling - console.print(Panel(panel_text, title="[bold]Market Events and Indicators", border_style="grey50")) + print(term.move_y(term.height // 4)) + print(term.center(event_info + economic_indicators)) 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 - self.previous_prices = self.products.copy() - - # Update prices based on supply and demand - for product in self.products: - supply_chain_impact = self.simulate_supply_chain_impact(product) - demand_factor = self.total_bought[product] - self.total_sold[product] - self.products[product] *= (1 + demand_factor * 0.05) * supply_chain_impact - - self.reset_trade_volumes() - - # Procedural generation of market trends - self.trend_factor = self.generate_market_trend() - - # Update economic indicators - self.update_economic_indicators() - - # Handle market events - self.handle_market_events() - def simulate_supply_chain_impact(self, product): return random.uniform(0.9, 1.1) - def generate_market_trend(self): - trend_change = random.uniform(-0.02, 0.02) - new_trend = self.trend_factor + trend_change - return max(0.5, min(new_trend, 1.5)) - - 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) @@ -294,33 +135,11 @@ 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.""" - - # Check bounds and change state if necessary - if self.inflation_rate <= 0.0001: # Lower bound - self.inflation_growing = True - self.inflation_change = random.uniform(0.0001, 0.001) # Reset change rate - elif self.inflation_rate >= 0.049: # Upper bound - self.inflation_growing = False - self.inflation_change = random.uniform(0.0001, 0.001) # Reset change rate - - # Adjust inflation rate based on the current state - if self.inflation_growing: - self.inflation_rate += self.inflation_change - else: - self.inflation_rate -= self.inflation_change - - # Unemployment rate adjustment with random fluctuation - self.unemployment_rate = min(max(self.unemployment_rate + random.uniform(-0.005, 0.005), 0), 1) - 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 @@ -342,21 +161,6 @@ class Market: class Company: - """ - 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 @@ -371,11 +175,9 @@ class Company: @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] @@ -389,7 +191,6 @@ class Company: 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(): @@ -401,7 +202,6 @@ class Company: 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 = [] @@ -429,34 +229,28 @@ class Company: 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 + market.total_bought[product] += quantity + market.record_trade(total_cost) 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 + market.total_sold[product] += quantity + market.record_trade(total_cost) else: print("Insufficient inventory to complete sale.") - # Update market prices after trading - market.update_prices() # This call ensures prices are updated with constraints + market.update_prices() - def crafting_decision(self): - """Displays crafting options and handles the user's crafting choice.""" - print("\nCrafting Decision") + def crafting_decision(self, term): + print(term.home + term.clear + term.move_y(term.height // 2)) 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]) + recipe_choice = self.display_menu("Choose a recipe to craft:", recipe_keys, term) + self.craft_product(recipe_keys[recipe_choice]) 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." @@ -471,11 +265,9 @@ class Company: return "Invalid stock action." def _buy_stock(self, company_id, amount): - # Calculate the total number of shares currently owned total_shares_owned = sum(self._market.companies[company_id].own_stock_ownership.values()) available_shares = self.total_shares - total_shares_owned - # Determine the maximum shares that can be bought based on available cash and available shares max_affordable_shares = int(self.cash / self._market.get_stock_price(company_id)) amount = min(amount, available_shares, max_affordable_shares) @@ -490,7 +282,6 @@ class Company: if company_id != self.player_id: self.stock_holdings[company_id] += amount else: - # Update own_stock_ownership when buying own shares self.own_stock_ownership[self.player_id] += amount return f"Bought {amount} stocks of {company_id}." @@ -499,174 +290,96 @@ class Company: if not is_ai: if self.stock_holdings[company_id] < amount: return "Not enough stocks to sell." - # Update buyer's stock_holdings for non-AI (like player) self.stock_holdings[company_id] -= amount else: if self.own_stock_ownership[company_id] < amount: return "Not enough stocks to sell." - # Update own_stock_ownership for AI self.own_stock_ownership[company_id] -= amount self.cash += total_value self._market.update_stock_ledger(company_id, self.player_id, -amount) - # If an AI company is selling its own shares, update the ownership for all shareholders if is_ai and company_id == self.player_id: for shareholder in self._market.companies[company_id].own_stock_ownership.keys(): - self._market.companies[company_id].own_stock_ownership[shareholder] -= amount * \ - (self._market.companies[ - company_id].own_stock_ownership[ - shareholder] / self.total_shares) + self._market.companies[company_id].own_stock_ownership[shareholder] -= amount * ( + self._market.companies[company_id].own_stock_ownership[shareholder] / self.total_shares) return f"Sold {amount} stocks of {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 + def display_menu(self, prompt, options, term): + selected_option = 0 - 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) + with term.cbreak(), term.hidden_cursor(): + while True: + print(term.home + term.clear + term.move_y(term.height // 2)) + print(term.center(prompt)) + for i, option in enumerate(options): + prefix = '-> ' if i == selected_option else ' ' + print(term.move_down(2) + term.center(f"{prefix}{option}")) + key = term.inkey() + if key.name == 'KEY_UP': + selected_option = (selected_option - 1) % len(options) + elif key.name == 'KEY_DOWN': + selected_option = (selected_option + 1) % len(options) + elif key.name == 'KEY_ENTER': + return selected_option - 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, term): + status_info = ( + f"\n{term.bold}{self.player_id}'s Turn - Turn {market.current_turn}{term.normal}\n\n" + f"Cash: {term.yellow}{self.cash:.2f} €{term.normal}\n" + f"Inventory: {', '.join([f'{item}: {quantity}' for item, quantity in self.inventory.items()])}\n" + f"Market Prices: {', '.join([f'{product}: {term.green if market.products[product] > market.previous_prices[product] else term.red}{market.products[product]:.2f} € {term.normal}{"▲" if market.products[product] > market.previous_prices[product] else "▼"}' for product in market.products])}\n" + f"Your Shareholders: {', '.join([f'{company}: {ownership} shares' for company, ownership in self.own_stock_ownership.items()])}\n" + f"Your Investments: {', '.join([f'{company}: {holding} shares' for company, holding in self.stock_holdings.items() if holding > 0])}" + ) - def make_decision(self, market, competitors): - console = Console() - status_table = Table(title=f"\n[bold cyan]{self.player_id}'s Turn - Turn {market.current_turn}", - box=box.ROUNDED) + actions = [ + "Trade Products 📦", + "Craft 🛠️", + "Trade Stocks 📈", + "Skip Turn ⏭️" + ] - # 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} €") + selected_action = self.display_menu(status_info + "\n\nChoose your action:", actions, term) - # Inventory Row - inventory_display = ', '.join([f"[bold]{item}: {quantity}" for item, quantity in self.inventory.items()]) - status_table.add_row("Inventory", inventory_display) + if selected_action == 0: + self.trade_products_decision(market, term) + elif selected_action == 1: + self.crafting_decision(term) + elif selected_action == 2: + stock_actions = ["Buy 📈", "Sell 📉"] + stock_action_choice = self.display_menu("Choose stock action:", stock_actions, term) + self.trade_stocks_decision(stock_action_choice.lower(), market, competitors, term) + elif selected_action == 3: + pass - # 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) - - 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") + def trade_products_decision(self, market, term): 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] + product_choice = self.display_menu("Choose a product to trade:", products, term) + product = products[product_choice] 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:") + trade_types = ["Buy 📈", "Sell 📉"] + trade_choice = self.display_menu("Choose trade type:", trade_types, term) + buying = trade_choice == 0 + self.trade_product(market, product, quantity, buying=buying) + def trade_stocks_decision(self, action, market, competitors, term): companies = competitors + [self] + company_names = [company.player_id for company in companies] - 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 + company_choice = self.display_menu("Enter the company number to trade stocks:", company_names, term) + company_id = company_names[company_choice] 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)) @@ -676,23 +389,8 @@ class Company: 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()) - 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 @@ -700,21 +398,17 @@ class AICompany(Company): self.average_prices = {product: market.products[product] for product in market.products} def update_average_prices(self): - """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) - else: # Low-Risk AI - self.low_risk_decision(market) + def make_decision(self, market, competitors, term): + if self.risk_tolerance > 0.5: + self.high_risk_decision(market, term) + else: + self.low_risk_decision(market, term) - def low_risk_decision(self, market): - """ - Defines the decision-making process for a low-risk AI player. - """ + def low_risk_decision(self, market, term): if market.current_turn == 1: self.buy_product('Coal', self.cash / 3) elif market.current_turn == 2: @@ -722,10 +416,7 @@ class AICompany(Company): elif market.current_turn >= 3: self.buy_and_craft() - def high_risk_decision(self, market): - """ - Defines the decision-making process for a high-risk AI player. - """ + def high_risk_decision(self, market, term): if market.current_turn == 1: self.sell_own_shares(market) self.buy_product('Coal', self.cash / 2) @@ -739,9 +430,6 @@ class AICompany(Company): 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) @@ -769,33 +457,21 @@ class AICompany(Company): 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 + amount_to_sell = int(self.own_stock_ownership[self.player_id] * 0.25) 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): - """ - 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): - """ - 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) @@ -803,9 +479,6 @@ class AICompany(Company): self.actions_history.append(action) 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('Manual Synthesis') @@ -816,48 +489,176 @@ class AICompany(Company): print("Not enough resources to craft using Manual Synthesis") -def print_ai_actions(ai_competitors): - """Displays the actions history, current cash, inventory, and stock information 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()]) +def apply_effects(player, effects, narrative_game): + for effect in effects: + for key, value in effect.items(): + if key == 'employee_morale': + player.employee_morale += value + elif key == 'company_budget': + player.cash += value + elif key == 'company_reputation': + player.reputation += value + elif key == 'set_flag': + player.flags[value] = True + elif key == 'set_variable': + if value in narrative_game.scenes[narrative_game.current_scene].get('Names', {}): + narrative_game.set_random_name( + narrative_game.scenes[narrative_game.current_scene]['Names'][value], value + ) + else: + narrative_game.variables[value] = value - # List all actions - for idx, action in enumerate(ai.actions_history, 1): - panel_text.append(f"{idx}. Action: {action}\n", style="grey50") +def handle_narrative_event(player, narrative_game): + # Iterate through the scenes to find the applicable one + for scene_name, scene in narrative_game.scenes.items(): + if all(player.flags.get(cond, 0) == value for cond, value in scene.get('Conditions', {}).items()): + narrative_game.current_scene = scene_name + break - # Append current cash and inventory to the text - panel_text.append(f"\nCurrent Cash: {ai.cash:.2f} €\n", style="bold blue") - panel_text.append(f"Inventory: {inventory_text}\n", style="bold cyan") + # Ensure variables are set before displaying content + scene = narrative_game.scenes[narrative_game.current_scene] + for option in scene['Links']: + for effect in option['Effects']: + if 'set_variable' in effect: + variable_name = effect['set_variable'] + if variable_name in scene.get('Names', {}): + narrative_game.set_random_name(scene['Names'][variable_name], variable_name) - # Display Shareholders and Investments - shareholders_text = ', '.join( - [f"{company}: {ownership} shares" for company, ownership in ai.own_stock_ownership.items()]) - investments_text = ', '.join( - [f"{company}: {holding} shares" for company, holding in ai.stock_holdings.items() if holding > 0]) + # Display the content with variables replaced + narrative_game.display_text(narrative_game.scenes[narrative_game.current_scene]['Content']) - panel_text.append(f"Shareholders: {shareholders_text}\n", style="bold green") - panel_text.append(f"Investments: {investments_text}", style="bold magenta") - - # Display in a panel - console.print(Panel(panel_text, title=f"[bold]{ai.player_id}'s Status", border_style="grey50")) + # Handle user input and apply effects + while True: + scene = narrative_game.scenes[narrative_game.current_scene] + if not scene['Links']: + narrative_game.display_text("The End. Thank you for playing!") + break + choice = narrative_game.handle_input(scene['Links']) + selected_option = scene['Links'][choice] + apply_effects(player, selected_option['Effects'], narrative_game) + narrative_game.current_scene = selected_option['Target'] + break -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") +class Player(Company): + def __init__(self, player_id, competitors_ids, market): + super().__init__(player_id, competitors_ids, market) + self.flags = {"daughterevent": 0} + self.variables = {} + +class TUIGame: + def __init__(self, config_path): + self.term = Terminal() + self.load_config(config_path) + self.scenes = {scene['PassageName']: scene for scene in self.config} + self.current_scene = None + self.variables = {} + + def load_config(self, path): + with open(path, 'r') as file: + self.config = yaml.safe_load(file) + + def display_text(self, text): + text = self.replace_variables(text) + print(self.term.home + self.term.clear + self.term.move_y(self.term.height // 2)) + for line in text.split('\n'): + print(self.term.center(line)) + print(self.term.move_down(2) + self.term.center("Press Enter to continue...")) + + with self.term.cbreak(), self.term.hidden_cursor(): + while True: + key = self.term.inkey() + if key.name == 'KEY_ENTER': + break + + def display_choices(self, scene, selected_choice): + content = self.replace_variables(scene['Content']) + print(self.term.home + self.term.clear + self.term.move_y(self.term.height // 2)) + print(self.term.center(content)) + for i, link in enumerate(scene['Links']): + prefix = '-> ' if i == selected_choice else ' ' + description = link.get('EffectDescription', '') + option_text = f"{prefix}{link['Option']}" + if i == selected_choice: + print(self.term.move_down(2) + self.term.center(self.term.bold(option_text))) + print(self.term.center(self.term.gray(description))) + else: + print(self.term.move_down(2) + self.term.center(option_text)) + + def handle_input(self, choices): + selected_choice = 0 + with self.term.cbreak(), self.term.hidden_cursor(): + while True: + self.display_choices(self.scenes[self.current_scene], selected_choice) + key = self.term.inkey() + if key.name == 'KEY_UP': + selected_choice = (selected_choice - 1) % len(choices) + elif key.name == 'KEY_DOWN': + selected_choice = (selected_choice + 1) % len(choices) + elif key.name == 'KEY_ENTER': + return selected_choice + + def replace_variables(self, text): + for var, value in self.variables.items(): + text = text.replace(f'{{{{ {var} }}}}', value) + return text + + def set_random_name(self, names_list, variable_name): + name = random.choice(names_list) + self.variables[variable_name] = name + + +def main(): + term = Terminal() + competitors_ids = ["Player", "RationalAI", "RiskTakingAI"] + market = Market() + + player = Player("Player", [cid for cid in competitors_ids if cid != "Player"], market) + player.employee_morale = 0 + player.reputation = 0 + + 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]} + + narrative_game = TUIGame('narrative_scenes.yml') + + for turn in range(1, 27): + print(term.home + term.clear + term.move_y(term.height // 2)) + print(term.center(f"\n--- Turn {turn} ---\n")) + + market.update_market() + market.print_market_events(term) + + for company in [player] + ai_competitors: + company.update_crafting() + company.make_decision(market, ai_competitors, term) + + handle_narrative_event(player, narrative_game) + + final_scores = market.calculate_final_scores() + print_final_scores(term, final_scores, market) + +def print_final_scores(term, final_scores, market): + score_table = ( + f"\n{'Company':<15} {'Final Score':<15} {'Majority Owner':<20} {'Ownership Percentage':<20}\n" + f"{'-'*70}\n" + ) 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) + score_table += f"{company_id:<15} {data['score']:<15.0f} {data['majority_owner']:<20} {ownership_percentage:<20}\n" + print(term.home + term.clear + term.move_y(term.height // 2)) + print(term.center(score_table)) +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) if __name__ == "__main__": main() diff --git a/narrative_scenes.yml b/narrative_scenes.yml new file mode 100644 index 0000000..8ebd218 --- /dev/null +++ b/narrative_scenes.yml @@ -0,0 +1,443 @@ +- PassageName: EmployeeRequest + Content: >- + After a long day at work, you find yourself chatting with one of your employees. {{ dad_name }} seems eager but anxious as they share a personal request. + + "My daughter just graduated and is looking for a job. She's hardworking and eager to learn. I was wondering if you could consider hiring her?" + + You ponder the request, knowing that your decision could impact both the employee's morale and the company's dynamics. + + Conditions: + daughterevent: 0 + Names: + daughter_name: + - Emily + - Sarah + - Jessica + - Rachel + - Laura + dad_name: + - John + - Michael + - David + - Robert + - James + Links: + - Option: "Sure, I’ll hire her." + Target: Hired + Effects: + - employee_morale: +10 + - company_budget: -100 + - set_flag: daughterevent + - set_flag: daughter_hired + - set_variable: daughter_name + - set_variable: dad_name + EffectDescription: "This will boost the employee's morale but will cost the company some money." + + - Option: "Sorry, we don’t have any openings." + Target: NoHiring + Effects: + - employee_morale: -5 + - set_flag: daughterevent + - set_variable: dad_name + EffectDescription: "This might disappoint the employee and affect their morale." + + - Option: "I’ll consider it, but no promises." + Target: Considered + Effects: + - employee_morale: +1 + - set_flag: interview_scheduled + - set_flag: daughterevent + - set_variable: daughter_name + - set_variable: dad_name + EffectDescription: "This will slightly improve morale and schedule an interview." + +- PassageName: Interview + Content: >- + The next day, you schedule an interview with {{ daughter_name }}. She arrives on time, dressed professionally and eager to impress. + + "Thank you for considering me for this position," she says with a warm smile. "I'm excited about the opportunity to contribute to your company." + + You now have to decide how to proceed with the interview. + + Conditions: + interview_scheduled: 1 + Links: + - Option: "Offer her the job." + Target: JobOffered + Effects: + - employee_morale: +5 + - company_budget: -100 + - set_flag: daughter_hired + - set_flag: interview_scheduled_done + EffectDescription: "Hiring her will improve morale but will require a financial investment." + + - Option: "Politely decline." + Target: Declined + Effects: + - employee_morale: -1 + - set_flag: interview_scheduled_done + EffectDescription: "Declining might slightly decrease morale." + +- PassageName: OfficeEncounter + Content: >- + While walking through the office, you see {{ daughter_name }} hard at work. She looks up and smiles as you pass by. + + "Thank you again for this opportunity," she says. "I won't let you down." + + Her dedication is evident, and it seems she's fitting in well with the team. + + Conditions: + daughter_hired: 1 + Links: + - Option: "You're doing great. Keep it up." + Target: Encouraged + Effects: + - employee_morale: +2 + EffectDescription: "This will boost her morale." + + - Option: "Let's discuss your progress." + Target: DiscussProgress + Effects: + - employee_morale: +3 + - set_flag: progress_meeting + EffectDescription: "This will slightly improve morale and set up a progress meeting." + +- PassageName: Encouraged + Content: >- + {{ daughter_name }} beams at your encouragement and gets back to work with renewed energy. + + Conditions: + daughter_hired: 1 + progress_meeting: 0 + Links: + - Option: "Continue." + Target: Continue + Effects: [] + EffectDescription: "" + +- PassageName: DiscussProgress + Content: >- + You sit down with {{ daughter_name }} to discuss her progress. She shares her achievements and goals for the future. + + "I've learned so much already," she says. "I have some ideas I'd like to share with you." + + Conditions: + progress_meeting: 1 + daughter_hired: 1 + Links: + - Option: "Listen to her ideas." + Target: IdeasShared + Effects: + - employee_morale: +5 + - company_reputation: +2 + EffectDescription: "Listening to her ideas will boost her morale and improve company reputation." + + - Option: "Encourage her to keep learning." + Target: KeepLearning + Effects: + - employee_morale: +3 + EffectDescription: "Encouraging her to keep learning will slightly improve morale." + +- PassageName: IdeasShared + Content: >- + {{ daughter_name }} shares innovative ideas that could benefit the company. You feel impressed by her creativity and drive. + + Conditions: + progress_meeting: 1 + daughter_hired: 1 + Links: + - Option: "Implement some of her ideas." + Target: IdeasImplemented + Effects: + - company_reputation: +5 + - company_budget: -50 + EffectDescription: "Implementing her ideas will improve company reputation but will cost some money." + + - Option: "Thank her and conclude the meeting." + Target: MeetingConcluded + Effects: + - employee_morale: +2 + EffectDescription: "Thanking her will slightly improve morale." + +- PassageName: KeepLearning + Content: >- + You encourage {{ daughter_name }} to keep learning and growing in her role. She nods enthusiastically, ready to take on new challenges. + + Conditions: + progress_meeting: 1 + daughter_hired: 1 + Links: + - Option: "Continue." + Target: Continue + Effects: [] + EffectDescription: "" + +- PassageName: IdeasImplemented + Content: >- + Implementing {{ daughter_name }}'s ideas proves beneficial. The company sees improvements in efficiency and innovation. + + Conditions: + progress_meeting: 1 + daughter_hired: 1 + Links: + - Option: "Celebrate the success." + Target: Continue + Effects: [] + EffectDescription: "" + +- PassageName: MeetingConcluded + Content: >- + You thank {{ daughter_name }} for her ideas and conclude the meeting. She leaves with a sense of accomplishment. + + Conditions: + progress_meeting: 1 + daughter_hired: 1 + Links: + - Option: "Continue." + Target: Continue + Effects: [] + EffectDescription: "" + +- PassageName: UnexpectedAudit + Content: >- + The government has announced an unexpected audit of your company. Inspectors will be arriving tomorrow to review your financial records and operations. + + This sudden development requires immediate attention and preparation. How will you handle this situation? + + Conditions: + audit_done: 0 + Links: + - Option: "Cooperate fully and provide all requested documents." + Target: Cooperate + Effects: + - company_reputation: +5 + - company_budget: -50 + - set_flag: audit_done + EffectDescription: "Cooperating will improve your reputation but will incur some costs." + + - Option: "Attempt to delay the audit." + Target: DelayAudit + Effects: + - company_reputation: -5 + - company_budget: -20 + - set_flag: audit_done + EffectDescription: "Delaying the audit will harm your reputation and cost some money." + +- PassageName: MarketOpportunity + Content: >- + You receive an intriguing proposal from a potential business partner. They offer an opportunity to expand into a new market, which could significantly boost your profits. + + However, this expansion involves considerable risk and investment. What will you do? + + Conditions: {} + Links: + - Option: "Accept the proposal and expand into the new market." + Target: MarketExpanded + Effects: + - company_reputation: +10 + - company_budget: -200 + - set_flag: expansion_success_check + EffectDescription: "Accepting will improve reputation significantly but will require a large investment." + + - Option: "Decline the proposal and focus on existing markets." + Target: MarketDeclined + Effects: + - company_reputation: -1 + EffectDescription: "Declining will slightly harm your reputation." + +- PassageName: MarketExpansionSuccess + Content: >- + The market expansion has been a success! Your company's products are well received in the new market, and profits are on the rise. + + This success story boosts your company's reputation and financial standing. + + Conditions: + expansion_success_check: 1 + Links: + - Option: "Celebrate and continue growing." + Target: GrowthContinues + Effects: + - company_reputation: +10 + - company_budget: +300 + EffectDescription: "Celebrating the success will greatly improve reputation and boost your budget." + +- PassageName: MarketExpansionFailure + Content: >- + Unfortunately, the market expansion did not go as planned. The new market proved to be more challenging than anticipated, and the investment has not paid off. + + This setback impacts your company's reputation and financial standing. + + Conditions: + expansion_success_check: 1 + Links: + - Option: "Regroup and refocus on core markets." + Target: Refocus + Effects: + - company_reputation: -5 + - company_budget: -100 + EffectDescription: "Refocusing will hurt your reputation and reduce your budget." + +- PassageName: NewEmployeeConflict + Content: >- + A conflict arises between two employees, causing tension in the office. As the manager, you need to address the situation to maintain a healthy work environment. + + "I can't work with them anymore," one employee says. "They are making my job impossible!" + + Conditions: + conflict_resolved: 0 + Links: + - Option: "Mediating the conflict." + Target: MediateConflict + Effects: + - employee_morale: +5 + - set_flag: conflict_resolved + EffectDescription: "Mediating will improve overall morale." + + - Option: "Taking disciplinary action." + Target: DisciplinaryAction + Effects: + - employee_morale: -5 + - set_flag: conflict_resolved + EffectDescription: "Disciplinary action will harm morale." + +- PassageName: MediateConflict + Content: >- + You mediate the conflict between the two employees, helping them reach a mutual understanding. The atmosphere in the office improves as a result. + + Conditions: + conflict_resolved: 1 + Links: + - Option: "Continue." + Target: Continue + Effects: [] + EffectDescription: "" + +- PassageName: DisciplinaryAction + Content: >- + You take disciplinary action against the employee causing the conflict. While the issue is resolved, it causes some unrest among other employees. + + Conditions: + conflict_resolved: 1 + Links: + - Option: "Continue." + Target: Continue + Effects: [] + EffectDescription: "" + +- PassageName: EmployeeAppreciation + Content: >- + You decide to show appreciation to your employees for their hard work by organizing a small office party. The gesture boosts morale and creates a positive atmosphere. + + Conditions: + appreciation_done: 0 + Links: + - Option: "Celebrate with your team." + Target: Celebration + Effects: + - employee_morale: +10 + - company_budget: -50 + - set_flag: appreciation_done + EffectDescription: "Celebrating will boost morale and cost some money." + +- PassageName: Celebration + Content: >- + The office party is a success! Employees feel appreciated and motivated to continue giving their best effort. + + Conditions: + appreciation_done: 1 + Links: + - Option: "Continue." + Target: Continue + Effects: [] + EffectDescription: "" + +- PassageName: UnexpectedResignation + Content: >- + One of your key employees unexpectedly resigns, leaving a crucial role vacant. You need to act quickly to find a replacement and ensure a smooth transition. + + Conditions: + resignation_done: 0 + Links: + - Option: "Promote an existing employee." + Target: PromoteEmployee + Effects: + - employee_morale: +5 + - set_flag: resignation_done + EffectDescription: "Promoting internally will boost morale." + + - Option: "Hire externally." + Target: HireExternally + Effects: + - company_budget: -100 + - set_flag: resignation_done + EffectDescription: "Hiring externally will cost some money." + +- PassageName: PromoteEmployee + Content: >- + You promote an existing employee to fill the vacant role. The transition goes smoothly, and the promoted employee is eager to prove themselves. + + Conditions: + resignation_done: 1 + Links: + - Option: "Continue." + Target: Continue + Effects: [] + EffectDescription: "" + +- PassageName: HireExternally + Content: >- + You hire a new employee from outside the company to fill the vacant role. It takes some time for them to adjust, but they bring valuable skills and experience to the team. + + Conditions: + resignation_done: 1 + Links: + - Option: "Continue." + Target: Continue + Effects: [] + EffectDescription: "" + +- PassageName: UnexpectedPromotion + Content: >- + An employee has gone above and beyond their duties, showing exceptional performance. You consider promoting them to a higher position. + + Conditions: + promotion_done: 0 + Links: + - Option: "Promote the employee." + Target: PromotionSuccess + Effects: + - employee_morale: +15 + - company_reputation: +5 + - set_flag: promotion_done + EffectDescription: "Promoting the employee will greatly boost morale and improve company reputation." + + - Option: "Delay the promotion." + Target: PromotionDelayed + Effects: + - employee_morale: -5 + - set_flag: promotion_done + EffectDescription: "Delaying the promotion will harm morale." + +- PassageName: PromotionSuccess + Content: >- + The promoted employee is thrilled and expresses their gratitude. Their performance continues to excel, setting a positive example for the team. + + Conditions: + promotion_done: 1 + Links: + - Option: "Continue." + Target: Continue + Effects: [] + EffectDescription: "" + +- PassageName: PromotionDelayed + Content: >- + The employee is visibly disappointed but continues to perform their duties. The morale in the office takes a slight hit. + + Conditions: + promotion_done: 1 + Links: + - Option: "Continue." + Target: Continue + Effects: [] + EffectDescription: ""