marketui/main.py
Isaak 84c4310c78
- Docstrings added
- Various bug fixes and code optimisations
2023-11-18 17:29:17 +01:00

604 lines
26 KiB
Python

"""
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 load_json(filename):
"""Loads JSON data from a file."""
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:
"""Manages market dynamics, including companies, products, stock transactions, and economic indicators."""
def __init__(self):
self.current_turn = 0
self.companies = {}
self.products = {'A': 10.0, 'B': 15.0, 'C': 20.0}
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 = {}
# Economic indicators
self.inflation_rate = 0.02 # Example starting inflation rate (2%)
self.unemployment_rate = 0.05 # Example starting unemployment rate (5%)
self.gdp = 500000 # Example starting GDP value
def update_stock_ledger(self, company_id, owner_id, amount):
"""Updates the ledger with the given stock transaction details."""
key = (company_id, owner_id)
self.stock_ledger[key] = self.stock_ledger.get(key, 0) + amount
def get_stock_ownership(self, company_id, owner_id):
"""Retrieves stock ownership details for a specified company and owner."""
return self.stock_ledger.get((company_id, owner_id), 0)
def get_stock_price(self, company_id):
"""Returns the current stock price of the specified company."""
company = self.companies[company_id]
company_value = company.value # No argument passed
return round(company_value / 100.0, 2)
def update_market(self):
"""Applies market events, adjusts prices, and updates economic indicators for the current turn."""
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):
"""Adjusts market product prices based on inflation and other factors."""
for product, price in self.products.items():
inflation_adjustment = price * self.inflation_rate
new_price = price + inflation_adjustment
# small random fluctuation for realism
fluctuation = random.uniform(-0.03, 0.03)
new_price += fluctuation
new_price = max(1.5, new_price)
self.products[product] = round(new_price, 2)
return self.products
def handle_competitor_event(self, effect):
"""Handles market changes due to new or exiting competitors."""
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 market's economic indicators like inflation and unemployment rates."""
# Update the inflation rate based on market conditions
self.inflation_rate += random.uniform(-0.01, 0.01) # Random fluctuation
self.inflation_rate = max(0.5, self.inflation_rate) # Ensure non-negative
# Update the unemployment rate
self.unemployment_rate += random.uniform(-0.005, 0.005) # Random fluctuation
self.unemployment_rate = min(max(self.unemployment_rate, 0), 1) # Bound between 0 and 1
# Update GDP based on market performance
self.gdp += self.gdp * (random.uniform(-0.01, 0.03) + self.inflation_rate)
def calculate_final_scores(self):
"""Calculates and returns final scores based on company values and stock ownership."""
final_scores = {}
for company_id, company in self.companies.items():
# Use the 'value' property instead of the 'get_value' method
final_score = company.value
# Determine the majority owner
majority_owner = max(company.own_stock_ownership,
key=lambda owner: (company.own_stock_ownership[owner], owner))
majority_percentage = company.own_stock_ownership[majority_owner]
# Check if the majority owner owns 51% or more
is_major_owner = majority_percentage >= 51
# Initialize or update the score
if company_id not in final_scores:
final_scores[company_id] = {'score': 0, 'note': '', 'majority_owner': majority_owner}
# Distribute scores
for owner_id, percentage in company.own_stock_ownership.items():
if percentage > 20:
score_transfer = final_score * (percentage / 100)
if owner_id not in final_scores:
final_scores[owner_id] = {'score': 0, 'note': '', 'majority_owner': ''}
final_scores[owner_id]['score'] += score_transfer
# If no one owns 51% or more, assign the remainder to the company itself
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):
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}
all_company_ids = set([player_id] + competitors_ids + ["RationalAI", "RiskTakingAI"])
self.other_stock_holdings = {cid: 0 for cid in all_company_ids}
self.total_shares = 100
self._market = market
@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 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_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 craft_product(self, recipe_key):
"""Processes crafting of a product based on the chosen recipe."""
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']})
for product, quantity in recipe['input'].items():
self.inventory[product] -= quantity
print("Crafting order placed.")
else:
print("Not enough resources to craft.")
def update_crafting(self):
"""Updates the crafting queue, completing orders as their turns conclude."""
for order in self.crafting_queue[:]:
order['turns_remaining'] -= 1
if order['turns_remaining'] == 0:
for product, quantity in order['recipe']['output'].items():
self.inventory[product] += quantity
self.crafting_queue.remove(order)
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."
self._update_stock_ownership(company_id, amount, total_value, buying=True)
message = f"Bought {amount} stocks of {company_id}."
if is_ai:
self.last_action = message
else:
print(message)
return message
def _sell_stock(self, company_id, amount, total_value, is_ai):
"""Handles the selling of stocks for the specified company."""
if self._get_stock_ownership(company_id) < amount:
return "Not enough stocks to sell."
self._update_stock_ownership(company_id, amount, total_value, buying=False)
message = f"Sold {amount} stocks of {company_id}."
if is_ai:
self.last_action = message
else:
print(message)
return message
def _calculate_available_shares(self, company_id):
"""Calculates the number of available shares for a given company."""
if company_id == self.player_id:
return self.total_shares - sum(self.own_stock_ownership.values()) + self.own_stock_ownership[self.player_id]
else:
return self.total_shares - self.other_stock_holdings.get(company_id, 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.other_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.other_stock_holdings[company_id] += amount
else:
self.other_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 green]{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 yellow]{self.cash:.2f}")
status_table.add_row("Inventory", ', '.join(
[f"[bold magenta]{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.other_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", choices=list(actions.keys()), 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]},00 €")
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.other_stock_holdings.get(company_id, 0) > 0:
stock_info += f", Owned: {self.other_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())
class AICompany(Company):
"""Represents an AI-driven company with automated trading, crafting, and stock actions."""
def __init__(self, player_id, competitors_ids, market):
super().__init__(player_id, competitors_ids, market)
self.last_action = "None"
self.average_prices = {product: market.products[product] for product in market.products}
def update_average_prices(self):
"""Updates the average prices of products based on the current market turn."""
turn = self._market.current_turn
for product, price in self._market.products.items():
self.average_prices[product] = (self.average_prices[product] * (turn - 1) + price) / turn
def choose_best_crafting_option(self):
"""Selects the most viable crafting option based on current inventory."""
return next((recipe_key for recipe_key, recipe in crafting_recipes.items()
if all(self.inventory[prod] >= qty for prod, qty in recipe['input'].items())), None)
def perform_trade_action(self, action):
"""Performs a trade action, either buying or selling, based on the specified action."""
if action == 'sell':
self.sell_inventory_items()
elif action == 'buy':
self.buy_inventory_items()
def sell_inventory_items(self):
"""Executes the selling of inventory items."""
for product, quantity in self.inventory.items():
if quantity > 0:
self.trade_product(self._market, product, quantity, buying=False)
self.last_action = f"Sold {quantity} of {product}"
break
def buy_inventory_items(self):
"""Executes the buying of inventory items based on current market prices."""
for product, price in self._market.products.items():
if price > 0:
quantity = int(self.cash // price)
if quantity > 0:
self.trade_product(self._market, product, quantity)
self.last_action = f"Bought {quantity} of {product}"
break
def perform_stock_action(self, action):
"""Performs a stock action, buying or selling, based on a selected strategy."""
company_id, amount = self.select_stock_action(action)
if company_id:
self.trade_stock(action, self._market, company_id, amount, is_ai=True)
def select_stock_action(self, action):
"""Selects a stock action to perform, either buying or selling."""
if action == 'buy':
return self.select_stock_to_buy()
elif action == 'sell':
return self.select_stock_to_sell()
return None, 0
def select_stock_to_buy(self):
"""Chooses a company's stock to buy based on market conditions."""
company_id = random.choice(list(self._market.companies.keys()))
amount = random.randint(1, 10)
return company_id, amount
def select_stock_to_sell(self):
"""Chooses a company's stock to sell from the AI's holdings."""
owned_stocks = [(comp_id, amount) for comp_id, amount in self.other_stock_holdings.items() if amount > 0]
if owned_stocks:
company_id, _ = random.choice(owned_stocks)
amount = random.randint(1, self.other_stock_holdings[company_id])
return company_id, amount
return None, 0
def attempt_crafting(self):
"""Attempts to craft an item based on the best available crafting option."""
recipe_key = self.choose_best_crafting_option()
if recipe_key:
self.craft_product(recipe_key)
self.last_action = f"Started crafting {recipe_key}"
class RationalAI(AICompany):
"""AI strategy focused on rational and market-condition-based decisions."""
def __init__(self, player_id, competitors_ids, market):
super().__init__(player_id, competitors_ids, market)
def make_decision(self, market, competitors):
"""Makes a market decision based on current market conditions and AI strategy."""
if self.should_craft():
self.attempt_crafting()
elif self.is_market_boom() or market.current_turn > 7:
self.perform_trade_action('sell')
else:
self.perform_trade_action('buy')
self.perform_stock_action(random.choice(['buy', 'sell']))
def should_craft(self):
"""Determines if crafting is a rational choice based on market conditions."""
return random.choice([True, False]) if self.is_market_boom() else self._market.current_turn <= 7
class RiskTakingAI(AICompany):
"""AI strategy focused on high-risk, high-reward decisions in the market."""
def __init__(self, player_id, competitors_ids, market):
super().__init__(player_id, competitors_ids, market)
def make_decision(self, market, competitors):
"""Makes bold market decisions based on a high-risk approach."""
if random.choice([True, False]):
self.perform_trade_action('buy')
elif self.should_craft():
self.attempt_crafting()
else:
self.perform_trade_action('sell')
self.perform_stock_action(random.choice(['buy', 'sell']))
def should_craft(self):
"""Determines if crafting is a suitable choice, leaning towards riskier decisions."""
return random.choice([True, False]) if self.is_market_boom() else self._market.current_turn <= 7
def print_ai_actions(ai_competitors):
"""Displays the last actions taken by AI competitors in a styled console format."""
console = Console()
for ai in ai_competitors:
action_text = Text()
if "Bought" in ai.last_action or "Sold" in ai.last_action:
action_color = "green" if "Bought" in ai.last_action else "red"
action_text.append(ai.last_action, style=f"bold {action_color}")
elif "Crafting" in ai.last_action:
action_text.append(ai.last_action, style="bold blue")
else:
action_text.append(ai.last_action, style="bold")
console.print(
Panel(action_text, title=f"[bold]{ai.player_id}'s Action", border_style="bright_yellow"))
def main():
"""
Simulates a 10-turn market game between the player and two AI competitors.
"""
competitors_ids = ["Player", "RationalAI", "RiskTakingAI"]
market = Market()
player = Company("Player", [company_type for company_type in competitors_ids if company_type != "Player"], market)
other_competitors = set(competitors_ids) - {"RationalAI", "RiskTakingAI"}
ai_competitors = [
RationalAI("RationalAI", list(other_competitors), market),
RiskTakingAI("RiskTakingAI", list(other_competitors), market),
]
market.companies = {"Player": player, "RationalAI": ai_competitors[0], "RiskTakingAI": ai_competitors[1]}
for turn in range(10):
print(f"\n---\n")
market.update_market()
player.update_crafting()
player.make_decision(market, ai_competitors)
print(f"\n---\n")
for ai in ai_competitors:
ai.make_decision(market, ai_competitors)
ai.update_crafting()
print_ai_actions(ai_competitors)
print(f"\n---\n")
final_scores = market.calculate_final_scores()
console = Console()
score_table = Table(header_style="bold magenta", box=box.DOUBLE_EDGE)
score_table.add_column("Company", style="bold cyan")
score_table.add_column("Final Score", justify="right", style="bold yellow")
score_table.add_column("Majority Owner", style="bold green")
score_table.add_column("Ownership Percentage", justify="right", style="dim")
sorted_scores = sorted(final_scores.items(), key=lambda item: item[1]['score'], reverse=True)
for company_id, data in sorted_scores:
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()