This commit is contained in:
team3
2026-09-16 18:35:59 +02:00
parent a0597110bd
commit 8173fb7658
8 changed files with 162 additions and 20 deletions

View File

@@ -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

View File

@@ -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)]

View File

@@ -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)

View File

@@ -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():

View File

@@ -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)

View File

@@ -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.

View File

@@ -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

View File

@@ -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()