update
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)]
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user