Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Praxisbeispiel: Eine Wirtschaftssimulation

Heinrich-Heine Universität Düsseldorf
import sys
import os
from dataclasses import dataclass, field
from itertools import cycle
from enum import Enum, auto
from typing import Optional

class GameException(Exception):
    """Base class for all game-related errors."""
    pass

class NotEnoughMoneyException(GameException):
    pass

class NotEnoughStockException(GameException):
    pass

class ProductType(Enum):
    FOOD = auto()
    # Später auch andere Produkte

    def __str__(self):
        return self.name.title()

@dataclass
class Inventory:
    """Each player has one inventory"""
    money: float
    stock: dict[ProductType, int] = field(default_factory=dict)

    def execute_trade(self, product: ProductType, quantity: int, unit_price: float):
        """
        Unified method for Buying AND Selling.
        - Positive quantity = BUY (Money down, Stock up)
        - Negative quantity = SELL (Money up, Stock down)
        """
        cost = quantity * unit_price

        # 1. Validation Logic
        if quantity > 0:  # BUYING
            if self.money < cost:
                raise NotEnoughMoneyException(f"Need {cost:.2f}€, have {self.money:.2f}€")
        
        elif quantity < 0: # SELLING
            # check current stock. absolute value needed because quantity is negative
            current_stock = self.stock.get(product, 0)
            if current_stock < abs(quantity):
                raise NotEnoughStockException(f"Not enough {product.name} to sell.")

        # 2. Execution Logic (Only runs if Validation passes)
        self.money -= cost
        current_qty = self.stock.get(product, 0)
        self.stock[product] = current_qty + quantity

    def __str__(self) -> str:
        """Creates a pretty, human-readable string for the player."""
        money_display = f"{self.money:.2f} €"
        items_list = [f"{product.name.title()}: {amount}"
                      for product, amount in self.stock.items()
                      if amount > 0]
        stock_display = ", ".join(items_list) if items_list else "Empty"
        return f"[Money: {money_display} | Stock: {stock_display}]"

@dataclass
class Market:
    """For each product type, there is one market"""
    product: ProductType
    current_price: float
    volatility: float

    def adjust_price(self, direction: int) -> None:
        """
        Adjusts price based on supply/demand.
        direction: 1 for Buy (Demand up), -1 for Sell (Supply up).
        """
        change = direction * self.volatility
        new_price = self.current_price + change
        self.current_price = max(1.0, round(new_price, 2))
class EconomyEngine:
    def __init__(self):
        # 1. Initialize State
        self.player = Inventory(money=100.0)
        self.food_market = Market(
            product=ProductType.FOOD, 
            current_price=10.0, 
            volatility=0.5
        )
        self.history: list[tuple] = []
        self.market_trend = cycle([0.2, 0.2, -0.1, -0.3])

    def interact_with_market(self, is_buy: bool, quantity: int) -> tuple[str, float]:
        """
        Executes the trade logic. 
        Returns details of the transaction (Action Name, Total Price) 
        or raises an exception.
        """
        market = self.food_market
        # Math: Buy is positive quantity, Sell is negative
        signed_qty = quantity if is_buy else -quantity
        
        # 1. Execute logic (Raises exceptions if invalid)
        self.player.execute_trade(market.product, signed_qty, market.current_price)
        
        # 2. Update History
        action_name = "BUY" if is_buy else "SELL"
        self.history.append((action_name, quantity, market.current_price))
        
        # 3. Update Market Price (Supply/Demand)
        # +1 direction for Buy, -1 for Sell
        direction = 1 if is_buy else -1
        market.adjust_price(direction)
        
        return action_name, market.current_price

    def tick(self):
        """Advances the simulation by one step (Background market forces)."""
        trend = next(self.market_trend)
        new_price = self.food_market.current_price + trend
        self.food_market.current_price = max(1.0, round(new_price, 2))
class TerminalUI:
    def __init__(self, engine: EconomyEngine):
        self.engine = engine

    def parse_command(self, raw_input: str) -> tuple[str, int]:
        """Parses 'b 5' into ('b', 5). Handles errors internally."""
        parts = raw_input.split()
        if not parts:
            raise ValueError("Empty command")
        
        command = parts[0]
        amount = int(parts[1]) if len(parts) > 1 else 1
        return command, amount

    def render(self):
        """Visualizes the Engine state."""
        # Accessing data from the Engine to display it
        m = self.engine.food_market
        
        print("-" * 40)
        print(f"MARKET: {m.product.name.title()} @ {m.current_price:.2f} €")
        print("-" * 40)
        print(self.engine.player) # Uses Inventory.__str__
        print("-" * 40)
        # Formatting the raw history data for the user
        last_3 = self.engine.history[-3:]
        print(f"History: {last_3}")
        print("-" * 40)
        print("COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)")

    def run(self):        
        while True:
            self.render()
            user_input = input(">> ").strip().lower()

            if user_input == 'q':
                print("Exiting...")
                break

            try:
                cmd, amount = self.parse_command(user_input)
                
                if cmd == 'b':
                    action, price = self.engine.interact_with_market(is_buy=True, quantity=amount)
                    print(f"\n[SUCCESS] {action} {amount} units @ {price:.2f}\n")
                elif cmd == 's':
                    action, price = self.engine.interact_with_market(is_buy=False, quantity=amount)
                    print(f"\n[SUCCESS] {action} {amount} units @ {price:.2f}\n")
                else:
                    print("\n[!] Unknown command.\n")
                    
                # Advance the game world
                self.engine.tick()

            except ValueError:
                print("\n[!] Invalid input format. Use 'b 5' or 's 1'.\n")
            except GameException as e:
                # Catching the logic errors from the Engine
                print(f"\n[!] TRANSACTION FAILED: {e}\n")
if __name__ == "__main__":
    # In a Jupyter notebook, this blocks the cell until 'q' is pressed.
    logic = EconomyEngine()
    app = TerminalUI(logic)
    app.run()
----------------------------------------
MARKET: Food @ 10.00 €
----------------------------------------
[Money: 100.00 € | Stock: Empty]
----------------------------------------
History: []
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  b

[SUCCESS] BUY 1 units @ 10.50

----------------------------------------
MARKET: Food @ 10.70 €
----------------------------------------
[Money: 90.00 € | Stock: Food: 1]
----------------------------------------
History: [('BUY', 1, 10.0)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  b

[SUCCESS] BUY 1 units @ 11.20

----------------------------------------
MARKET: Food @ 11.40 €
----------------------------------------
[Money: 79.30 € | Stock: Food: 2]
----------------------------------------
History: [('BUY', 1, 10.0), ('BUY', 1, 10.7)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  s 2

[SUCCESS] SELL 2 units @ 10.90

----------------------------------------
MARKET: Food @ 10.80 €
----------------------------------------
[Money: 102.10 € | Stock: Empty]
----------------------------------------
History: [('BUY', 1, 10.0), ('BUY', 1, 10.7), ('SELL', 2, 11.4)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  s 2

[!] TRANSACTION FAILED: Not enough FOOD to sell.

----------------------------------------
MARKET: Food @ 10.80 €
----------------------------------------
[Money: 102.10 € | Stock: Empty]
----------------------------------------
History: [('BUY', 1, 10.0), ('BUY', 1, 10.7), ('SELL', 2, 11.4)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  

[!] Invalid input format. Use 'b 5' or 's 1'.

----------------------------------------
MARKET: Food @ 10.80 €
----------------------------------------
[Money: 102.10 € | Stock: Empty]
----------------------------------------
History: [('BUY', 1, 10.0), ('BUY', 1, 10.7), ('SELL', 2, 11.4)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  

[!] Invalid input format. Use 'b 5' or 's 1'.

----------------------------------------
MARKET: Food @ 10.80 €
----------------------------------------
[Money: 102.10 € | Stock: Empty]
----------------------------------------
History: [('BUY', 1, 10.0), ('BUY', 1, 10.7), ('SELL', 2, 11.4)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  b

[SUCCESS] BUY 1 units @ 11.30

----------------------------------------
MARKET: Food @ 11.00 €
----------------------------------------
[Money: 91.30 € | Stock: Food: 1]
----------------------------------------
History: [('BUY', 1, 10.7), ('SELL', 2, 11.4), ('BUY', 1, 10.8)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  s

[SUCCESS] SELL 1 units @ 10.50

----------------------------------------
MARKET: Food @ 10.70 €
----------------------------------------
[Money: 102.30 € | Stock: Empty]
----------------------------------------
History: [('SELL', 2, 11.4), ('BUY', 1, 10.8), ('SELL', 1, 11.0)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  b

[SUCCESS] BUY 1 units @ 11.20

----------------------------------------
MARKET: Food @ 11.40 €
----------------------------------------
[Money: 91.60 € | Stock: Food: 1]
----------------------------------------
History: [('BUY', 1, 10.8), ('SELL', 1, 11.0), ('BUY', 1, 10.7)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  s

[SUCCESS] SELL 1 units @ 10.90

----------------------------------------
MARKET: Food @ 10.80 €
----------------------------------------
[Money: 103.00 € | Stock: Empty]
----------------------------------------
History: [('SELL', 1, 11.0), ('BUY', 1, 10.7), ('SELL', 1, 11.4)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  b

[SUCCESS] BUY 1 units @ 11.30

----------------------------------------
MARKET: Food @ 11.00 €
----------------------------------------
[Money: 92.20 € | Stock: Food: 1]
----------------------------------------
History: [('BUY', 1, 10.7), ('SELL', 1, 11.4), ('BUY', 1, 10.8)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  s

[SUCCESS] SELL 1 units @ 10.50

----------------------------------------
MARKET: Food @ 10.70 €
----------------------------------------
[Money: 103.20 € | Stock: Empty]
----------------------------------------
History: [('SELL', 1, 11.4), ('BUY', 1, 10.8), ('SELL', 1, 11.0)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  b

[SUCCESS] BUY 1 units @ 11.20

----------------------------------------
MARKET: Food @ 11.40 €
----------------------------------------
[Money: 92.50 € | Stock: Food: 1]
----------------------------------------
History: [('BUY', 1, 10.8), ('SELL', 1, 11.0), ('BUY', 1, 10.7)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  s

[SUCCESS] SELL 1 units @ 10.90

----------------------------------------
MARKET: Food @ 10.80 €
----------------------------------------
[Money: 103.90 € | Stock: Empty]
----------------------------------------
History: [('SELL', 1, 11.0), ('BUY', 1, 10.7), ('SELL', 1, 11.4)]
----------------------------------------
COMMANDS: 'b 1' (buy), 's 5' (sell), 'q' (quit)
>>  q
Exiting...