diff --git a/README.md b/README.md index 2bd8656..41497ef 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,14 @@ Mannschaft eine Stufe darunter. stärker zurück**: mehr Wertung, eine Stufe höher, ab „schwer" mehr Mannschaft. * Verlierst du, bleibt er, und deine Wertung sinkt. -* Der Stand liegt in `user://kampagne.json`. „Neu anfangen" löscht ihn. +* Jeder Kapitän hat ein **Temperament** - Bluffer, Zauderin, Genau-Rufer, + Rechner, Eiskalt, Draufgänger - dieselben vier Regler wie die Stufen, nur + je Kopf anders. Auf dem Hafen steht dazu deine **Siegchance** nach Elo. +* Nach einem Sieg wählst du **Beute**: eine Münze deiner Wahl, die du in die + nächste Partie mitnimmst. +* Die Karte meldet, wer zurück ist und wie stark. **Ziel** ist der Rang + Admiral; danach bleibt die See offen. +* Der Stand liegt in `user://kampagne.json`, samt Name. „Neu anfangen" löscht ihn. ## Münzen diff --git a/scripts/ai.gd b/scripts/ai.gd index 1dd0394..2747438 100644 --- a/scripts/ai.gd +++ b/scripts/ai.gd @@ -65,7 +65,14 @@ const FRECH := 0.12 const GENAU_LUST := 0.35 +## Ein Kapitän der Kampagne bringt sein eigenes Temperament mit: dieselben +## vier Regler, nur andere Werte. Leer heißt: die Stufe gilt. +static var temperament: Dictionary = {} + + static func _regler() -> Dictionary: + if not temperament.is_empty(): + return temperament return STUFEN[clampi(stufe, 0, STUFEN.size() - 1)] diff --git a/scripts/game.gd b/scripts/game.gd index 4622c51..152c133 100644 --- a/scripts/game.gd +++ b/scripts/game.gd @@ -90,7 +90,7 @@ func setup(cfg: Dictionary) -> void: for i in int(cfg.ai): var b: Dictionary = bots[i] if i < bots.size() else {} seats.append({"peer": 0, "name": String(b.get("name", "Bot %d" % (i + 1))), "ai": true, - "stufe": int(b.get("stufe", DiceAI.stufe)), + "stufe": int(b.get("stufe", DiceAI.stufe)), "regler": b.get("regler", {}), "count": dice, "alive": true, "verwaist": false}) # Der Spielserver spielt nicht mit und bekommt deshalb keinen Platz. @@ -123,6 +123,9 @@ func setup(cfg: Dictionary) -> void: # Die Startmünze zieht nur der Host. Beim Client kommt sie mit dem # ersten Zustand, sonst läge dort kurz eine andere. hand[i] = [BoosterDB.zieh(rng)] if multiplayer.is_server() else [] + # Beute aus der Kampagne: eine gewählte Münze dazu, nur für den Spieler. + if i == 0 and multiplayer.is_server() and String(cfg.get("beute", "")) != "": + hand[i].append(String(cfg.beute)) table.set_dice_count(i, seats[i].count) table.set_dice_visible(i, false) table.set_cup_down(i, i != local_seat) @@ -442,6 +445,7 @@ func _phase_bid() -> Dictionary: # Erst planen, dann passend dazu eine Münze werfen. Die kann die # Lage ändern - also danach noch einmal entscheiden. DiceAI.stufe = int(seats[turn].get("stufe", DiceAI.stufe)) + DiceAI.temperament = seats[turn].get("regler", {}) var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid, must_bid.get(turn, false), rng, _bot_wissen(turn)) if await _bot_muenze(turn, a): @@ -460,6 +464,7 @@ func _phase_bid() -> Dictionary: return res else: # Zeit abgelaufen oder getrennt: automatisch entscheiden + DiceAI.temperament = {} var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid, must_bid.get(turn, false), rng, _bot_wissen(turn)) _log("%s wird automatisch gespielt." % seats[turn].name) diff --git a/scripts/kampagne.gd b/scripts/kampagne.gd index 385dfc3..cca7ab6 100644 --- a/scripts/kampagne.gd +++ b/scripts/kampagne.gd @@ -13,14 +13,23 @@ const RAENGE := [ ] ## Kapitäne mit Wertung, Bot-Stufe, Mannschaft (weitere Bots eine Stufe ## darunter) und Platz auf der Karte (0..1 in Breite und Höhe). +## `art` ist das Temperament: die vier Regler aus ai.gd, je Kopf anders. +## `wesen` steht auf der Karte. const START_KAPITAENE := [ - {"name": "Roter Jack", "elo": 900, "stufe": 0, "mannschaft": 0, "pos": Vector2(0.14, 0.32)}, - {"name": "Salz-Marie", "elo": 960, "stufe": 0, "mannschaft": 1, "pos": Vector2(0.33, 0.68)}, - {"name": "Einauge Bill", "elo": 1030, "stufe": 1, "mannschaft": 1, "pos": Vector2(0.52, 0.24)}, - {"name": "Käpt'n Grau", "elo": 1110, "stufe": 1, "mannschaft": 2, "pos": Vector2(0.70, 0.62)}, - {"name": "Die Witwe", "elo": 1210, "stufe": 2, "mannschaft": 2, "pos": Vector2(0.87, 0.30)}, - {"name": "Admiral Krähe", "elo": 1360, "stufe": 2, "mannschaft": 3, "pos": Vector2(0.50, 0.86)}, + {"name": "Roter Jack", "elo": 900, "stufe": 0, "mannschaft": 0, "pos": Vector2(0.14, 0.32), + "wesen": "Bluffer", "art": {"bluff": 0.65, "genau": 0.40, "zweifel": 0.30, "sicher": 0.30}}, + {"name": "Salz-Marie", "elo": 960, "stufe": 0, "mannschaft": 1, "pos": Vector2(0.33, 0.68), + "wesen": "Zauderin", "art": {"bluff": 0.10, "genau": 0.45, "zweifel": 0.45, "sicher": 0.40}}, + {"name": "Einauge Bill", "elo": 1030, "stufe": 1, "mannschaft": 1, "pos": Vector2(0.52, 0.24), + "wesen": "Genau-Rufer", "art": {"bluff": 0.30, "genau": 0.15, "zweifel": 0.30, "sicher": 0.42}}, + {"name": "Käpt'n Grau", "elo": 1110, "stufe": 1, "mannschaft": 2, "pos": Vector2(0.70, 0.62), + "wesen": "Rechner", "art": {"bluff": 0.20, "genau": 0.26, "zweifel": 0.26, "sicher": 0.50}}, + {"name": "Die Witwe", "elo": 1210, "stufe": 2, "mannschaft": 2, "pos": Vector2(0.87, 0.30), + "wesen": "Eiskalt", "art": {"bluff": 0.45, "genau": 0.24, "zweifel": 0.22, "sicher": 0.50}}, + {"name": "Admiral Krähe", "elo": 1360, "stufe": 2, "mannschaft": 3, "pos": Vector2(0.50, 0.80), + "wesen": "Draufgänger", "art": {"bluff": 0.55, "genau": 0.20, "zweifel": 0.28, "sicher": 0.44}}, ] +const ZIEL_RANG := "Admiral" ## wer das erreicht, hat die Kampagne gewonnen var datei := DATEI ## Tests schreiben woandershin var elo := 1000.0 @@ -31,6 +40,11 @@ var aktiv := "" ## Kapitän der laufenden Partie, "" = keine Kampagn var letztes := "" ## Text zum letzten Ergebnis, fürs Endfeld var zur_karte := false ## nach dem Ende zurück auf die Karte statt ins Hauptmenü var kampagnenpartie := false ## die laufende (oder gerade beendete) Partie gehört zur Kampagne +var spielername := "Spieler" +var meldungen: Array = [] ## die letzten Ereignisse für die Karte, neueste zuerst +var beute := "" ## gewählte Münze für die nächste Partie +var beute_offen := false ## nach einem Sieg darf eine Münze gewählt werden +var gewonnen := false ## Zielrang erreicht func _ready() -> void: @@ -55,6 +69,10 @@ func zuruecksetzen() -> void: siege = 0 aktiv = "" letztes = "" + meldungen = [] + beute = "" + beute_offen = false + gewonnen = false kapitaene = [] for k in START_KAPITAENE: var e: Dictionary = k.duplicate() @@ -78,10 +96,25 @@ func starten(name: String) -> void: return aktiv = name kampagnenpartie = true - var bots: Array = [{"name": name, "stufe": int(k.stufe)}] + Net.player_name = spielername + var bots: Array = [{"name": name, "stufe": int(k.stufe), "regler": k.art}] for i in int(k.mannschaft): bots.append({"name": "Maat %d" % (i + 1), "stufe": maxi(0, int(k.stufe) - 1)}) - Net.start_singleplayer(Net.player_name, bots.size(), int(k.stufe), 5, bots) + var mit := beute + beute = "" + beute_offen = false + Net.start_singleplayer(spielername, bots.size(), int(k.stufe), 5, bots, mit) + + +## Erwartete Siegchance gegen einen Kapitän, nach Elo. +func siegchance(k: Dictionary) -> float: + return 1.0 / (1.0 + pow(10.0, (float(k.elo) - elo) / 400.0)) + + +func _melden(text: String) -> void: + meldungen.push_front(text) + if meldungen.size() > 3: + meldungen.pop_back() ## Wird vom Spiel am Ende gerufen. `ausgeschieden` ist die Reihenfolge der @@ -125,9 +158,15 @@ func ergebnis(seats: Array, ausgeschieden: Array) -> void: siege += 1 k.weg = AUF_SEE k.geschlagen = int(k.geschlagen) + 1 + beute_offen = true letztes = "Sieg gegen %s! %d → %d (%s)" % [k.name, int(round(vorher)), int(round(elo)), mein_rang()] + _melden("%s geschlagen - auf See für %d Partien. Beute wartet." % [k.name, AUF_SEE]) else: letztes = "%s behält die Oberhand. %d → %d (%s)" % [k.name, int(round(vorher)), int(round(elo)), mein_rang()] + _melden("%s behält die Oberhand." % k.name) + if not gewonnen and mein_rang() == ZIEL_RANG: + gewonnen = true + _melden("Du bist %s. Die Kampagne ist gewonnen - die See bleibt offen." % ZIEL_RANG) speichern() @@ -138,10 +177,14 @@ func _zurueck(k: Dictionary) -> void: k.stufe = int(k.stufe) + 1 else: k.mannschaft = mini(4, int(k.mannschaft) + 1) + _melden("%s ist zurück - %s, Wertung %d, Mannschaft %d." % [ + k.name, ["leicht", "normal", "schwer"][int(k.stufe)], int(round(float(k.elo))), int(k.mannschaft)]) func speichern() -> void: - var daten := {"elo": elo, "partien": partien, "siege": siege, "kapitaene": []} + var daten := {"elo": elo, "partien": partien, "siege": siege, "kapitaene": [], + "name": spielername, "meldungen": meldungen, "beute": beute, "beute_offen": beute_offen, + "gewonnen": gewonnen} for k in kapitaene: daten.kapitaene.append({"name": k.name, "elo": k.elo, "stufe": k.stufe, "mannschaft": k.mannschaft, "weg": k.weg, "geschlagen": k.geschlagen}) @@ -162,6 +205,11 @@ func laden() -> void: elo = float(daten.get("elo", 1000.0)) partien = int(daten.get("partien", 0)) siege = int(daten.get("siege", 0)) + spielername = String(daten.get("name", "Spieler")) + meldungen = daten.get("meldungen", []) + beute = String(daten.get("beute", "")) + beute_offen = bool(daten.get("beute_offen", false)) + gewonnen = bool(daten.get("gewonnen", false)) for d in daten.get("kapitaene", []): var k := kapitaen(String(d.get("name", ""))) if k.is_empty(): diff --git a/scripts/menu.gd b/scripts/menu.gd index 6070f9b..f3bc723 100644 --- a/scripts/menu.gd +++ b/scripts/menu.gd @@ -16,6 +16,9 @@ var lobby_stufe: OptionButton # Kampagne var kamp_kopf: Label +var kamp_meldung: Label +var kamp_name: LineEdit +var beute_row: HBoxContainer var karte: Control # Mehrspieler @@ -136,8 +139,28 @@ class Seekarte extends Control: func _build_kampagne() -> Control: var box := _box(940) box.add_child(_text("Seekarte", 26, Color(1.0, 0.82, 0.32))) + var kopf := HBoxContainer.new() + kopf.add_theme_constant_override("separation", 12) + box.add_child(kopf) + kamp_name = LineEdit.new() + kamp_name.placeholder_text = "Dein Name" + kamp_name.max_length = 16 + kamp_name.custom_minimum_size = Vector2(180, 0) + kamp_name.text_changed.connect(func(t: String) -> void: + Kampagne.spielername = t.strip_edges() if t.strip_edges() != "" else "Spieler" + Kampagne.speichern()) + kopf.add_child(kamp_name) kamp_kopf = _text("", 15, Color(0.85, 0.87, 0.92)) - box.add_child(kamp_kopf) + kamp_kopf.size_flags_horizontal = Control.SIZE_EXPAND_FILL + kopf.add_child(kamp_kopf) + kamp_meldung = _text("", 13, Color(0.62, 0.64, 0.70)) + kamp_meldung.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT + box.add_child(kamp_meldung) + # Beute nach einem Sieg: eine Münze für die nächste Partie. + beute_row = HBoxContainer.new() + beute_row.add_theme_constant_override("separation", 6) + beute_row.alignment = BoxContainer.ALIGNMENT_CENTER + box.add_child(beute_row) karte = Seekarte.new() karte.custom_minimum_size = Vector2(920, 440) box.add_child(karte) @@ -153,8 +176,32 @@ func _build_kampagne() -> Control: func _karte_fuellen() -> void: - kamp_kopf.text = "Du: %s, Wertung %d · %d Partien, %d Siege" % [ - Kampagne.mein_rang(), int(round(Kampagne.elo)), Kampagne.partien, Kampagne.siege] + if kamp_name.text != Kampagne.spielername: + kamp_name.text = Kampagne.spielername + kamp_kopf.text = "%s, Wertung %d · %d Partien, %d Siege%s" % [ + Kampagne.mein_rang(), int(round(Kampagne.elo)), Kampagne.partien, Kampagne.siege, + " · KAMPAGNE GEWONNEN" if Kampagne.gewonnen else ""] + kamp_meldung.text = "\n".join(Kampagne.meldungen) if not Kampagne.meldungen.is_empty() \ + else "Wähle einen Hafen. Geschlagene Kapitäne kommen stärker zurück." + for c in beute_row.get_children(): + beute_row.remove_child(c) + c.queue_free() + if Kampagne.beute_offen: + beute_row.add_child(_text("Beute für die nächste Partie:", 14, Color(1.0, 0.82, 0.32))) + for b in BoosterDB.ALL: + var btn := Button.new() + btn.text = String(b.name) + btn.tooltip_text = String(b.desc) + btn.toggle_mode = true + btn.button_pressed = Kampagne.beute == String(b.id) + btn.pressed.connect(func() -> void: + Kampagne.beute = String(b.id) + Kampagne.speichern() + _karte_fuellen()) + beute_row.add_child(btn) + elif Kampagne.beute != "": + beute_row.add_child(_text("Beute an Bord: %s" % BoosterDB.get_booster(Kampagne.beute).name, 14, + Color(1.0, 0.82, 0.32))) for c in karte.get_children(): if c is Button: karte.remove_child(c) @@ -168,11 +215,13 @@ func _karte_fuellen() -> void: b.text = "%s\nauf See · zurück in %d" % [k.name, weg] b.disabled = true else: - b.text = "%s\n%s · %d" % [k.name, ["leicht", "normal", "schwer"][int(k.stufe)], int(round(k.elo))] + b.text = "%s · %s\n%s · %d · Chance %d %%" % [k.name, k.wesen, + ["leicht", "normal", "schwer"][int(k.stufe)], int(round(k.elo)), + int(round(Kampagne.siegchance(k) * 100.0))] b.pressed.connect(_on_kapitaen.bind(String(k.name))) - b.custom_minimum_size = Vector2(170, 52) + b.custom_minimum_size = Vector2(190, 52) var p: Vector2 = k.pos - b.position = Vector2(p.x * groesse.x - 85, p.y * groesse.y + 4) + b.position = Vector2(p.x * groesse.x - 95, p.y * groesse.y + 4) karte.add_child(b) diff --git a/scripts/net.gd b/scripts/net.gd index eeada10..fc6dbe6 100644 --- a/scripts/net.gd +++ b/scripts/net.gd @@ -260,7 +260,7 @@ func _push_lobby(code: String, data: Dictionary, besitzer: int) -> void: ## Einzelspieler: kein Netzwerk, nur der lokale Spieler und Bots. ## `bots`: optionale Liste {name, stufe} - die Kampagne setzt so Kapitän und ## Mannschaft mit eigenen Stufen an den Tisch. -func start_singleplayer(pname: String, ai_count: int, stufe := 1, dice := 5, bots: Array = []) -> void: +func start_singleplayer(pname: String, ai_count: int, stufe := 1, dice := 5, bots: Array = [], beute := "") -> void: player_name = pname multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() room_code = SOLO @@ -270,7 +270,7 @@ func start_singleplayer(pname: String, ai_count: int, stufe := 1, dice := 5, bot in_game = true lobby_changed.emit() game_started.emit(SOLO, {"peers": [1], "names": lobby.duplicate(), "ai": ai_count, - "stufe": stufe, "dice": dice, "bots": bots}) + "stufe": stufe, "dice": dice, "bots": bots, "beute": beute}) ## Nach einem Abbruch mit demselben Namen zurück in die laufende Partie. diff --git a/tests/kampagne.gd b/tests/kampagne.gd index 3da42eb..828ddf6 100644 --- a/tests/kampagne.gd +++ b/tests/kampagne.gd @@ -60,12 +60,31 @@ func _lauf() -> void: absf(Kampagne.elo - elo_alt) < 0.01 and int(Kampagne.kapitaen("Salz-Marie").weg) == Kampagne.AUF_SEE and int(Kampagne.kapitaen("Roter Jack").stufe) == 1) - # --- Echte Partie gegen Jack im Simulationsmodus + # --- Beute, Meldung, Siegchance, Ziel + _pruef("Nach dem Sieg ist Beute offen", Kampagne.beute_offen) + _pruef("Rückkehr wird gemeldet", Kampagne.meldungen.any(func(m): return "ist zurück" in String(m))) + var c_jack := Kampagne.siegchance(Kampagne.kapitaen("Roter Jack")) + var c_kraehe := Kampagne.siegchance(Kampagne.kapitaen("Admiral Krähe")) + _pruef("Siegchance: leicht hoch, schwer niedrig", c_jack > 0.5 and c_kraehe < 0.3) + Kampagne.elo = 1425.0 + Kampagne.aktiv = "Salz-Marie" + Kampagne.ergebnis(_seats([true, false], ["Spieler", "Salz-Marie"]), [1]) + _pruef("Zielrang erreicht: Kampagne gewonnen", Kampagne.gewonnen and Kampagne.meldungen.any(func(m): return "gewonnen" in String(m))) + Kampagne.elo = 1000.0 + Kampagne.gewonnen = false + Kampagne.spielername = "Kaperfahrer" + Kampagne.beute = "peek" + Kampagne.kapitaen("Roter Jack").weg = 0 + + # --- Echte Partie gegen Jack im Simulationsmodus, mit Beute und Temperament var partien_vorher := Kampagne.partien Kampagne.starten("Roter Jack") await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 20.0) _pruef("Partie gegen den Kapitän steht", game != null and game.seats.size() == 2 and String(game.seats[1].name) == "Roter Jack" and int(game.seats[1].stufe) == 1) + _pruef("Spielername und Beute am Tisch", String(game.seats[0].name) == "Kaperfahrer" + and game.hand[0].has("peek") and game.hand[0].size() == 2 and Kampagne.beute == "") + _pruef("Kapitän bringt sein Temperament", float(game.seats[1].regler.get("bluff", 0.0)) == 0.65) game.tempo = 0.0 game.simulation = true game.seats[game.local_seat].ai = true diff --git a/tests/shots.gd b/tests/shots.gd index ab4f827..d5cef91 100644 --- a/tests/shots.gd +++ b/tests/shots.gd @@ -19,9 +19,16 @@ func _ready() -> void: main.menu._show("main") await get_tree().create_timer(0.2).timeout await _bild("menue_start") + # Karte mit offener Beute und einer Meldung, nur im Speicher - nichts wird gespeichert. + var beute_alt: bool = Kampagne.beute_offen + var meld_alt: Array = Kampagne.meldungen.duplicate() + Kampagne.beute_offen = true + Kampagne.meldungen = ["Roter Jack geschlagen - auf See für 2 Partien. Beute wartet."] main.menu._show("kampagne") await get_tree().create_timer(0.2).timeout await _bild("menue_seekarte") + Kampagne.beute_offen = beute_alt + Kampagne.meldungen = meld_alt Net.start_singleplayer("Tester", 3, 1) _lauf()