update
This commit is contained in:
10
Makefile
10
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: dev server prod logs down test sim
|
.PHONY: dev server prod logs down test sim shots
|
||||||
|
|
||||||
GODOT ?= $(HOME)/bin/godot
|
GODOT ?= $(HOME)/bin/godot
|
||||||
|
|
||||||
@@ -16,9 +16,13 @@ dev:
|
|||||||
test:
|
test:
|
||||||
GODOT=$(GODOT) bash tests/run.sh
|
GODOT=$(GODOT) bash tests/run.sh
|
||||||
|
|
||||||
# Bot gegen Bot, je Stufe 12 Partien parallel (~3 min): Rundenzahl und Siegquoten.
|
# Screenshots nach tests/out/ - mit Display oder per xvfb-run.
|
||||||
|
shots:
|
||||||
|
GODOT=$(GODOT) bash tests/shots.sh
|
||||||
|
|
||||||
|
# Bot gegen Bot ohne Physik, je Stufe 50 Partien parallel: Rundenzahl und Siegquoten.
|
||||||
sim:
|
sim:
|
||||||
@for s in 0 1 2; do $(GODOT) --headless --path . res://tests/sim.tscn -- $$s 12 2>/dev/null | grep "^SIM" & done; wait
|
@for s in 0 1 2; do $(GODOT) --headless --path . res://tests/sim.tscn -- $$s 50 2>/dev/null | grep "^SIM" & done; wait
|
||||||
|
|
||||||
# ── Produktion ───────────────────────────────────────────────────────────────
|
# ── Produktion ───────────────────────────────────────────────────────────────
|
||||||
# Auf dem Server ausführen, im Projektverzeichnis. Der Build holt Godot selbst.
|
# Auf dem Server ausführen, im Projektverzeichnis. Der Build holt Godot selbst.
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -226,10 +226,11 @@ Neuwurf sind deshalb raus - sie nahmen dem Lesen der anderen den Sinn.
|
|||||||
Würfel mehr aus Glück würde die Partie kippen. *Nachschub* gibt einen Würfel -
|
Würfel mehr aus Glück würde die Partie kippen. *Nachschub* gibt einen Würfel -
|
||||||
das ist seine Grundwirkung, und Wappen macht daraus nur eine 6.
|
das ist seine Grundwirkung, und Wappen macht daraus nur eine 6.
|
||||||
|
|
||||||
**Bonuswürfel füllen nur auf.** Nachschub und der Genau-Bonus gehen nie über
|
**Bonuswürfel gehen nur an den, der hinten liegt.** Nachschub kann werfen,
|
||||||
die Startzahl hinaus; wer voll ist, kann Nachschub nicht werfen. Sonst kämen
|
wer höchstens die halbe Startzahl hat; den Genau-Bonus bekommt nur ein Rufer
|
||||||
Würfel zurück, und der Tisch würde nie leerer - gemessen: einzelne Partien
|
mit weniger Würfeln als der Ansager. Sonst kämen Würfel zurück, und der Tisch
|
||||||
liefen über 30 Runden.
|
würde nie leerer - `make sim` zeigte davor einzelne Partien mit über 50
|
||||||
|
Runden.
|
||||||
|
|
||||||
### Anzeige
|
### Anzeige
|
||||||
|
|
||||||
@@ -247,9 +248,12 @@ liefen über 30 Runden.
|
|||||||
die Münzen zu seiner Zone.
|
die Münzen zu seiner Zone.
|
||||||
* Klänge (Würfel, Münze, Becher, Gewinn) werden beim Start berechnet. Es gibt
|
* Klänge (Würfel, Münze, Becher, Gewinn) werden beim Start berechnet. Es gibt
|
||||||
keine Tondateien, so wie es auch keine Bilddateien gibt.
|
keine Tondateien, so wie es auch keine Bilddateien gibt.
|
||||||
* Holz, Filz und Messing sind Shader: Maserung, Flaum und Anlauf kommen aus
|
* Kein Bild, kein Mesh aus einer Datei. **Stoffe** sind Regeln (`stoffe.gd`):
|
||||||
Rauschen, nicht aus Texturen. Die Meshes bleiben Godot-Primitive. Der Plan
|
Farbverlauf, Rauschart, Reliefstärke, Rauheit. Daraus entstehen zur
|
||||||
dazu steht in `assets.md`.
|
Laufzeit drei Texturen je Stoff - Farbe, Relief, Rauheit. Das Relief macht
|
||||||
|
aus gemustertem Plastik Filz, Holz und Messing. **Formen** sind Bauregeln
|
||||||
|
(`formen.gd`): Becher = Kegelstumpf + Rand + zwei Bänder, Laterne = Sockel +
|
||||||
|
Glas + Kappe. Kanten sind schmale Ringe. Der Plan steht in `assets.md`.
|
||||||
* Auf dem Handy nur Querformat - ein Tisch von oben braucht Breite.
|
* Auf dem Handy nur Querformat - ein Tisch von oben braucht Breite.
|
||||||
* Die Regeln stehen im Menü unter **Regeln**, inklusive aller sechs Münzen.
|
* Die Regeln stehen im Menü unter **Regeln**, inklusive aller sechs Münzen.
|
||||||
|
|
||||||
@@ -282,7 +286,8 @@ vermutlich haarscharf, rufen sie „Genau".
|
|||||||
| `scripts/ai.gd` | Bot-Entscheidungen (Binomialabschätzung für Gebot/Anfechtung) |
|
| `scripts/ai.gd` | Bot-Entscheidungen (Binomialabschätzung für Gebot/Anfechtung) |
|
||||||
| `scripts/hud.gd` | Spiel-UI |
|
| `scripts/hud.gd` | Spiel-UI |
|
||||||
| `scripts/klang.gd` | Autoload: berechnete Klänge, keine Dateien |
|
| `scripts/klang.gd` | Autoload: berechnete Klänge, keine Dateien |
|
||||||
| `scripts/stoffe.gd` | Holz, Filz, Messing als Shader aus Rauschen - keine Texturdateien |
|
| `scripts/stoffe.gd` | Materialien aus Regeln, Texturen zur Laufzeit - keine Dateien |
|
||||||
|
| `scripts/formen.gd` | Becher, Fass, Laterne aus Primitiven nach Bauregeln |
|
||||||
| `scripts/icons.gd` | Gezeichnete Münz- und Würfelbilder fürs HUD |
|
| `scripts/icons.gd` | Gezeichnete Münz- und Würfelbilder fürs HUD |
|
||||||
| `scripts/hud_panels.gd` | Vorrat, Topf, Erklärung, Verlauf, Protokoll |
|
| `scripts/hud_panels.gd` | Vorrat, Topf, Erklärung, Verlauf, Protokoll |
|
||||||
| `tests/` | Headless-Tests, `make test` (~30 s); `make sim` misst die Bots - siehe `tests/README.md` |
|
| `tests/` | Headless-Tests, `make test` (~30 s); `make sim` misst die Bots - siehe `tests/README.md` |
|
||||||
|
|||||||
16
assets.md
16
assets.md
@@ -1,6 +1,20 @@
|
|||||||
# Asset-Plan
|
# Asset-Plan
|
||||||
|
|
||||||
Status: Konzept. Noch nicht umgesetzt.
|
Status: umgesetzt in `scripts/stoffe.gd` (Materialregeln) und
|
||||||
|
`scripts/formen.gd` (Bauregeln). Unten der Plan, darunter, was davon wie
|
||||||
|
gebaut ist.
|
||||||
|
|
||||||
|
## Stand
|
||||||
|
|
||||||
|
- Stoffe: Filz, Holz, dunkles Holz, Messing, Knochen. Je Regel drei
|
||||||
|
Laufzeit-Texturen aus `NoiseTexture2D`: Farbe (Rauschen durch
|
||||||
|
Farbverlauf), Relief (`as_normal_map`), Rauheit. Kein Shadercode.
|
||||||
|
- Formen: Becher, Fass, Laterne aus Primitiven. Kanten als Torus-Ringe.
|
||||||
|
- Gelernt: Rauschen nur als Farbe bleibt flach - das Relief macht den
|
||||||
|
Unterschied. Grobes, glänzendes Relief auf Filz sieht nach nassem Moos aus:
|
||||||
|
fein, matt, kaum Höhe. Maserung entsteht durch gestreckte UV (1:9).
|
||||||
|
- Verworfen: Dielen auf dem Boden (aus Kamerahöhe nur Rauschen), Lampe über
|
||||||
|
der Mitte (hängt der Kamera vor der Linse), Fass am Boden (ein Fleck).
|
||||||
|
|
||||||
## Grundidee
|
## Grundidee
|
||||||
|
|
||||||
|
|||||||
@@ -69,13 +69,19 @@ static func _regler() -> Dictionary:
|
|||||||
return STUFEN[clampi(stufe, 0, STUFEN.size() - 1)]
|
return STUFEN[clampi(stufe, 0, STUFEN.size() - 1)]
|
||||||
|
|
||||||
|
|
||||||
|
## `wissen`: was der Bot über die anderen weiss - "bekannt" sind aufgedeckte
|
||||||
|
## Gegnerwürfel (Spähen), "fenster" ist je Augenzahl [von, bis] vom Zählen.
|
||||||
|
## Ohne das würfe er die Münzen und ignorierte, was sie ihm sagen.
|
||||||
static func choose_action(values: Array, total_dice: int, bid: Dictionary,
|
static func choose_action(values: Array, total_dice: int, bid: Dictionary,
|
||||||
must_bid: bool, rng: RandomNumberGenerator) -> Dictionary:
|
must_bid: bool, rng: RandomNumberGenerator, wissen: Dictionary = {}) -> Dictionary:
|
||||||
var known: Array = []
|
var known: Array = []
|
||||||
for v in values:
|
for v in values:
|
||||||
if v > 0:
|
if v > 0:
|
||||||
known.append(v)
|
known.append(v)
|
||||||
var unknown := total_dice - known.size()
|
for v in wissen.get("bekannt", []):
|
||||||
|
known.append(int(v))
|
||||||
|
var unknown := maxi(0, total_dice - known.size())
|
||||||
|
var fenster: Dictionary = wissen.get("fenster", {})
|
||||||
|
|
||||||
if bid.is_empty():
|
if bid.is_empty():
|
||||||
var op := _opening(known, unknown, rng)
|
var op := _opening(known, unknown, rng)
|
||||||
@@ -83,6 +89,21 @@ static func choose_action(values: Array, total_dice: int, bid: Dictionary,
|
|||||||
|
|
||||||
var r := _regler()
|
var r := _regler()
|
||||||
var hold := chance(known, unknown, bid.count, bid.face)
|
var hold := chance(known, unknown, bid.count, bid.face)
|
||||||
|
var genau_p := exact_chance(known, unknown, bid.count, bid.face)
|
||||||
|
if fenster.has(int(bid.face)):
|
||||||
|
# Gezählt: das Fenster schlägt jede Schätzung.
|
||||||
|
var f: Array = fenster[int(bid.face)]
|
||||||
|
var von := int(f[0])
|
||||||
|
var bis := int(f[1])
|
||||||
|
if bis < int(bid.count):
|
||||||
|
hold = 0.0
|
||||||
|
genau_p = 0.0
|
||||||
|
elif von >= int(bid.count):
|
||||||
|
hold = 1.0
|
||||||
|
genau_p = 1.0 if von == bis and von == int(bid.count) else (0.34 if von == int(bid.count) else 0.0)
|
||||||
|
else:
|
||||||
|
hold = float(bis - int(bid.count) + 1) / float(bis - von + 1)
|
||||||
|
genau_p = 1.0 / float(bis - von + 1)
|
||||||
var raise_ = _best_raise(known, unknown, total_dice, bid, float(r.sicher))
|
var raise_ = _best_raise(known, unknown, total_dice, bid, float(r.sicher))
|
||||||
var low := lowest_raise(bid, total_dice)
|
var low := lowest_raise(bid, total_dice)
|
||||||
|
|
||||||
@@ -91,8 +112,8 @@ static func choose_action(values: Array, total_dice: int, bid: Dictionary,
|
|||||||
# Anfechtung: der Ansager verliert, und man bekommt selbst einen dazu.
|
# Anfechtung: der Ansager verliert, und man bekommt selbst einen dazu.
|
||||||
# Aber nur ab und zu: Genau nimmt keinen Würfel vom Tisch. Ein Bot,
|
# Aber nur ab und zu: Genau nimmt keinen Würfel vom Tisch. Ein Bot,
|
||||||
# der es bei jeder Gelegenheit ruft, zieht die Partie ins Endlose.
|
# der es bei jeder Gelegenheit ruft, zieht die Partie ins Endlose.
|
||||||
if exact_chance(known, unknown, bid.count, bid.face) > float(r.genau) and hold < 0.6 \
|
# Sicheres Wissen (Fenster exakt) nutzt er dagegen immer.
|
||||||
and rng.randf() < GENAU_LUST:
|
if genau_p >= 0.999 or (genau_p > float(r.genau) and hold < 0.6 and rng.randf() < GENAU_LUST):
|
||||||
return {"type": "exact"}
|
return {"type": "exact"}
|
||||||
var doubt := float(r.zweifel) + rng.randf_range(-0.07, 0.07)
|
var doubt := float(r.zweifel) + rng.randf_range(-0.07, 0.07)
|
||||||
if hold < doubt and (raise_.is_empty() or raise_.p < 0.32):
|
if hold < doubt and (raise_.is_empty() or raise_.p < 0.32):
|
||||||
|
|||||||
@@ -52,8 +52,10 @@ func _phase_reveal(result: Dictionary) -> void:
|
|||||||
# bringt, weniger als "alle anderen" - das kippte bei vielen Spielern.
|
# bringt, weniger als "alle anderen" - das kippte bei vielen Spielern.
|
||||||
verlierer = [int(b.seat)]
|
verlierer = [int(b.seat)]
|
||||||
winner = rufer
|
winner = rufer
|
||||||
# Der Bonuswürfel füllt nur auf: wer schon seine Startzahl hat, bekommt keinen.
|
# Der Bonuswürfel geht nur an den, der hinten liegt: weniger Würfel als
|
||||||
if g.seats[rufer].count < g.dice_start:
|
# der Ansager. Sonst schiebt Genau nur hin und her, und der Tisch wird
|
||||||
|
# nie leerer - gemessen an Partien mit über 50 Runden.
|
||||||
|
if g.seats[rufer].count < g.seats[int(b.seat)].count:
|
||||||
bonus = rufer
|
bonus = rufer
|
||||||
g.banner = "Genau! %s verliert, %s bekommt einen Würfel" % [
|
g.banner = "Genau! %s verliert, %s bekommt einen Würfel" % [
|
||||||
g.seats[int(b.seat)].name, g.seats[rufer].name]
|
g.seats[int(b.seat)].name, g.seats[rufer].name]
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ const TOPF_MAX := 6
|
|||||||
const WAPPEN_CHANCE := 0.5
|
const WAPPEN_CHANCE := 0.5
|
||||||
|
|
||||||
## `target`: fragt nach einem eigenen Würfel. `gebot`: geht nur, wenn eine
|
## `target`: fragt nach einem eigenen Würfel. `gebot`: geht nur, wenn eine
|
||||||
## Ansage auf dem Tisch liegt. `platz`: geht nur, wenn man unter seiner
|
## Ansage auf dem Tisch liegt. `platz`: geht nur, wer wirklich hinten liegt -
|
||||||
## Startzahl liegt - Bonuswürfel füllen auf, nie darüber hinaus. Sonst
|
## höchstens die halbe Startzahl. Sonst kämen Würfel zurück, die Partie zöge
|
||||||
## kämen Würfel zurück, die Partie zöge sich, und der Tisch würde nie leerer.
|
## sich, und der Tisch würde nie leerer.
|
||||||
const ALL := [
|
const ALL := [
|
||||||
{
|
{
|
||||||
"id": "peek", "kind": Kind.BUFF, "target": false, "gebot": false,
|
"id": "peek", "kind": Kind.BUFF, "target": false, "gebot": false,
|
||||||
@@ -49,7 +49,7 @@ const ALL := [
|
|||||||
{
|
{
|
||||||
"id": "gain_die", "kind": Kind.BUFF, "target": false, "gebot": false, "platz": true,
|
"id": "gain_die", "kind": Kind.BUFF, "target": false, "gebot": false, "platz": true,
|
||||||
"name": "Nachschub",
|
"name": "Nachschub",
|
||||||
"desc": "Du bekommst einen Würfel dazu.",
|
"desc": "Du bekommst einen Würfel dazu. Nur, wenn du höchstens die halbe Startzahl hast.",
|
||||||
"stark": "Der neue Würfel zeigt sofort eine 6.",
|
"stark": "Der neue Würfel zeigt sofort eine 6.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const PIPS := {
|
|||||||
|
|
||||||
# Körper und Augen stecken in einem gemeinsamen Mesh mit Vertexfarben,
|
# Körper und Augen stecken in einem gemeinsamen Mesh mit Vertexfarben,
|
||||||
# damit ein Würfel nur ein einziges Renderobjekt ist.
|
# damit ein Würfel nur ein einziges Renderobjekt ist.
|
||||||
|
const Stoffe = preload("res://scripts/stoffe.gd")
|
||||||
static var _mesh: ArrayMesh
|
static var _mesh: ArrayMesh
|
||||||
static var _mat: StandardMaterial3D
|
static var _mat: StandardMaterial3D
|
||||||
static var _gold: StandardMaterial3D ## fürs Zählen beim Aufdecken
|
static var _gold: StandardMaterial3D ## fürs Zählen beim Aufdecken
|
||||||
@@ -121,14 +122,11 @@ static func _add_pips(st: SurfaceTool, n: Vector3, t: Array, value: int) -> void
|
|||||||
func _build_visuals() -> void:
|
func _build_visuals() -> void:
|
||||||
if _mesh == null:
|
if _mesh == null:
|
||||||
_mesh = _build_mesh()
|
_mesh = _build_mesh()
|
||||||
_mat = StandardMaterial3D.new()
|
# Knochen statt Plastik: Vertexfarben bleiben (Körper hell, Augen
|
||||||
_mat.vertex_color_use_as_albedo = true
|
# dunkel), der Shader legt Maserung und einen warmen Kantenschein darüber.
|
||||||
# Kamera und Deckenlicht stehen beide senkrecht: bei glatter Oberfläche
|
_mat = Stoffe.knochen()
|
||||||
# liegt der Glanzpunkt genau auf der Fläche und hellt die Augen auf.
|
|
||||||
_mat.roughness = 0.9
|
|
||||||
_mat.metallic_specular = 0.0
|
|
||||||
if _gold == null:
|
if _gold == null:
|
||||||
# Vertexfarben bleiben, werden nur gold getönt - die Augen bleiben dunkel.
|
# Fürs Zählen: dieselbe Fläche, gold getönt - die Augen bleiben dunkel.
|
||||||
_gold = _mat.duplicate()
|
_gold = _mat.duplicate()
|
||||||
_gold.albedo_color = Color(1.0, 0.82, 0.35)
|
_gold.albedo_color = Color(1.0, 0.82, 0.35)
|
||||||
_gold.emission_enabled = true
|
_gold.emission_enabled = true
|
||||||
|
|||||||
80
scripts/formen.gd
Normal file
80
scripts/formen.gd
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
extends RefCounted
|
||||||
|
## Formen aus Regeln: jedes Ding hat eine Grundform aus Godot-Primitiven, und
|
||||||
|
## Regeln setzen Teile darauf - Rand, Bänder, Reifen, Kappe. Kanten sind
|
||||||
|
## schmale Ringe statt Fasen. Kein Mesh kommt aus einer Datei.
|
||||||
|
## Kein class_name: den Klassencache baut nur der Editor, headless fehlt er.
|
||||||
|
|
||||||
|
const Stoffe = preload("res://scripts/stoffe.gd")
|
||||||
|
|
||||||
|
|
||||||
|
static func _teil(mesh: Mesh, stoff: Material, pos: Vector3, eltern: Node3D) -> MeshInstance3D:
|
||||||
|
var mi := MeshInstance3D.new()
|
||||||
|
mi.mesh = mesh
|
||||||
|
mi.material_override = stoff
|
||||||
|
mi.position = pos
|
||||||
|
eltern.add_child(mi)
|
||||||
|
return mi
|
||||||
|
|
||||||
|
|
||||||
|
static func _zylinder(oben: float, unten: float, hoehe: float, segmente := 28) -> CylinderMesh:
|
||||||
|
var c := CylinderMesh.new()
|
||||||
|
c.top_radius = oben
|
||||||
|
c.bottom_radius = unten
|
||||||
|
c.height = hoehe
|
||||||
|
c.radial_segments = segmente
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
static func _ring(radius: float, dicke: float) -> TorusMesh:
|
||||||
|
var t := TorusMesh.new()
|
||||||
|
t.inner_radius = radius - dicke
|
||||||
|
t.outer_radius = radius + dicke
|
||||||
|
t.rings = 32
|
||||||
|
t.ring_segments = 8
|
||||||
|
return t
|
||||||
|
|
||||||
|
|
||||||
|
## Würfelbecher: Kegelstumpf aus dunklem Holz, oben ein Messingrand, zwei
|
||||||
|
## Messingbänder um den Bauch. Der Deckel oben ist geschlossen - die Kamera
|
||||||
|
## schaut von oben, ein offener Becher zeigte nur ein schwarzes Loch.
|
||||||
|
static func becher(hoehe := 0.44, oben := 0.33, unten := 0.42) -> Node3D:
|
||||||
|
var n := Node3D.new()
|
||||||
|
_teil(_zylinder(oben, unten, hoehe), Stoffe.holz_dunkel(), Vector3(0, hoehe * 0.5, 0), n)
|
||||||
|
_teil(_ring(oben, 0.012), Stoffe.messing(), Vector3(0, hoehe - 0.004, 0), n)
|
||||||
|
for k in [0.22, 0.62]:
|
||||||
|
var r: float = lerpf(unten, oben, k)
|
||||||
|
_teil(_ring(r, 0.008), Stoffe.messing(), Vector3(0, hoehe * k, 0), n)
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
## Fass: bauchiger Zylinder, drei Reifen, Boden und Deckel etwas kleiner.
|
||||||
|
static func fass(hoehe := 0.6, radius := 0.24) -> Node3D:
|
||||||
|
var n := Node3D.new()
|
||||||
|
_teil(_zylinder(radius * 0.86, radius * 0.86, hoehe), Stoffe.holz(), Vector3(0, hoehe * 0.5, 0), n)
|
||||||
|
_teil(_zylinder(radius, radius, hoehe * 0.5), Stoffe.holz(), Vector3(0, hoehe * 0.5, 0), n)
|
||||||
|
for k in [0.12, 0.5, 0.88]:
|
||||||
|
var r: float = radius if absf(k - 0.5) < 0.2 else radius * 0.88
|
||||||
|
_teil(_ring(r, 0.012), Stoffe.messing(), Vector3(0, hoehe * k, 0), n)
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
## Laterne: Sockel, Glaskörper (leuchtend), Kappe mit Ring.
|
||||||
|
static func laterne(hoehe := 0.5) -> Node3D:
|
||||||
|
var n := Node3D.new()
|
||||||
|
_teil(_zylinder(0.09, 0.11, 0.05), Stoffe.messing(), Vector3(0, 0.025, 0), n)
|
||||||
|
var glas := StandardMaterial3D.new()
|
||||||
|
glas.albedo_color = Color(1.0, 0.85, 0.5, 0.75)
|
||||||
|
glas.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
glas.emission_enabled = true
|
||||||
|
glas.emission = Color(1.0, 0.75, 0.35)
|
||||||
|
glas.emission_energy_multiplier = 2.5
|
||||||
|
_teil(_zylinder(0.07, 0.08, hoehe * 0.6), glas, Vector3(0, 0.05 + hoehe * 0.3, 0), n)
|
||||||
|
_teil(_zylinder(0.03, 0.10, 0.08), Stoffe.messing(), Vector3(0, 0.05 + hoehe * 0.6 + 0.04, 0), n)
|
||||||
|
_teil(_ring(0.035, 0.006), Stoffe.messing(), Vector3(0, 0.05 + hoehe * 0.6 + 0.10, 0), n)
|
||||||
|
var licht := OmniLight3D.new()
|
||||||
|
licht.light_color = Color(1.0, 0.8, 0.45)
|
||||||
|
licht.light_energy = 1.2
|
||||||
|
licht.omni_range = 1.8
|
||||||
|
licht.position = Vector3(0, 0.05 + hoehe * 0.3, 0)
|
||||||
|
n.add_child(licht)
|
||||||
|
return n
|
||||||
@@ -21,6 +21,10 @@ var tempo := 1.0
|
|||||||
## Würfel je Platz zu Beginn. Bonuswürfel (Nachschub, Genau) füllen nur bis
|
## Würfel je Platz zu Beginn. Bonuswürfel (Nachschub, Genau) füllen nur bis
|
||||||
## hierhin auf - so wird der Tisch über die Partie sicher leerer.
|
## hierhin auf - so wird der Tisch über die Partie sicher leerer.
|
||||||
var dice_start := START_DICE
|
var dice_start := START_DICE
|
||||||
|
## Simulation: kein Würfelwurf, keine Physik - Werte werden direkt gezogen.
|
||||||
|
## Nur für `make sim`; fünfzig Partien in Sekunden statt Minuten.
|
||||||
|
var simulation := false
|
||||||
|
var runde := 0 ## laufende Rundennummer, für Anzeige und Messung
|
||||||
|
|
||||||
var rng := RandomNumberGenerator.new()
|
var rng := RandomNumberGenerator.new()
|
||||||
var seats: Array = [] ## [{peer, name, ai, count, alive}]
|
var seats: Array = [] ## [{peer, name, ai, count, alive}]
|
||||||
@@ -259,6 +263,7 @@ func _now() -> float:
|
|||||||
|
|
||||||
func _phase_roll() -> void:
|
func _phase_roll() -> void:
|
||||||
phase = "roll"
|
phase = "roll"
|
||||||
|
runde += 1
|
||||||
deadline = 0.0
|
deadline = 0.0
|
||||||
reveal_total = 0
|
reveal_total = 0
|
||||||
bid = {}
|
bid = {}
|
||||||
@@ -277,6 +282,15 @@ func _phase_roll() -> void:
|
|||||||
table.set_cup_down(i, i != local_seat)
|
table.set_cup_down(i, i != local_seat)
|
||||||
_push_state()
|
_push_state()
|
||||||
|
|
||||||
|
if simulation:
|
||||||
|
for i in _alive_seats():
|
||||||
|
host_values[i] = []
|
||||||
|
for _k in seats[i].count:
|
||||||
|
host_values[i].append(rng.randi_range(1, 6))
|
||||||
|
table.arrange_and_show(i, host_values[i], rng)
|
||||||
|
_push_state()
|
||||||
|
return
|
||||||
|
|
||||||
_stream = true
|
_stream = true
|
||||||
for i in _alive_seats():
|
for i in _alive_seats():
|
||||||
table.throw_dice(i, rng)
|
table.throw_dice(i, rng)
|
||||||
@@ -369,6 +383,11 @@ func _all_values() -> Array:
|
|||||||
|
|
||||||
## Würfel neu werfen und die Augenzahl danach übernehmen.
|
## Würfel neu werfen und die Augenzahl danach übernehmen.
|
||||||
func _neu_werfen(seat: int, idxs: Array) -> void:
|
func _neu_werfen(seat: int, idxs: Array) -> void:
|
||||||
|
if simulation:
|
||||||
|
for k: int in idxs:
|
||||||
|
if k < host_values[seat].size():
|
||||||
|
host_values[seat][k] = rng.randi_range(1, 6)
|
||||||
|
return
|
||||||
_stream = true
|
_stream = true
|
||||||
_throw_subset(seat, idxs)
|
_throw_subset(seat, idxs)
|
||||||
await _await_settled([seat])
|
await _await_settled([seat])
|
||||||
@@ -415,10 +434,10 @@ func _phase_bid() -> Dictionary:
|
|||||||
# Erst planen, dann passend dazu eine Münze werfen. Die kann die
|
# Erst planen, dann passend dazu eine Münze werfen. Die kann die
|
||||||
# Lage ändern - also danach noch einmal entscheiden.
|
# Lage ändern - also danach noch einmal entscheiden.
|
||||||
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
||||||
must_bid.get(turn, false), rng)
|
must_bid.get(turn, false), rng, _bot_wissen(turn))
|
||||||
if await _bot_muenze(turn, a):
|
if await _bot_muenze(turn, a):
|
||||||
a = DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
a = DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
||||||
must_bid.get(turn, false), rng)
|
must_bid.get(turn, false), rng, _bot_wissen(turn))
|
||||||
if not _alive():
|
if not _alive():
|
||||||
return {}
|
return {}
|
||||||
var res := _apply_action(turn, a)
|
var res := _apply_action(turn, a)
|
||||||
@@ -433,7 +452,7 @@ func _phase_bid() -> Dictionary:
|
|||||||
else:
|
else:
|
||||||
# Zeit abgelaufen oder getrennt: automatisch entscheiden
|
# Zeit abgelaufen oder getrennt: automatisch entscheiden
|
||||||
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
var a := DiceAI.choose_action(host_values[turn], _total_dice(), bid,
|
||||||
must_bid.get(turn, false), rng)
|
must_bid.get(turn, false), rng, _bot_wissen(turn))
|
||||||
_log("%s wird automatisch gespielt." % seats[turn].name)
|
_log("%s wird automatisch gespielt." % seats[turn].name)
|
||||||
var res := _apply_action(turn, a)
|
var res := _apply_action(turn, a)
|
||||||
if not res.is_empty():
|
if not res.is_empty():
|
||||||
@@ -442,6 +461,22 @@ func _phase_bid() -> Dictionary:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
## Nachschub gibt es nur, wer wirklich hinten liegt: höchstens die halbe
|
||||||
|
## Startzahl. Gemessen: mit jedem Rückweg dehnt sich die Partie - einzelne
|
||||||
|
## liefen über 50 Runden.
|
||||||
|
func _hinten(seat: int) -> bool:
|
||||||
|
return seats[seat].count * 2 <= dice_start
|
||||||
|
|
||||||
|
|
||||||
|
## Was ein Platz über die anderen weiss: aufgedeckte Würfel und Zählfenster.
|
||||||
|
func _bot_wissen(seat: int) -> Dictionary:
|
||||||
|
var bekannt: Array = []
|
||||||
|
for o in peeked.get(seat, {}):
|
||||||
|
for paar in peeked[seat][o]:
|
||||||
|
bekannt.append(int(paar[1]))
|
||||||
|
return {"bekannt": bekannt, "fenster": gezaehlt.get(seat, {})}
|
||||||
|
|
||||||
|
|
||||||
## Gibt {} zurück wenn das Spiel weitergeht, sonst das Anfechtungs-Ergebnis.
|
## Gibt {} zurück wenn das Spiel weitergeht, sonst das Anfechtungs-Ergebnis.
|
||||||
func _apply_action(seat: int, a: Dictionary) -> Dictionary:
|
func _apply_action(seat: int, a: Dictionary) -> Dictionary:
|
||||||
if a.type == "exact":
|
if a.type == "exact":
|
||||||
@@ -633,7 +668,7 @@ func _state_for(seat: int) -> Dictionary:
|
|||||||
"bids": _seat_bids(),
|
"bids": _seat_bids(),
|
||||||
"verlauf": verlauf.duplicate(true),
|
"verlauf": verlauf.duplicate(true),
|
||||||
"topf_an": _topf_ziel,
|
"topf_an": _topf_ziel,
|
||||||
"voll": seats[seat].count >= dice_start,
|
"voll": not _hinten(seat),
|
||||||
"log": log_lines.duplicate(),
|
"log": log_lines.duplicate(),
|
||||||
"hand": (hand.get(seat, []) as Array).duplicate(),
|
"hand": (hand.get(seat, []) as Array).duplicate(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -498,11 +498,37 @@ func _on_exact() -> void:
|
|||||||
exact_pressed.emit()
|
exact_pressed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
## Tastatur: Esc Pause, 1-6 Augenzahl, +/- Anzahl, Enter Ansagen,
|
||||||
|
## A Anfechten, G Genau. Die Knöpfe entscheiden, was gerade erlaubt ist -
|
||||||
|
## eine Taste drückt nur, was auch klickbar wäre.
|
||||||
func _unhandled_key_input(event: InputEvent) -> void:
|
func _unhandled_key_input(event: InputEvent) -> void:
|
||||||
if event is InputEventKey and event.pressed and not event.echo \
|
if not (event is InputEventKey and event.pressed and not event.echo):
|
||||||
and event.keycode == KEY_ESCAPE:
|
return
|
||||||
|
var k: Key = event.keycode
|
||||||
|
if k == KEY_ESCAPE:
|
||||||
_set_paused(not pause_slot.visible)
|
_set_paused(not pause_slot.visible)
|
||||||
get_viewport().set_input_as_handled()
|
elif k >= KEY_1 and k <= KEY_6:
|
||||||
|
var f := int(k - KEY_1) + 1
|
||||||
|
if _my_turn and _face_allowed(f):
|
||||||
|
_on_face(f)
|
||||||
|
elif k == KEY_PLUS or k == KEY_KP_ADD or k == KEY_UP:
|
||||||
|
if not plus_btn.disabled:
|
||||||
|
_step_count(1)
|
||||||
|
elif k == KEY_MINUS or k == KEY_KP_SUBTRACT or k == KEY_DOWN:
|
||||||
|
if not minus_btn.disabled:
|
||||||
|
_step_count(-1)
|
||||||
|
elif k == KEY_ENTER or k == KEY_KP_ENTER:
|
||||||
|
if not bid_btn.disabled:
|
||||||
|
_on_bid()
|
||||||
|
elif k == KEY_A:
|
||||||
|
if not challenge_btn.disabled:
|
||||||
|
_on_challenge()
|
||||||
|
elif k == KEY_G:
|
||||||
|
if not exact_btn.disabled:
|
||||||
|
_on_exact()
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
func _set_paused(on: bool) -> void:
|
func _set_paused(on: bool) -> void:
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ func _build_regeln() -> Control:
|
|||||||
["Würfeln", "Jeder hat fünf Würfel unter dem Becher. Nur die eigenen sieht man."],
|
["Würfeln", "Jeder hat fünf Würfel unter dem Becher. Nur die eigenen sieht man."],
|
||||||
["Ansagen", "Reihum sagt man, wie viele Würfel einer Augenzahl auf dem ganzen Tisch liegen - zum Beispiel „vier Dreien\". Wer dran ist, muss höher gehen: mehr Würfel, oder gleich viele mit höherer Augenzahl. Oder er zweifelt."],
|
["Ansagen", "Reihum sagt man, wie viele Würfel einer Augenzahl auf dem ganzen Tisch liegen - zum Beispiel „vier Dreien\". Wer dran ist, muss höher gehen: mehr Würfel, oder gleich viele mit höherer Augenzahl. Oder er zweifelt."],
|
||||||
["Anfechten", "Stimmt die Ansage nicht, verliert der Ansager einen Würfel. Stimmt sie, verliert der Zweifler einen. Mehr als angesagt zählt als richtig."],
|
["Anfechten", "Stimmt die Ansage nicht, verliert der Ansager einen Würfel. Stimmt sie, verliert der Zweifler einen. Mehr als angesagt zählt als richtig."],
|
||||||
["Genau", "Wer glaubt, die Ansage trifft exakt, ruft „Genau\". Trifft es, verliert der Ansager einen Würfel und der Rufer bekommt einen dazu - aber nie über seine Startzahl hinaus. Daneben verliert der Rufer einen."],
|
["Genau", "Wer glaubt, die Ansage trifft exakt, ruft „Genau\". Trifft es, verliert der Ansager einen Würfel; hat der Rufer weniger Würfel als der Ansager, bekommt er einen dazu. Daneben verliert der Rufer einen."],
|
||||||
["Münzen", "Jeder startet mit einer Münze; wer einen Würfel verliert, bekommt eine. Geworfen wird nur am eigenen Zug, vor der Ansage. Wappen verstärkt die Wirkung, Zahl lässt es bei der Grundwirkung - die kommt immer."],
|
["Münzen", "Jeder startet mit einer Münze; wer einen Würfel verliert, bekommt eine. Geworfen wird nur am eigenen Zug, vor der Ansage. Wappen verstärkt die Wirkung, Zahl lässt es bei der Grundwirkung - die kommt immer."],
|
||||||
["Topf", "Geworfene Münzen liegen in der Mitte. Wer das Aufdecken gewinnt, nimmt sie. Mehr als vier passen nicht in den Vorrat; der Rest bleibt liegen. Im Topf liegen höchstens sechs - die älteste fällt vom Tisch."],
|
["Topf", "Geworfene Münzen liegen in der Mitte. Wer das Aufdecken gewinnt, nimmt sie. Mehr als vier passen nicht in den Vorrat; der Rest bleibt liegen. Im Topf liegen höchstens sechs - die älteste fällt vom Tisch."],
|
||||||
["Ende", "Wer keine Würfel mehr hat, schaut zu. Der Letzte gewinnt."],
|
["Ende", "Wer keine Würfel mehr hat, schaut zu. Der Letzte gewinnt."],
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ func _do_play(seat: int, id: String, target: int) -> void:
|
|||||||
return
|
return
|
||||||
if BoosterDB.needs_bid(id) and g.bid.is_empty():
|
if BoosterDB.needs_bid(id) and g.bid.is_empty():
|
||||||
return
|
return
|
||||||
if BoosterDB.needs_room(id) and g.seats[seat].count >= g.dice_start:
|
if BoosterDB.needs_room(id) and not g._hinten(seat):
|
||||||
return
|
return
|
||||||
var muenzen: Array = g.hand.get(seat, [])
|
var muenzen: Array = g.hand.get(seat, [])
|
||||||
if not muenzen.has(id):
|
if not muenzen.has(id):
|
||||||
@@ -158,7 +158,7 @@ func _bot_muenze(seat: int, a: Dictionary) -> bool:
|
|||||||
var ziel := 0
|
var ziel := 0
|
||||||
# Wirklich hinten liegen: Nachschub ist der Weg zurück. Bei Gleichstand
|
# Wirklich hinten liegen: Nachschub ist der Weg zurück. Bei Gleichstand
|
||||||
# nicht - sonst wirft ihn zu Beginn jeder, und der Tisch wird nie leerer.
|
# nicht - sonst wirft ihn zu Beginn jeder, und der Tisch wird nie leerer.
|
||||||
if muenzen.has("gain_die") and vals.size() < _groesste_hand() and vals.size() < g.dice_start:
|
if muenzen.has("gain_die") and g._hinten(seat):
|
||||||
wahl = "gain_die"
|
wahl = "gain_die"
|
||||||
# Vor dem Anfechten lieber noch nachsehen, wenn es knapp ist.
|
# Vor dem Anfechten lieber noch nachsehen, wenn es knapp ist.
|
||||||
elif not will_bieten and liegt > 0.2 and liegt < 0.55 and muenzen.has("count"):
|
elif not will_bieten and liegt > 0.2 and liegt < 0.55 and muenzen.has("count"):
|
||||||
@@ -181,7 +181,7 @@ func _bot_muenze(seat: int, a: Dictionary) -> bool:
|
|||||||
for id: String in muenzen:
|
for id: String in muenzen:
|
||||||
if BoosterDB.needs_bid(id) and g.bid.is_empty():
|
if BoosterDB.needs_bid(id) and g.bid.is_empty():
|
||||||
continue
|
continue
|
||||||
if BoosterDB.needs_room(id) and vals.size() >= g.dice_start:
|
if BoosterDB.needs_room(id) and not g._hinten(seat):
|
||||||
continue
|
continue
|
||||||
wahl = id
|
wahl = id
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -1,129 +1,145 @@
|
|||||||
extends RefCounted
|
extends RefCounted
|
||||||
## Materialien ohne Texturdateien: Holz, Filz und Messing werden im Shader
|
## Materialien aus Regeln, ohne eine einzige Datei. Je Stoff ein Eintrag:
|
||||||
## aus Rauschen gerechnet. Alles, was Maserung, Flecken oder Anlauf ist, sind
|
## Farbverlauf, Rauschart, Reliefstärke, Rauheit, Maßstab. Daraus baut
|
||||||
## Zahlen - keine Bilder. Siehe assets.md. Kein class_name (headless kein Cache).
|
## `mach()` ein StandardMaterial3D mit drei zur Laufzeit erzeugten Texturen:
|
||||||
|
## Farbe (Rauschen durch einen Farbverlauf), Relief (dasselbe Rauschen als
|
||||||
|
## Normalenkarte) und Rauheit. Das Relief ist der Unterschied zwischen
|
||||||
|
## "gemustertes Plastik" und Filz, Holz, Messing.
|
||||||
|
##
|
||||||
|
## Kein class_name: den Klassencache baut nur der Editor, headless fehlt er.
|
||||||
|
|
||||||
const RAUSCHEN := """
|
const STOFFE := {
|
||||||
float hash2(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
|
"filz": {
|
||||||
float noise2(vec2 p) {
|
# Feiner Flaum, kaum Relief, ganz matt. Grobes, glänzendes Relief sah
|
||||||
vec2 i = floor(p); vec2 f = fract(p); f = f * f * (3.0 - 2.0 * f);
|
# nach nassem Moos aus.
|
||||||
return mix(mix(hash2(i), hash2(i + vec2(1, 0)), f.x),
|
"farben": [Color(0.06, 0.18, 0.11), Color(0.08, 0.23, 0.14), Color(0.10, 0.26, 0.16)],
|
||||||
mix(hash2(i + vec2(0, 1)), hash2(i + vec2(1, 1)), f.x), f.y);
|
"art": FastNoiseLite.TYPE_SIMPLEX_SMOOTH, "freq": 0.35, "oktaven": 3,
|
||||||
|
"relief": 0.12, "rauheit": [0.96, 1.0], "metall": 0.0, "uv": Vector3(14, 14, 1),
|
||||||
|
"glanz": 0.05,
|
||||||
|
},
|
||||||
|
"holz": {
|
||||||
|
"farben": [Color(0.20, 0.12, 0.06), Color(0.36, 0.23, 0.11), Color(0.46, 0.31, 0.16)],
|
||||||
|
"art": FastNoiseLite.TYPE_SIMPLEX, "freq": 0.03, "oktaven": 5, "warp": 25.0,
|
||||||
|
"relief": 0.6, "rauheit": [0.45, 0.75], "metall": 0.0,
|
||||||
|
# stark gestreckt: aus dem Rauschen wird Maserung
|
||||||
|
"uv": Vector3(1.0, 9.0, 1.0),
|
||||||
|
},
|
||||||
|
"holz_dunkel": {
|
||||||
|
"farben": [Color(0.14, 0.06, 0.04), Color(0.30, 0.12, 0.07), Color(0.40, 0.18, 0.10)],
|
||||||
|
"art": FastNoiseLite.TYPE_SIMPLEX, "freq": 0.03, "oktaven": 5, "warp": 25.0,
|
||||||
|
"relief": 0.5, "rauheit": [0.4, 0.7], "metall": 0.0, "uv": Vector3(1.0, 7.0, 1.0),
|
||||||
|
},
|
||||||
|
"messing": {
|
||||||
|
"farben": [Color(0.55, 0.40, 0.14), Color(0.80, 0.62, 0.26), Color(0.95, 0.80, 0.42)],
|
||||||
|
"art": FastNoiseLite.TYPE_CELLULAR, "freq": 0.12, "oktaven": 2,
|
||||||
|
"relief": 0.25, "rauheit": [0.2, 0.5], "metall": 0.9, "uv": Vector3(3, 3, 1),
|
||||||
|
},
|
||||||
|
"knochen": {
|
||||||
|
# Farbe kommt von den Vertexfarben (Körper hell, Augen dunkel); hier
|
||||||
|
# nur feine Dellen und eine warme, leicht fettige Oberfläche.
|
||||||
|
"farben": [], "art": FastNoiseLite.TYPE_SIMPLEX_SMOOTH, "freq": 0.25, "oktaven": 3,
|
||||||
|
"relief": 0.2, "rauheit": [0.45, 0.65], "metall": 0.0, "uv": Vector3(4, 4, 1),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
float fbm2(vec2 p) {
|
|
||||||
float v = 0.0; float a = 0.5;
|
|
||||||
for (int k = 0; k < 4; k++) { v += a * noise2(p); p = p * 2.03 + 17.0; a *= 0.5; }
|
|
||||||
return v;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
## Holz: Jahresringe entlang einer Achse, leicht verzerrt, plus feine
|
static var _fertig: Dictionary = {} ## name -> StandardMaterial3D, einmal gebaut
|
||||||
## Faser. Weltkoordinaten, damit die Maserung über Teile hinweg durchläuft.
|
|
||||||
const HOLZ := """
|
|
||||||
shader_type spatial;
|
|
||||||
uniform vec4 base_color : source_color = vec4(0.42, 0.27, 0.13, 1.0);
|
|
||||||
uniform vec4 dark_color : source_color = vec4(0.22, 0.13, 0.06, 1.0);
|
|
||||||
uniform float wear : hint_range(0.0, 1.0) = 0.4;
|
|
||||||
uniform float ring_scale = 9.0;
|
|
||||||
varying vec3 wp;
|
|
||||||
""" + RAUSCHEN + """
|
|
||||||
void vertex() { wp = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; }
|
|
||||||
void fragment() {
|
|
||||||
vec2 p = wp.xz;
|
|
||||||
float verzerrt = p.x * ring_scale + fbm2(p * 2.5) * 3.0;
|
|
||||||
float ring = smoothstep(0.2, 0.8, abs(fract(verzerrt) - 0.5) * 2.0);
|
|
||||||
float faser = noise2(vec2(p.x * 60.0, p.y * 4.0)) * 0.12;
|
|
||||||
vec3 col = mix(dark_color.rgb, base_color.rgb, ring) + faser - 0.06;
|
|
||||||
float flecken = fbm2(p * 6.0 + 40.0);
|
|
||||||
col = mix(col, col * 0.75, smoothstep(0.55, 0.8, flecken) * wear);
|
|
||||||
ALBEDO = col;
|
|
||||||
ROUGHNESS = 0.55 + ring * 0.25 + wear * 0.15;
|
|
||||||
SPECULAR = 0.3;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
## Filz: gleichmässige Fläche mit feinem Flaum, leicht fleckig, an den
|
|
||||||
## Rändern dunkler. Ohne das sähe die Zone aus wie Plastik.
|
|
||||||
const FILZ := """
|
|
||||||
shader_type spatial;
|
|
||||||
uniform vec4 base_color : source_color = vec4(0.07, 0.20, 0.13, 1.0);
|
|
||||||
uniform float wear : hint_range(0.0, 1.0) = 0.5;
|
|
||||||
uniform float radius = 0.68;
|
|
||||||
varying vec3 lp;
|
|
||||||
varying vec3 wp;
|
|
||||||
""" + RAUSCHEN + """
|
|
||||||
void vertex() { lp = VERTEX; wp = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; }
|
|
||||||
void fragment() {
|
|
||||||
float flaum = noise2(wp.xz * 90.0) * 0.10 + noise2(wp.xz * 300.0) * 0.05;
|
|
||||||
float flecken = fbm2(wp.xz * 3.0) ;
|
|
||||||
float rand = smoothstep(radius * 0.7, radius, length(lp.xz));
|
|
||||||
vec3 col = base_color.rgb * (0.9 + flaum) * (1.0 - flecken * 0.25 * wear) * (1.0 - rand * 0.25);
|
|
||||||
ALBEDO = col;
|
|
||||||
ROUGHNESS = 0.97;
|
|
||||||
SPECULAR = 0.1;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
## Messing: metallisch, mit Anlauf in den Vertiefungen und einem Hauch
|
|
||||||
## Farbschwankung. Die Prägung bleibt ein eigenes Mesh.
|
|
||||||
const MESSING := """
|
|
||||||
shader_type spatial;
|
|
||||||
uniform vec4 base_color : source_color = vec4(0.80, 0.62, 0.26, 1.0);
|
|
||||||
uniform float wear : hint_range(0.0, 1.0) = 0.4;
|
|
||||||
uniform float emission : hint_range(0.0, 2.0) = 0.35;
|
|
||||||
varying vec3 wp;
|
|
||||||
""" + RAUSCHEN + """
|
|
||||||
void vertex() { wp = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; }
|
|
||||||
void fragment() {
|
|
||||||
float anlauf = fbm2(wp.xz * 40.0);
|
|
||||||
vec3 col = base_color.rgb * (0.85 + anlauf * 0.3);
|
|
||||||
col = mix(col, col * vec3(0.7, 0.6, 0.5), smoothstep(0.6, 0.85, anlauf) * wear);
|
|
||||||
ALBEDO = col;
|
|
||||||
METALLIC = 0.9;
|
|
||||||
ROUGHNESS = 0.25 + anlauf * 0.35 * wear;
|
|
||||||
EMISSION = base_color.rgb * 0.6 * emission;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|
||||||
static var _holz: Shader
|
|
||||||
static var _filz: Shader
|
|
||||||
static var _messing: Shader
|
|
||||||
|
|
||||||
|
|
||||||
static func _shader(code: String) -> Shader:
|
static func _rauschen(r: Dictionary, seed: int) -> FastNoiseLite:
|
||||||
var s := Shader.new()
|
var n := FastNoiseLite.new()
|
||||||
s.code = code
|
n.seed = seed
|
||||||
return s
|
n.noise_type = r.art
|
||||||
|
n.frequency = r.freq
|
||||||
|
n.fractal_octaves = r.oktaven
|
||||||
|
if r.has("warp"):
|
||||||
|
n.domain_warp_enabled = true
|
||||||
|
n.domain_warp_amplitude = r.warp
|
||||||
|
return n
|
||||||
|
|
||||||
|
|
||||||
static func holz(farbe: Color, dunkel: Color, wear := 0.4, ring_scale := 9.0) -> ShaderMaterial:
|
static func _textur(n: FastNoiseLite, groesse: int) -> NoiseTexture2D:
|
||||||
if _holz == null:
|
var t := NoiseTexture2D.new()
|
||||||
_holz = _shader(HOLZ)
|
t.noise = n
|
||||||
var m := ShaderMaterial.new()
|
t.width = groesse
|
||||||
m.shader = _holz
|
t.height = groesse
|
||||||
m.set_shader_parameter("base_color", farbe)
|
t.seamless = true
|
||||||
m.set_shader_parameter("dark_color", dunkel)
|
t.generate_mipmaps = true
|
||||||
m.set_shader_parameter("wear", wear)
|
return t
|
||||||
m.set_shader_parameter("ring_scale", ring_scale)
|
|
||||||
|
|
||||||
|
## Baut das Material nach Regel `name`. Gleiche Regel, gleiches Material:
|
||||||
|
## alle Becher teilen eine Instanz, so bleibt es ein Renderobjekt je Stoff.
|
||||||
|
static func mach(name: String) -> StandardMaterial3D:
|
||||||
|
if _fertig.has(name):
|
||||||
|
return _fertig[name]
|
||||||
|
var r: Dictionary = STOFFE[name]
|
||||||
|
var m := StandardMaterial3D.new()
|
||||||
|
var basis := _rauschen(r, name.hash() % 1000)
|
||||||
|
|
||||||
|
if not r.farben.is_empty():
|
||||||
|
var verlauf := Gradient.new()
|
||||||
|
var farben: Array = r.farben
|
||||||
|
verlauf.offsets = PackedFloat32Array([0.0, 0.5, 1.0])
|
||||||
|
verlauf.colors = PackedColorArray([farben[0], farben[1], farben[2]])
|
||||||
|
var farbe := _textur(basis, 256)
|
||||||
|
farbe.color_ramp = verlauf
|
||||||
|
m.albedo_texture = farbe
|
||||||
|
else:
|
||||||
|
m.vertex_color_use_as_albedo = true
|
||||||
|
|
||||||
|
var relief := _textur(basis, 256)
|
||||||
|
relief.as_normal_map = true
|
||||||
|
relief.bump_strength = float(r.relief) * 8.0
|
||||||
|
m.normal_enabled = true
|
||||||
|
m.normal_texture = relief
|
||||||
|
m.normal_scale = 1.0
|
||||||
|
|
||||||
|
# Rauheit schwankt mit dem Rauschen zwischen den zwei Werten.
|
||||||
|
var rau := _textur(_rauschen(r, name.hash() % 1000 + 7), 128)
|
||||||
|
m.roughness_texture = rau
|
||||||
|
m.roughness_texture_channel = BaseMaterial3D.TEXTURE_CHANNEL_RED
|
||||||
|
m.roughness = float(r.rauheit[1])
|
||||||
|
m.metallic = float(r.metall)
|
||||||
|
m.metallic_specular = float(r.get("glanz", 0.5 if r.metall > 0.5 else 0.3))
|
||||||
|
m.uv1_scale = r.uv
|
||||||
|
m.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC
|
||||||
|
_fertig[name] = m
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
static func filz(farbe: Color, radius: float, wear := 0.5) -> ShaderMaterial:
|
## Eigene Instanz, wenn ein Objekt Farbe oder Leuchten ändern muss.
|
||||||
if _filz == null:
|
static func eigene(name: String) -> StandardMaterial3D:
|
||||||
_filz = _shader(FILZ)
|
return mach(name).duplicate()
|
||||||
var m := ShaderMaterial.new()
|
|
||||||
m.shader = _filz
|
|
||||||
m.set_shader_parameter("base_color", farbe)
|
static func filz() -> StandardMaterial3D:
|
||||||
m.set_shader_parameter("radius", radius)
|
return mach("filz")
|
||||||
m.set_shader_parameter("wear", wear)
|
|
||||||
|
|
||||||
|
static func holz() -> StandardMaterial3D:
|
||||||
|
return mach("holz")
|
||||||
|
|
||||||
|
|
||||||
|
static func holz_dunkel() -> StandardMaterial3D:
|
||||||
|
return mach("holz_dunkel")
|
||||||
|
|
||||||
|
|
||||||
|
static func messing(emission := 0.0) -> StandardMaterial3D:
|
||||||
|
var m := eigene("messing")
|
||||||
|
if emission > 0.0:
|
||||||
|
m.emission_enabled = true
|
||||||
|
m.emission = Color(1.0, 0.75, 0.30)
|
||||||
|
m.emission_energy_multiplier = emission
|
||||||
return m
|
return m
|
||||||
|
|
||||||
|
|
||||||
static func messing(emission := 0.35, wear := 0.4) -> ShaderMaterial:
|
static func knochen() -> StandardMaterial3D:
|
||||||
if _messing == null:
|
return mach("knochen")
|
||||||
_messing = _shader(MESSING)
|
|
||||||
var m := ShaderMaterial.new()
|
|
||||||
m.shader = _messing
|
## Zonenrand: Holz, das leuchten kann, wenn der Platz dran ist.
|
||||||
m.set_shader_parameter("emission", emission)
|
static func rand() -> StandardMaterial3D:
|
||||||
m.set_shader_parameter("wear", wear)
|
var m := eigene("holz")
|
||||||
|
m.emission = Color(1.0, 0.78, 0.30)
|
||||||
|
m.emission_enabled = false
|
||||||
return m
|
return m
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ const WALL_SEGS := 20 ## Bandensegmente - Godot kennt keinen hohlen Zylin
|
|||||||
const WALL_H := 0.55 ## unsichtbare Begrenzung, damit nichts herausspringt
|
const WALL_H := 0.55 ## unsichtbare Begrenzung, damit nichts herausspringt
|
||||||
const SEAT_ARC := ZONE_RAD * 2.0 + 0.85 ## Bogenlänge, die ein Platz am Tischrand braucht
|
const SEAT_ARC := ZONE_RAD * 2.0 + 0.85 ## Bogenlänge, die ein Platz am Tischrand braucht
|
||||||
const MIN_ZONE_R := 1.15
|
const MIN_ZONE_R := 1.15
|
||||||
const Stoffe = preload("res://scripts/stoffe.gd") ## Holz, Filz, Messing aus Rauschen
|
const Stoffe = preload("res://scripts/stoffe.gd") ## Materialien aus Regeln, ohne Dateien
|
||||||
|
const Formen = preload("res://scripts/formen.gd") ## Becher, Fass, Laterne aus Primitiven
|
||||||
const RIM_RUHE := Color(0.22, 0.15, 0.09) ## Zonenrand, wenn der Platz wartet
|
const RIM_RUHE := Color(0.22, 0.15, 0.09) ## Zonenrand, wenn der Platz wartet
|
||||||
const RIM_ZUG := Color(0.85, 0.66, 0.22) ## Zonenrand des Platzes, der dran ist
|
const RIM_ZUG := Color(0.85, 0.66, 0.22) ## Zonenrand des Platzes, der dran ist
|
||||||
const TILT_DEG := 15.0 ## Neigung der Kamera gegen die Senkrechte
|
const TILT_DEG := 15.0 ## Neigung der Kamera gegen die Senkrechte
|
||||||
@@ -33,7 +34,7 @@ var plates: Array[Sprite3D] = []
|
|||||||
var rims: Array[MeshInstance3D] = [] ## Rand der Zone, leuchtet beim Zug
|
var rims: Array[MeshInstance3D] = [] ## Rand der Zone, leuchtet beim Zug
|
||||||
var topf_muenzen: Dictionary = {} ## Wurfnummer -> Münze in der Tischmitte
|
var topf_muenzen: Dictionary = {} ## Wurfnummer -> Münze in der Tischmitte
|
||||||
static var _muenz_mesh: CylinderMesh
|
static var _muenz_mesh: CylinderMesh
|
||||||
static var _muenz_mat: ShaderMaterial
|
static var _muenz_mat: StandardMaterial3D
|
||||||
var dice: Array = [] ## seat -> Array[Die]
|
var dice: Array = [] ## seat -> Array[Die]
|
||||||
|
|
||||||
var camera: Camera3D
|
var camera: Camera3D
|
||||||
@@ -52,9 +53,19 @@ func build(count: int, local_index: int) -> void:
|
|||||||
for i in count:
|
for i in count:
|
||||||
_build_seat(i)
|
_build_seat(i)
|
||||||
dice.append([])
|
dice.append([])
|
||||||
|
_deko()
|
||||||
_build_camera()
|
_build_camera()
|
||||||
|
|
||||||
|
|
||||||
|
## Eine Laterne am Tischrand - weit genug draussen, dass sie keine Zone
|
||||||
|
## verdeckt, nah genug, dass ihr Schein den Filz erreicht. Das Fass aus den
|
||||||
|
## Formregeln bleibt weg: vom Boden aus ist es aus Kamerahöhe nur ein Fleck.
|
||||||
|
func _deko() -> void:
|
||||||
|
var laterne := Formen.laterne()
|
||||||
|
laterne.position = Vector3(-(table_r + 0.35), TABLE_H, table_r * 0.55)
|
||||||
|
add_child(laterne)
|
||||||
|
|
||||||
|
|
||||||
func _build_room() -> void:
|
func _build_room() -> void:
|
||||||
var env := WorldEnvironment.new()
|
var env := WorldEnvironment.new()
|
||||||
var e := Environment.new()
|
var e := Environment.new()
|
||||||
@@ -64,6 +75,10 @@ func _build_room() -> void:
|
|||||||
e.ambient_light_color = Color(0.35, 0.38, 0.45)
|
e.ambient_light_color = Color(0.35, 0.38, 0.45)
|
||||||
e.ambient_light_energy = 1.1
|
e.ambient_light_energy = 1.1
|
||||||
e.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
e.tonemap_mode = Environment.TONE_MAPPER_FILMIC
|
||||||
|
e.glow_enabled = true
|
||||||
|
e.glow_intensity = 0.45
|
||||||
|
e.glow_bloom = 0.05
|
||||||
|
e.glow_hdr_threshold = 0.9
|
||||||
e.fog_enabled = true
|
e.fog_enabled = true
|
||||||
e.fog_light_color = Color(0.05, 0.06, 0.09)
|
e.fog_light_color = Color(0.05, 0.06, 0.09)
|
||||||
e.fog_density = 0.02
|
e.fog_density = 0.02
|
||||||
@@ -81,6 +96,9 @@ func _build_room() -> void:
|
|||||||
lamp.shadow_enabled = true
|
lamp.shadow_enabled = true
|
||||||
add_child(lamp)
|
add_child(lamp)
|
||||||
|
|
||||||
|
# Kein sichtbarer Lampenschirm: die Kamera schaut fast senkrecht von oben,
|
||||||
|
# alles über der Tischmitte hinge ihr vor der Linse.
|
||||||
|
|
||||||
var fill := DirectionalLight3D.new()
|
var fill := DirectionalLight3D.new()
|
||||||
fill.rotation_degrees = Vector3(-55, 35, 0)
|
fill.rotation_degrees = Vector3(-55, 35, 0)
|
||||||
fill.light_energy = 0.25
|
fill.light_energy = 0.25
|
||||||
@@ -98,6 +116,8 @@ func _build_room() -> void:
|
|||||||
var fpm := PlaneMesh.new()
|
var fpm := PlaneMesh.new()
|
||||||
fpm.size = Vector2(24, 24)
|
fpm.size = Vector2(24, 24)
|
||||||
floor_mi.mesh = fpm
|
floor_mi.mesh = fpm
|
||||||
|
# Bewusst glatt und dunkel: eine Maserung auf der riesigen Fläche wird aus
|
||||||
|
# Kamerahöhe zu Rauschen, und der Nebel soll den Boden ohnehin schlucken.
|
||||||
floor_mi.material_override = _mat(Color(0.08, 0.07, 0.09), 0.9)
|
floor_mi.material_override = _mat(Color(0.08, 0.07, 0.09), 0.9)
|
||||||
floor_body.add_child(floor_mi)
|
floor_body.add_child(floor_mi)
|
||||||
add_child(floor_body)
|
add_child(floor_body)
|
||||||
@@ -118,7 +138,7 @@ func _build_room() -> void:
|
|||||||
tcm.height = 0.12
|
tcm.height = 0.12
|
||||||
tmi.mesh = tcm
|
tmi.mesh = tcm
|
||||||
tmi.position = Vector3(0, TABLE_H - 0.06, 0)
|
tmi.position = Vector3(0, TABLE_H - 0.06, 0)
|
||||||
tmi.material_override = Stoffe.filz(Color(0.10, 0.28, 0.17), table_r, 0.35)
|
tmi.material_override = Stoffe.filz()
|
||||||
table.add_child(tmi)
|
table.add_child(tmi)
|
||||||
add_child(table)
|
add_child(table)
|
||||||
|
|
||||||
@@ -130,7 +150,7 @@ func _build_room() -> void:
|
|||||||
lcm.height = TABLE_H - 0.12
|
lcm.height = TABLE_H - 0.12
|
||||||
leg.mesh = lcm
|
leg.mesh = lcm
|
||||||
leg.position = Vector3(0, (TABLE_H - 0.12) * 0.5, 0)
|
leg.position = Vector3(0, (TABLE_H - 0.12) * 0.5, 0)
|
||||||
leg.material_override = Stoffe.holz(Color(0.30, 0.19, 0.09), Color(0.16, 0.10, 0.05), 0.5, 4.0)
|
leg.material_override = Stoffe.holz()
|
||||||
add_child(leg)
|
add_child(leg)
|
||||||
|
|
||||||
|
|
||||||
@@ -167,7 +187,7 @@ func _build_seat(i: int) -> void:
|
|||||||
fm.radial_segments = 32
|
fm.radial_segments = 32
|
||||||
felt.mesh = fm
|
felt.mesh = fm
|
||||||
felt.position = Vector3(0, 0.006, 0)
|
felt.position = Vector3(0, 0.006, 0)
|
||||||
felt.material_override = Stoffe.filz(Color(0.07, 0.20, 0.13), ZONE_RAD, 0.6)
|
felt.material_override = Stoffe.filz()
|
||||||
zone.add_child(felt)
|
zone.add_child(felt)
|
||||||
|
|
||||||
# Sichtbarer Rand
|
# Sichtbarer Rand
|
||||||
@@ -179,7 +199,7 @@ func _build_seat(i: int) -> void:
|
|||||||
tm.ring_segments = 8
|
tm.ring_segments = 8
|
||||||
rim.mesh = tm
|
rim.mesh = tm
|
||||||
rim.position = Vector3(0, 0.02, 0)
|
rim.position = Vector3(0, 0.02, 0)
|
||||||
rim.material_override = _mat(RIM_RUHE, 0.6)
|
rim.material_override = Stoffe.rand()
|
||||||
zone.add_child(rim)
|
zone.add_child(rim)
|
||||||
rims.append(rim)
|
rims.append(rim)
|
||||||
|
|
||||||
@@ -187,17 +207,7 @@ func _build_seat(i: int) -> void:
|
|||||||
var cup := Node3D.new()
|
var cup := Node3D.new()
|
||||||
cup.position = Vector3(0, 0.02, 0)
|
cup.position = Vector3(0, 0.02, 0)
|
||||||
zone.add_child(cup)
|
zone.add_child(cup)
|
||||||
var cup_mi := MeshInstance3D.new()
|
cup.add_child(Formen.becher())
|
||||||
var cm := CylinderMesh.new()
|
|
||||||
cm.top_radius = 0.33
|
|
||||||
cm.bottom_radius = 0.42
|
|
||||||
cm.height = 0.44
|
|
||||||
cup_mi.mesh = cm
|
|
||||||
cup_mi.position = Vector3(0, 0.22, 0)
|
|
||||||
# Enge, weiche Maserung: die Ringe laufen am Becher senkrecht, zu grob sähe
|
|
||||||
# das nach Fassdauben aus.
|
|
||||||
cup_mi.material_override = Stoffe.holz(Color(0.40, 0.17, 0.11), Color(0.30, 0.12, 0.08), 0.5, 5.0)
|
|
||||||
cup.add_child(cup_mi)
|
|
||||||
cups.append(cup)
|
cups.append(cup)
|
||||||
cup_tweens.append(null)
|
cup_tweens.append(null)
|
||||||
|
|
||||||
@@ -550,7 +560,7 @@ func set_topf(liste: Array, ziel := -1) -> void:
|
|||||||
tw.tween_callback(mi.queue_free)
|
tw.tween_callback(mi.queue_free)
|
||||||
|
|
||||||
|
|
||||||
static var _muenz_mat_wappen: ShaderMaterial
|
static var _muenz_mat_wappen: StandardMaterial3D
|
||||||
static var _praeg_mesh: TorusMesh
|
static var _praeg_mesh: TorusMesh
|
||||||
static var _praeg_mat: StandardMaterial3D
|
static var _praeg_mat: StandardMaterial3D
|
||||||
|
|
||||||
@@ -567,9 +577,9 @@ static func _muenz_form() -> CylinderMesh:
|
|||||||
return _muenz_mesh
|
return _muenz_mesh
|
||||||
|
|
||||||
|
|
||||||
static func _muenz_stoff(wappen: bool) -> ShaderMaterial:
|
static func _muenz_stoff(wappen: bool) -> StandardMaterial3D:
|
||||||
if _muenz_mat == null:
|
if _muenz_mat == null:
|
||||||
_muenz_mat = Stoffe.messing(0.35)
|
_muenz_mat = Stoffe.messing(0.25)
|
||||||
_muenz_mat_wappen = Stoffe.messing(0.9)
|
_muenz_mat_wappen = Stoffe.messing(0.9)
|
||||||
return _muenz_mat_wappen if wappen else _muenz_mat
|
return _muenz_mat_wappen if wappen else _muenz_mat
|
||||||
|
|
||||||
@@ -620,12 +630,8 @@ func hervorhebung_weg() -> void:
|
|||||||
func set_active_seat(seat: int) -> void:
|
func set_active_seat(seat: int) -> void:
|
||||||
for i in rims.size():
|
for i in rims.size():
|
||||||
var m: StandardMaterial3D = rims[i].material_override
|
var m: StandardMaterial3D = rims[i].material_override
|
||||||
var dran: bool = i == seat
|
m.emission_enabled = i == seat
|
||||||
m.albedo_color = RIM_ZUG if dran else RIM_RUHE
|
m.emission_energy_multiplier = 1.2
|
||||||
m.emission_enabled = dran
|
|
||||||
if dran:
|
|
||||||
m.emission = Color(1.0, 0.78, 0.30)
|
|
||||||
m.emission_energy_multiplier = 1.1
|
|
||||||
|
|
||||||
|
|
||||||
func set_dice_visible(seat: int, on: bool) -> void:
|
func set_dice_visible(seat: int, on: bool) -> void:
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func _lauf() -> void:
|
|||||||
for d in game.table.dice_of(i):
|
for d in game.table.dice_of(i):
|
||||||
print(" Wuerfel Platz %d: pos=%s sleeping=%s freeze=%s settled=%s flat=%.2f" % [
|
print(" Wuerfel Platz %d: pos=%s sleeping=%s freeze=%s settled=%s flat=%.2f" % [
|
||||||
i, d.global_position, d.sleeping, d.freeze, d.is_settled(), d.flatness()])
|
i, d.global_position, d.sleeping, d.freeze, d.is_settled(), d.flatness()])
|
||||||
_pruef("Partie endet (%s, Stufe %d, %d Runden, %.0fs Spielzeit)" % [game.banner, DiceAI.stufe, runden, t], game.phase == "over")
|
_pruef("Partie endet (%s, Stufe %d, %d Runden, %.0fs Spielzeit)" % [game.banner, DiceAI.stufe, game.runde, t], game.phase == "over")
|
||||||
_pruef("Münzen wurden geworfen (%d)" % zaehler[" wirft "], zaehler[" wirft "] > 0)
|
_pruef("Münzen wurden geworfen (%d)" % zaehler[" wirft "], zaehler[" wirft "] > 0)
|
||||||
_pruef("Topf wurde vergeben (%d)" % zaehler["Topf"], zaehler["Topf"] > 0)
|
_pruef("Topf wurde vergeben (%d)" % zaehler["Topf"], zaehler["Topf"] > 0)
|
||||||
print(" Genau-Rufe=%d, vom Tisch=%d, Topf am Ende=%d" % [zaehler["Genau"], zaehler["vom Tisch"], game.topf.size()])
|
print(" Genau-Rufe=%d, vom Tisch=%d, Topf am Ende=%d" % [zaehler["Genau"], zaehler["vom Tisch"], game.topf.size()])
|
||||||
|
|||||||
@@ -88,6 +88,36 @@ func _lauf() -> void:
|
|||||||
_pruef("Würfeln endet trotz verirrtem Würfel, Wert gelesen",
|
_pruef("Würfeln endet trotz verirrtem Würfel, Wert gelesen",
|
||||||
d0.global_position.y > 0.5 and game.host_values[me][0] >= 1 and game.host_values[me][0] <= 6)
|
d0.global_position.y > 0.5 and game.host_values[me][0] >= 1 and game.host_values[me][0] <= 6)
|
||||||
|
|
||||||
|
# --- KI nutzt, was Spähen und Zählen verraten
|
||||||
|
var r := RandomNumberGenerator.new()
|
||||||
|
r.seed = 7
|
||||||
|
var gebot := {"seat": 1, "count": 4, "face": 5}
|
||||||
|
var a: Dictionary = DiceAI.choose_action([1, 2, 3], 9, gebot, false, r, {"fenster": {5: [1, 2]}})
|
||||||
|
_pruef("KI: Zählfenster unter der Ansage -> Anfechten", a.type == "challenge")
|
||||||
|
a = DiceAI.choose_action([5, 5, 2], 9, gebot, false, r, {"bekannt": [5, 5, 5]})
|
||||||
|
_pruef("KI: aufgedeckte Fünfen machen die Ansage sicher -> kein Anfechten", a.type != "challenge")
|
||||||
|
a = DiceAI.choose_action([1, 2, 3], 9, gebot, false, r, {"fenster": {5: [4, 4]}})
|
||||||
|
_pruef("KI: exaktes Fenster -> Genau", a.type == "exact")
|
||||||
|
|
||||||
|
# --- Tastatur: Taste tut nur, was der Knopf gerade darf
|
||||||
|
var taste := func(code: Key) -> void:
|
||||||
|
var ev := InputEventKey.new()
|
||||||
|
ev.keycode = code
|
||||||
|
ev.pressed = true
|
||||||
|
hud._unhandled_key_input(ev)
|
||||||
|
var gebote := []
|
||||||
|
hud.bid_pressed.connect(func(c, f): gebote.append([c, f]))
|
||||||
|
game._push_state()
|
||||||
|
await get_tree().create_timer(0.3).timeout
|
||||||
|
taste.call(KEY_3)
|
||||||
|
taste.call(KEY_PLUS)
|
||||||
|
taste.call(KEY_ENTER)
|
||||||
|
_pruef("Tastatur: 3, +, Enter sagt 2 x 3 an", gebote == [[2, 3]])
|
||||||
|
var angefochten := []
|
||||||
|
hud.challenge_pressed.connect(func(): angefochten.append(1))
|
||||||
|
taste.call(KEY_A)
|
||||||
|
_pruef("Tastatur: A ohne Ansage tut nichts", angefochten.is_empty())
|
||||||
|
|
||||||
# --- Protokoll und Tipp-Schranke im HUD
|
# --- Protokoll und Tipp-Schranke im HUD
|
||||||
game._push_state()
|
game._push_state()
|
||||||
await get_tree().create_timer(0.3).timeout
|
await get_tree().create_timer(0.3).timeout
|
||||||
|
|||||||
2
tests/out/.gitignore
vendored
Normal file
2
tests/out/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
52
tests/shots.gd
Normal file
52
tests/shots.gd
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
extends "res://tests/basis.gd"
|
||||||
|
## Bilder vom Spiel und vom Menü für die Sichtprüfung. Braucht ein Display -
|
||||||
|
## oder xvfb, siehe tests/shots.sh. Schreibt nach tests/out/.
|
||||||
|
|
||||||
|
var game: Node
|
||||||
|
const OUT := "res://tests/out"
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(OUT))
|
||||||
|
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
|
||||||
|
DisplayServer.window_set_size(Vector2i(1280, 720))
|
||||||
|
var main := preload("res://scenes/main.tscn").instantiate()
|
||||||
|
add_child(main)
|
||||||
|
await get_tree().process_frame
|
||||||
|
main.menu._show("regeln")
|
||||||
|
await get_tree().create_timer(0.3).timeout
|
||||||
|
await _bild("menue_regeln")
|
||||||
|
main.menu._show("main")
|
||||||
|
await get_tree().create_timer(0.2).timeout
|
||||||
|
await _bild("menue_start")
|
||||||
|
Net.start_singleplayer("Tester", 3, 1)
|
||||||
|
_lauf()
|
||||||
|
|
||||||
|
|
||||||
|
func _bild(name: String) -> void:
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
var ok := get_viewport().get_texture().get_image().save_png("%s/%s.png" % [OUT, name])
|
||||||
|
_pruef("Bild %s" % name, ok == OK)
|
||||||
|
|
||||||
|
|
||||||
|
func _lauf() -> void:
|
||||||
|
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null, 20.0)
|
||||||
|
await _warte(func(): return game.phase == "bid", 40.0)
|
||||||
|
var me: int = game.local_seat
|
||||||
|
game.hand[me] = ["gag", "peek", "count"]
|
||||||
|
game._push_state()
|
||||||
|
await get_tree().create_timer(0.4).timeout
|
||||||
|
await _bild("tisch_zug")
|
||||||
|
await game._do_play(me, "gag", 0)
|
||||||
|
await game._do_play(me, "peek", 0)
|
||||||
|
await get_tree().create_timer(1.2).timeout
|
||||||
|
await _bild("tisch_topf")
|
||||||
|
game.hud.hand_box.get_child(0).mouse_entered.emit()
|
||||||
|
await get_tree().create_timer(0.3).timeout
|
||||||
|
await _bild("tisch_erklaerung")
|
||||||
|
var n5: int = game._count_face(5)
|
||||||
|
game.bid = {"seat": 1, "count": maxi(1, n5), "face": 5}
|
||||||
|
game._phase_reveal({"challenger": me, "bid": game.bid, "exact": false})
|
||||||
|
await get_tree().create_timer(1.3 + 1.2).timeout
|
||||||
|
await _bild("aufdecken_zaehlen")
|
||||||
|
_fertig()
|
||||||
14
tests/shots.sh
Executable file
14
tests/shots.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Screenshots für die Sichtprüfung nach tests/out/. Mit Display direkt, ohne
|
||||||
|
# Display über xvfb-run (Paket xvfb). Aufruf: make shots
|
||||||
|
set -u
|
||||||
|
GODOT="${GODOT:-$HOME/bin/godot}"
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
if [ -n "${DISPLAY:-}${WAYLAND_DISPLAY:-}" ]; then
|
||||||
|
timeout 120 "$GODOT" --path . res://tests/shots.tscn 2>&1 | grep -E "^(OK |FEHL|TEST:)"
|
||||||
|
elif command -v xvfb-run >/dev/null; then
|
||||||
|
timeout 120 xvfb-run -a -s "-screen 0 1280x720x24" "$GODOT" --path . res://tests/shots.tscn 2>&1 | grep -E "^(OK |FEHL|TEST:)"
|
||||||
|
else
|
||||||
|
echo "Kein Display und kein xvfb-run - Screenshots übersprungen."; exit 0
|
||||||
|
fi
|
||||||
|
ls -1 tests/out/
|
||||||
6
tests/shots.tscn
Normal file
6
tests/shots.tscn
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://tests/shots.gd" id="1"]
|
||||||
|
|
||||||
|
[node name="Test" type="Node"]
|
||||||
|
script = ExtResource("1")
|
||||||
15
tests/sim.gd
15
tests/sim.gd
@@ -29,18 +29,13 @@ func _lauf() -> void:
|
|||||||
Net.start_singleplayer("Tester", 2, stufe, 3)
|
Net.start_singleplayer("Tester", 2, stufe, 3)
|
||||||
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null and game.phase != "lobby", 20.0)
|
await _warte(func(): game = _find(get_tree().root, "Game"); return game != null and game.phase != "lobby", 20.0)
|
||||||
game.tempo = 0.0
|
game.tempo = 0.0
|
||||||
|
game.simulation = true
|
||||||
game.seats[game.local_seat].ai = true
|
game.seats[game.local_seat].ai = true
|
||||||
var runden := 0
|
|
||||||
var letzte := ""
|
|
||||||
var gesehen := {}
|
var gesehen := {}
|
||||||
var t := 0.0
|
var t := 0.0
|
||||||
while game.phase != "over" and t < 300.0:
|
while game.phase != "over" and t < 300.0:
|
||||||
await get_tree().create_timer(0.1).timeout
|
await get_tree().create_timer(0.02).timeout
|
||||||
t += 0.1
|
t += 0.02
|
||||||
if game.phase != letzte:
|
|
||||||
letzte = game.phase
|
|
||||||
if letzte == "roll":
|
|
||||||
runden += 1
|
|
||||||
for z in game.log_lines:
|
for z in game.log_lines:
|
||||||
if gesehen.has(z):
|
if gesehen.has(z):
|
||||||
continue
|
continue
|
||||||
@@ -49,8 +44,8 @@ func _lauf() -> void:
|
|||||||
genau += 1
|
genau += 1
|
||||||
if " wirft " in z:
|
if " wirft " in z:
|
||||||
wuerfe += 1
|
wuerfe += 1
|
||||||
runden_summe += runden
|
runden_summe += game.runde
|
||||||
laengste = maxi(laengste, runden)
|
laengste = maxi(laengste, game.runde)
|
||||||
var sieger := ""
|
var sieger := ""
|
||||||
for s in game.seats:
|
for s in game.seats:
|
||||||
if s.alive:
|
if s.alive:
|
||||||
|
|||||||
Reference in New Issue
Block a user