Inhalt

Aktueller Ordner: /

primaten.py

#!/usr/bin/env python3
"""
Primaten – erweiterte Kultursimulation mit GUI
Eine agentenbasierte Simulation zur Erforschung kultureller Dynamiken in Primatengruppen
"""

import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import numpy as np
import random
from datetime import datetime
import csv
from PIL import Image, ImageTk, ImageDraw
import threading
import time

class Primat:
    """Klasse für einen einzelnen Primaten"""
    def __init__(self, status=0, alter=0, geschlecht=0, kultur=0, macht=0):
        self.status = status      # 0=kein Primat, 1=jung, 2=erwachsen
        self.alter = alter        # in Ticks
        self.geschlecht = geschlecht  # 1=weiblich, 2=männlich
        self.kultur = kultur      # 1-9 für verschiedene Kulturen
        self.macht = macht        # Sozialer Einfluss (1-9)

class PrimatenSimulation:
    """Hauptklasse für die Primaten-Simulation"""
    
    def __init__(self, breite=60, hoehe=60, initial_dichte=0.1):
        self.breite = breite
        self.hoehe = hoehe
        self.raum = np.empty((hoehe, breite), dtype=object)
        self.tick_index = 0
        self.history = []
        self.max_history = 5000
        self.kultur_farben = [
            None, '#e6194B', '#3cb44b', '#ffe119', '#4363d8', 
            '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c'
        ]
        self.initialisiere_raum(initial_dichte)
        
    def initialisiere_raum(self, dichte=0.1):
        """Initialisiert den Raum mit zufällig verteilten Primaten"""
        for y in range(self.hoehe):
            for x in range(self.breite):
                if random.random() < dichte:
                    # Zufällige Eigenschaften für neuen Primaten
                    geschlecht = 1 if random.random() < 0.5 else 2
                    kultur = random.randint(1, 9)
                    macht = random.randint(1, 9)
                    self.raum[y][x] = Primat(1, 0, geschlecht, kultur, macht)
                else:
                    self.raum[y][x] = Primat()
        
        self.tick_index = 0
        self.history = []
        self.berechne_statistik()
    
    def nachbarn(self, x, y):
        """Gibt die 8 Nachbarn einer Position zurück (toroidale Geometrie)"""
        nachbarn_pos = []
        for dy in [-1, 0, 1]:
            for dx in [-1, 0, 1]:
                if dx == 0 and dy == 0:
                    continue
                nx = (x + dx) % self.breite
                ny = (y + dy) % self.hoehe
                nachbarn_pos.append(self.raum[ny][nx])
        return nachbarn_pos
    
    def kind_erzeugen(self, mutter):
        """Erzeugt ein neues Kind basierend auf der Mutter"""
        geschlecht = 1 if random.random() < 0.5 else 2
        return Primat(1, 0, geschlecht, mutter.kultur, mutter.macht)
    
    def neue_generation(self, x, y):
        """Berechnet den neuen Zustand für eine Position"""
        aktuell = self.raum[y][x]
        nachbarn = self.nachbarn(x, y)
        
        # Kopie des aktuellen Zustands
        neu = Primat(aktuell.status, aktuell.alter, aktuell.geschlecht, 
                    aktuell.kultur, aktuell.macht)
        
        # Alterungsprozess
        if neu.status > 0:
            if random.random() < 0.8:
                neu.alter += 1
            if neu.alter > 19:
                neu.status = 0  # Tod
            elif neu.alter >= 3 and neu.status == 1:
                neu.status = 2  # Erwachsen werden
        
        # Geburt neuer Primaten
        if neu.status == 0:
            weibchen = [p for p in nachbarn if p.status == 2 and p.geschlecht == 1]
            maennchen = [p for p in nachbarn if p.status == 2 and p.geschlecht == 2]
            
            if weibchen and maennchen and random.random() < 0.25:
                mutter = random.choice(weibchen)
                return self.kind_erzeugen(mutter)
            
            # Spontane Entstehung (Migration)
            if random.random() < 0.001:
                geschlecht = 1 if random.random() < 0.5 else 2
                kultur = random.randint(1, 9)
                macht = random.randint(1, 9)
                return Primat(1, 0, geschlecht, kultur, macht)
        
        # Isolationstod - wenn komplett von anderen Kulturen umgeben
        if neu.status > 0:
            fremde = [p for p in nachbarn if p.kultur != neu.kultur]
            if len(fremde) == 8:  # Alle Nachbarn sind fremd
                return Primat()
        
        # Kulturelle Beeinflussung
        if neu.status == 2:
            staerkere = [p for p in nachbarn if p.status == 2 and p.macht > neu.macht]
            if staerkere:
                einflussreichster = max(staerkere, key=lambda p: p.macht)
                if random.random() < 0.3:
                    neu.kultur = einflussreichster.kultur
                    neu.macht = max(0, neu.macht - 1)
            else:
                neu.macht = min(9, neu.macht + 0.1)
        
        return neu
    
    def tick(self):
        """Führt einen Simulationsschritt durch"""
        neuer_raum = np.empty((self.hoehe, self.breite), dtype=object)
        
        for y in range(self.hoehe):
            for x in range(self.breite):
                neuer_raum[y][x] = self.neue_generation(x, y)
        
        self.raum = neuer_raum
        self.tick_index += 1
        return self.berechne_statistik()
    
    def berechne_statistik(self):
        """Berechnet Statistiken über die aktuelle Population"""
        kultur_zaehler = [0] * 9
        gesamt_population = 0
        
        for y in range(self.hoehe):
            for x in range(self.breite):
                p = self.raum[y][x]
                if p.status > 0 and p.kultur > 0:
                    kultur_zaehler[p.kultur - 1] += 1
                    gesamt_population += 1
        
        # Anteile berechnen
        anteile = [count / gesamt_population if gesamt_population > 0 else 0 
                  for count in kultur_zaehler]
        
        # Zur Historie hinzufügen
        datenpunkt = {
            'tick': self.tick_index,
            'population': gesamt_population,
            'anteile': anteile.copy(),
            'kultur_counts': kultur_zaehler.copy()
        }
        
        self.history.append(datenpunkt)
        if len(self.history) > self.max_history:
            self.history.pop(0)
        
        return anteile, gesamt_population
    
    def monokultur_erkannt(self, anteile, population):
        """Prüft, ob eine Monokultur erreicht wurde"""
        if population < 10:
            return False, None
        
        max_anteil = max(anteile)
        if max_anteil >= 0.995:
            dominante_kultur = anteile.index(max_anteil) + 1
            return True, dominante_kultur
        
        return False, None
    
    def export_csv(self, dateiname=None):
        """Exportiert die Simulationsdaten als CSV"""
        if not dateiname:
            zeitstempel = datetime.now().strftime("%Y%m%d_%H%M%S")
            dateiname = f"kulturverlauf_{zeitstempel}.csv"
        
        with open(dateiname, 'w', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile)
            # Header schreiben
            header = ['tick', 'population'] + [f'kultur_{i+1}' for i in range(9)]
            writer.writerow(header)
            
            # Daten schreiben
            for eintrag in self.history:
                zeile = [eintrag['tick'], eintrag['population']]
                zeile.extend([f"{a:.5f}" for a in eintrag['anteile']])
                writer.writerow(zeile)
        
        return dateiname

class PrimatenGUI:
    """Grafische Benutzeroberfläche für die Primaten-Simulation"""
    
    def __init__(self, root):
        self.root = root
        self.root.title("Primaten – erweiterte Kultursimulation")
        self.root.geometry("1200x800")
        
        # Simulation initialisieren
        self.simulation = PrimatenSimulation(40, 40, 0.1)
        self.laufend = False
        self.tick_intervall = 150  # ms
        
        # GUI-Elemente erstellen
        self.erste_gui()
        self.aktualisiere_anzeige()
        
    def erste_gui(self):
        """Erstellt die grafische Benutzeroberfläche"""
        # Hauptframe
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Konfiguration der Grid-Gewichtung
        self.root.columnconfigure(0, weight=1)
        self.root.rowconfigure(0, weight=1)
        main_frame.columnconfigure(1, weight=1)
        main_frame.rowconfigure(1, weight=1)
        
        # Steuerungsbereich oben
        control_frame = ttk.LabelFrame(main_frame, text="Simulationssteuerung", padding="5")
        control_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
        
        # Buttons
        btn_frame = ttk.Frame(control_frame)
        btn_frame.grid(row=0, column=0, sticky=tk.W)
        
        ttk.Button(btn_frame, text="🔀 Zufallsverteilung", 
                  command=self.zufallsverteilung).grid(row=0, column=0, padx=(0, 5))
        ttk.Button(btn_frame, text="▶ Start", 
                  command=self.start_simulation, style="Accent.TButton").grid(row=0, column=1, padx=5)
        ttk.Button(btn_frame, text="⏸ Stopp", 
                  command=self.stopp_simulation).grid(row=0, column=2, padx=5)
        
        # Einstellungen
        settings_frame = ttk.Frame(control_frame)
        settings_frame.grid(row=0, column=1, sticky=tk.E)
        
        self.auto_stopp_var = tk.BooleanVar()
        ttk.Checkbutton(settings_frame, text="Auto-Stopp bei Monokultur", 
                       variable=self.auto_stopp_var).grid(row=0, column=0, padx=5)
        
        self.zeige_population_var = tk.BooleanVar(value=True)
        ttk.Checkbutton(settings_frame, text="Population anzeigen", 
                       variable=self.zeige_population_var).grid(row=0, column=1, padx=5)
        
        # Geschwindigkeits-Steuerung
        speed_frame = ttk.Frame(control_frame)
        speed_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(5, 0))
        
        ttk.Label(speed_frame, text="Geschwindigkeit:").grid(row=0, column=0, padx=(0, 5))
        self.speed_var = tk.StringVar(value="150")
        speed_combo = ttk.Combobox(speed_frame, textvariable=self.speed_var, 
                                  values=["50", "100", "150", "250", "400"], 
                                  state="readonly", width=8)
        speed_combo.grid(row=0, column=1, padx=5)
        speed_combo.bind('<<ComboboxSelected>>', self.geschwindigkeit_aendern)
        
        # Statistik-Anzeige
        stats_frame = ttk.Frame(control_frame)
        stats_frame.grid(row=1, column=2, sticky=tk.E)
        
        self.stats_label = ttk.Label(stats_frame, 
                                   text="Tick: 0 | Population: 0 | Dominante Kultur: -")
        self.stats_label.grid(row=0, column=0)
        
        # Hauptanzeige-Bereich
        display_frame = ttk.Frame(main_frame)
        display_frame.grid(row=1, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S))
        display_frame.columnconfigure(0, weight=1)
        display_frame.columnconfigure(1, weight=1)
        display_frame.rowconfigure(0, weight=1)
        
        # Linke Seite: Visualisierungen
        left_frame = ttk.Frame(display_frame)
        left_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(0, 5))
        left_frame.columnconfigure(0, weight=1)
        left_frame.rowconfigure(0, weight=1)
        left_frame.rowconfigure(1, weight=1)
        
        # Status-Anzeige
        status_frame = ttk.LabelFrame(left_frame, text="Status (Geschlecht & Alter)")
        status_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 5))
        status_frame.columnconfigure(0, weight=1)
        status_frame.rowconfigure(0, weight=1)
        
        self.status_canvas = tk.Canvas(status_frame, width=320, height=320, bg="black")
        self.status_canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Kultur-Anzeige
        kultur_frame = ttk.LabelFrame(left_frame, text="Kulturverteilung")
        kultur_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        kultur_frame.columnconfigure(0, weight=1)
        kultur_frame.rowconfigure(0, weight=1)
        
        self.kultur_canvas = tk.Canvas(kultur_frame, width=320, height=320, bg="black")
        self.kultur_canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Rechte Seite: Diagramm und Legende
        right_frame = ttk.Frame(display_frame)
        right_frame.grid(row=0, column=1, sticky=(tk.W, tk.E, tk.N, tk.S), padx=(5, 0))
        right_frame.columnconfigure(0, weight=1)
        right_frame.rowconfigure(0, weight=3)
        right_frame.rowconfigure(1, weight=1)
        
        # Diagramm
        diagramm_frame = ttk.LabelFrame(right_frame, text="Kulturentwicklung")
        diagramm_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=(0, 5))
        diagramm_frame.columnconfigure(0, weight=1)
        diagramm_frame.rowconfigure(0, weight=1)
        
        self.diagramm_canvas = tk.Canvas(diagramm_frame, width=400, height=300, bg="black")
        self.diagramm_canvas.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Legende und Export
        bottom_right_frame = ttk.Frame(right_frame)
        bottom_right_frame.grid(row=1, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        bottom_right_frame.columnconfigure(0, weight=1)
        bottom_right_frame.rowconfigure(0, weight=1)
        bottom_right_frame.rowconfigure(1, weight=0)
        
        # Legende
        legende_frame = ttk.LabelFrame(bottom_right_frame, text="Legende")
        legende_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
        
        # Status-Legende
        status_legende_frame = ttk.Frame(legende_frame)
        status_legende_frame.grid(row=0, column=0, sticky=tk.W, padx=5, pady=5)
        
        ttk.Label(status_legende_frame, text="Status:").grid(row=0, column=0, columnspan=2, sticky=tk.W)
        
        farben_status = [
            ("weiblich, jung", "#ffb6c1"),
            ("männlich, jung", "#87cefa"), 
            ("weiblich, erwachsen", "#ff69b4"),
            ("männlich, erwachsen", "#1e90ff")
        ]
        
        for i, (text, farbe) in enumerate(farben_status):
            canvas = tk.Canvas(status_legende_frame, width=15, height=15, bg=farbe, highlightthickness=1)
            canvas.grid(row=i+1, column=0, padx=(0, 5), pady=2)
            ttk.Label(status_legende_frame, text=text).grid(row=i+1, column=1, sticky=tk.W, pady=2)
        
        # Kultur-Legende
        kultur_legende_frame = ttk.Frame(legende_frame)
        kultur_legende_frame.grid(row=0, column=1, sticky=tk.W, padx=5, pady=5)
        
        ttk.Label(kultur_legende_frame, text="Kulturen:").grid(row=0, column=0, columnspan=2, sticky=tk.W)
        
        for i in range(1, 10):
            row = (i-1) // 3 + 1
            col = (i-1) % 3
            farbe = self.simulation.kultur_farben[i]
            canvas = tk.Canvas(kultur_legende_frame, width=15, height=15, bg=farbe, highlightthickness=1)
            canvas.grid(row=row, column=col*2, padx=(5, 2), pady=2)
            ttk.Label(kultur_legende_frame, text=str(i)).grid(row=row, column=col*2+1, sticky=tk.W, pady=2)
        
        # Export-Buttons
        export_frame = ttk.Frame(bottom_right_frame)
        export_frame.grid(row=1, column=0, sticky=tk.E, pady=(5, 0))
        
        ttk.Button(export_frame, text="💾 CSV Export", 
                  command=self.export_csv).grid(row=0, column=0, padx=5)
        ttk.Button(export_frame, text="🖼 PNG Export", 
                  command=self.export_png).grid(row=0, column=1, padx=5)
    
    def hex_to_rgb(self, hex_color):
        """Wandelt Hex-Farben in RGB um"""
        hex_color = hex_color.lstrip('#')
        return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
    
    def zeichne_status(self):
        """Zeichnet die Status-Ansicht"""
        canvas = self.status_canvas
        canvas.delete("all")
        
        breite = canvas.winfo_width()
        hoehe = canvas.winfo_height()
        zell_groesse = min(breite // self.simulation.breite, hoehe // self.simulation.hoehe)
        
        farb_map = {
            (1, 1): "#ffb6c1",  # weiblich, jung
            (1, 2): "#87cefa",  # männlich, jung
            (2, 1): "#ff69b4",  # weiblich, erwachsen
            (2, 2): "#1e90ff"   # männlich, erwachsen
        }
        
        for y in range(self.simulation.hoehe):
            for x in range(self.simulation.breite):
                p = self.simulation.raum[y][x]
                if p.status > 0:
                    farbe = farb_map.get((p.status, p.geschlecht), "white")
                    x1 = x * zell_groesse
                    y1 = y * zell_groesse
                    x2 = x1 + zell_groesse
                    y2 = y1 + zell_groesse
                    canvas.create_rectangle(x1, y1, x2, y2, fill=farbe, outline="")
    
    def zeichne_kultur(self):
        """Zeichnet die Kultur-Ansicht"""
        canvas = self.kultur_canvas
        canvas.delete("all")
        
        breite = canvas.winfo_width()
        hoehe = canvas.winfo_height()
        zell_groesse = min(breite // self.simulation.breite, hoehe // self.simulation.hoehe)
        
        for y in range(self.simulation.hoehe):
            for x in range(self.simulation.breite):
                p = self.simulation.raum[y][x]
                if p.kultur > 0:
                    farbe = self.simulation.kultur_farben[p.kultur]
                    x1 = x * zell_groesse
                    y1 = y * zell_groesse
                    x2 = x1 + zell_groesse
                    y2 = y1 + zell_groesse
                    canvas.create_rectangle(x1, y1, x2, y2, fill=farbe, outline="")
    
    def zeichne_diagramm(self):
        """Zeichnet das Entwicklungsdiagramm"""
        canvas = self.diagramm_canvas
        canvas.delete("all")
        
        if len(self.simulation.history) < 2:
            return
        
        breite = canvas.winfo_width()
        hoehe = canvas.winfo_height()
        padding = 40
        
        # Fenstergröße für Diagramm
        fenster = min(len(self.simulation.history), 600)
        start_index = max(0, len(self.simulation.history) - fenster)
        daten = self.simulation.history[start_index:]
        
        if len(daten) < 2:
            return
        
        # Koordinaten berechnen
        ticks = [d['tick'] for d in daten]
        min_tick = min(ticks)
        max_tick = max(ticks)
        
        # Raster zeichnen
        canvas.create_rectangle(padding, padding, breite-padding, hoehe-padding, 
                               outline="#666666", fill="black")
        
        # Y-Achse beschriften
        for i in range(6):
            y = padding + i * (hoehe - 2*padding) / 5
            wert = 1.0 - i * 0.2
            canvas.create_text(padding - 10, y, text=f"{wert:.1f}", 
                              fill="white", anchor="e", font=("Arial", 8))
            canvas.create_line(padding, y, breite-padding, y, fill="#333333")
        
        # X-Achse beschriften
        tick_step = max(1, (max_tick - min_tick) // 5)
        for i in range(0, len(daten), max(1, len(daten)//5)):
            if i < len(daten):
                x = padding + i * (breite - 2*padding) / len(daten)
                canvas.create_text(x, hoehe - padding + 15, text=str(daten[i]['tick']), 
                                  fill="white", anchor="n", font=("Arial", 8))
        
        # Kulturlinien zeichnen
        for k in range(9):
            farbe = self.simulation.kultur_farben[k + 1]
            punkte = []
            
            for i, datenpunkt in enumerate(daten):
                x = padding + i * (breite - 2*padding) / len(daten)
                y = hoehe - padding - datenpunkt['anteile'][k] * (hoehe - 2*padding)
                punkte.extend([x, y])
            
            if len(punkte) >= 4:
                canvas.create_line(punkte, fill=farbe, width=2, smooth=True)
        
        # Populationslinie zeichnen (falls aktiviert)
        if self.zeige_population_var.get():
            max_pop = self.simulation.breite * self.simulation.hoehe
            punkte = []
            
            for i, datenpunkt in enumerate(daten):
                x = padding + i * (breite - 2*padding) / len(daten)
                y = hoehe - padding - (datenpunkt['population'] / max_pop) * (hoehe - 2*padding)
                punkte.extend([x, y])
            
            if len(punkte) >= 4:
                canvas.create_line(punkte, fill="white", width=1.5, dash=(4, 2))
    
    def aktualisiere_statistik(self):
        """Aktualisiert die Statistik-Anzeige"""
        if self.simulation.history:
            aktuell = self.simulation.history[-1]
            anteile = aktuell['anteile']
            dominante_kultur = anteile.index(max(anteile)) + 1
            
            text = f"Tick: {aktuell['tick']} | Population: {aktuell['population']} | Dominante Kultur: K{dominante_kultur}"
            
            # Monokultur-Prüfung
            mono, kultur = self.simulation.monokultur_erkannt(anteile, aktuell['population'])
            if mono:
                text += f" | MONOKULTUR: K{kultur}"
                if self.auto_stopp_var.get() and self.laufend:
                    self.stopp_simulation()
                    messagebox.showinfo("Monokultur erkannt", f"Monokultur erreicht: Kultur {kultur}")
            
            self.stats_label.config(text=text)
    
    def aktualisiere_anzeige(self):
        """Aktualisiert alle Anzeigen"""
        self.zeichne_status()
        self.zeichne_kultur()
        self.zeichne_diagramm()
        self.aktualisiere_statistik()
    
    def simulations_loop(self):
        """Haupt-Schleife für die Simulation"""
        if self.laufend:
            self.simulation.tick()
            self.aktualisiere_anzeige()
            self.root.after(self.tick_intervall, self.simulations_loop)
    
    def start_simulation(self):
        """Startet die Simulation"""
        if not self.laufend:
            self.laufend = True
            self.simulations_loop()
    
    def stopp_simulation(self):
        """Stoppt die Simulation"""
        self.laufend = False
    
    def zufallsverteilung(self):
        """Setzt eine neue Zufallsverteilung"""
        self.stopp_simulation()
        self.simulation.initialisiere_raum(0.1)
        self.aktualisiere_anzeige()
    
    def geschwindigkeit_aendern(self, event=None):
        """Ändert die Simulationsgeschwindigkeit"""
        self.tick_intervall = int(self.speed_var.get())
    
    def export_csv(self):
        """Exportiert die Daten als CSV"""
        try:
            dateiname = filedialog.asksaveasfilename(
                defaultextension=".csv",
                filetypes=[("CSV Dateien", "*.csv"), ("Alle Dateien", "*.*")],
                title="Simulationsdaten speichern"
            )
            if dateiname:
                export_datei = self.simulation.export_csv(dateiname)
                messagebox.showinfo("Export erfolgreich", f"Daten exportiert nach:\n{export_datei}")
        except Exception as e:
            messagebox.showerror("Export Fehler", f"Fehler beim Export: {str(e)}")
    
    def export_png(self):
        """Exportiert das Diagramm als PNG (vereinfacht)"""
        try:
            # Hier könnte man mit PIL ein echtes PNG erstellen
            # Für diese Version zeigen wir einfach eine Info an
            messagebox.showinfo("PNG Export", 
                              "PNG Export wird in dieser Version vereinfacht dargestellt.\n"
                              "Die Daten sind im CSV-Export enthalten.")
        except Exception as e:
            messagebox.showerror("Export Fehler", f"Fehler beim PNG-Export: {str(e)}")

def main():
    """Hauptfunktion"""
    try:
        root = tk.Tk()
        app = PrimatenGUI(root)
        root.mainloop()
    except Exception as e:
        print(f"Fehler: {e}")
        input("Drücken Sie Enter zum Beenden...")

if __name__ == "__main__":
    main()