M1c: Gleichschritt im Netz #9

Merged
steven merged 20 commits from m1c-gleichschritt-im-netz into main 2026-10-02 06:41:02 +00:00
12 changed files with 284 additions and 5 deletions
Showing only changes of commit d2da661452 - Show all commits
+27
View File
@@ -8,3 +8,30 @@ INVENTORY_FULL,Die Tasche ist voll,Your bag is full
PAUSED,Pause,Paused PAUSED,Pause,Paused
SIMULATION_STOPPED,Die Simulation wurde angehalten,The simulation stopped SIMULATION_STOPPED,Die Simulation wurde angehalten,The simulation stopped
HELP,WASD: laufen · Linke Maustaste: Stein schlagen · F5: speichern · F9: laden · Esc: Pause,WASD: walk · Left mouse button: break rock · F5: save · F9: load · Esc: pause HELP,WASD: laufen · Linke Maustaste: Stein schlagen · F5: speichern · F9: laden · Esc: Pause,WASD: walk · Left mouse button: break rock · F5: save · F9: load · Esc: pause
DEFAULT_NAME,Spieler,Player
MENU_NAME,Dein Name,Your name
MENU_SOLO,Allein spielen,Play alone
MENU_HOST,Welt öffnen,Open your world
MENU_ADDRESS,Adresse des Hosts,Address of the host
MENU_JOIN,Beitreten,Join
CONNECTING,Verbinde mit %s …,Connecting to %s …
CONNECT_FAILED,Keine Verbindung zu %s,Could not reach %s
ADDRESS_INVALID,Das ist keine Adresse: %s,That is not an address: %s
HOST_LEFT,Der Host hat die Welt geschlossen,The host closed the world
PROTOCOL_ERROR,Die Verbindung zum Host ist gestört,The connection to the host is broken
REFUSED_OTHER_PROTOCOL,Der Host hat eine andere Version des Spiels,The host has another version of the game
REFUSED_OTHER_RULES,Der Host hat andere Spielregeln. Ihr braucht denselben Stand des Spiels.,The host has other rules. You need the same version of the game.
REFUSED_WORLD_FULL,Die Welt ist voll: Vier Spieler sind schon da,The world is full: four players are in it
REFUSED_TOO_MANY_PLAYERS,Diese Welt kennt schon 16 Spieler und nimmt keine neuen mehr auf,This world knows 16 players already and takes no new ones
REFUSED_ALREADY_PLAYING,Ein Spieler mit deinem Schlüssel ist schon in der Welt,A player with your key is in the world already
STATUS_HOST,Welt offen auf Port %d · Spieler: %d,World open on port %d · players: %d
STATUS_GUEST,Gast bei %s · Spieler: %d,Guest of %s · players: %d
OPEN_FAILED,Port %d lässt sich nicht öffnen,Port %d cannot be opened
PLAYER_JOINED,%s ist da,%s joined
PLAYER_LEFT,%s ist gegangen,%s left
RESYNCED,Welt mit dem Host abgeglichen,World matched with the host again
PAUSE_ONLY_ALONE,Pause gibt es nur ohne Gäste,Pausing only works without guests
LOAD_ONLY_ALONE,Laden gibt es nur beim Spielen allein,Loading only works when playing alone
HOST_SAVES,Nur der Host speichert,Only the host saves
KEY_UNREADABLE,Dein Spielerschlüssel lässt sich gerade nicht lesen,Your player key cannot be read right now
SAVE_UNREADABLE,Der Spielstand lässt sich gerade nicht lesen,The save cannot be read right now
1 keys de en
8 PAUSED Pause Paused
9 SIMULATION_STOPPED Die Simulation wurde angehalten The simulation stopped
10 HELP WASD: laufen · Linke Maustaste: Stein schlagen · F5: speichern · F9: laden · Esc: Pause WASD: walk · Left mouse button: break rock · F5: save · F9: load · Esc: pause
11 DEFAULT_NAME Spieler Player
12 MENU_NAME Dein Name Your name
13 MENU_SOLO Allein spielen Play alone
14 MENU_HOST Welt öffnen Open your world
15 MENU_ADDRESS Adresse des Hosts Address of the host
16 MENU_JOIN Beitreten Join
17 CONNECTING Verbinde mit %s … Connecting to %s …
18 CONNECT_FAILED Keine Verbindung zu %s Could not reach %s
19 ADDRESS_INVALID Das ist keine Adresse: %s That is not an address: %s
20 HOST_LEFT Der Host hat die Welt geschlossen The host closed the world
21 PROTOCOL_ERROR Die Verbindung zum Host ist gestört The connection to the host is broken
22 REFUSED_OTHER_PROTOCOL Der Host hat eine andere Version des Spiels The host has another version of the game
23 REFUSED_OTHER_RULES Der Host hat andere Spielregeln. Ihr braucht denselben Stand des Spiels. The host has other rules. You need the same version of the game.
24 REFUSED_WORLD_FULL Die Welt ist voll: Vier Spieler sind schon da The world is full: four players are in it
25 REFUSED_TOO_MANY_PLAYERS Diese Welt kennt schon 16 Spieler und nimmt keine neuen mehr auf This world knows 16 players already and takes no new ones
26 REFUSED_ALREADY_PLAYING Ein Spieler mit deinem Schlüssel ist schon in der Welt A player with your key is in the world already
27 STATUS_HOST Welt offen auf Port %d · Spieler: %d World open on port %d · players: %d
28 STATUS_GUEST Gast bei %s · Spieler: %d Guest of %s · players: %d
29 OPEN_FAILED Port %d lässt sich nicht öffnen Port %d cannot be opened
30 PLAYER_JOINED %s ist da %s joined
31 PLAYER_LEFT %s ist gegangen %s left
32 RESYNCED Welt mit dem Host abgeglichen World matched with the host again
33 PAUSE_ONLY_ALONE Pause gibt es nur ohne Gäste Pausing only works without guests
34 LOAD_ONLY_ALONE Laden gibt es nur beim Spielen allein Loading only works when playing alone
35 HOST_SAVES Nur der Host speichert Only the host saves
36 KEY_UNREADABLE Dein Spielerschlüssel lässt sich gerade nicht lesen Your player key cannot be read right now
37 SAVE_UNREADABLE Der Spielstand lässt sich gerade nicht lesen The save cannot be read right now
+63
View File
@@ -0,0 +1,63 @@
class_name Figures
extends Node2D
## The figures of the other players, with their names. A new position comes
## over the network about ten times a second; in between, each figure glides
## toward the latest one.
## How quickly a figure closes the gap to its latest position, per second.
const GLIDE := 15.0
const COLOR := Color(0.55, 0.8, 0.95)
const OUTLINE := Color(0.05, 0.1, 0.15)
const NAME_SIZE := 8
const NAME_WIDTH := 80.0
var session: GwSession
var _shown: Dictionary[int, Vector2] = {}
var _latest: Dictionary[int, Vector2] = {}
## Moves the figure of `player` toward `position`. A figure that was not
## shown yet appears there at once.
func place(player: int, position: Vector2) -> void:
if not _shown.has(player):
_shown[player] = position
_latest[player] = position
queue_redraw()
## Removes the figure of `player`.
func forget(player: int) -> void:
_shown.erase(player)
_latest.erase(player)
queue_redraw()
func clear() -> void:
_shown.clear()
_latest.clear()
queue_redraw()
## Where the figure of `player` is shown, or Vector2.INF if it is not.
func shown_at(player: int) -> Vector2:
return _shown.get(player, Vector2.INF)
func _process(delta: float) -> void:
if _shown.is_empty():
return
var weight := minf(1.0, GLIDE * delta)
for player in _shown:
_shown[player] = _shown[player].lerp(_latest[player], weight)
queue_redraw()
func _draw() -> void:
var font := ThemeDB.fallback_font
for player in _shown:
var at := _shown[player]
draw_circle(at, Avatar.RADIUS + 1.0, OUTLINE)
draw_circle(at, Avatar.RADIUS, COLOR)
if session != null:
var label := Vector2(at.x - NAME_WIDTH / 2.0, at.y - Avatar.RADIUS - 3.0)
draw_string(font, label, session.player_name(player), HORIZONTAL_ALIGNMENT_CENTER, NAME_WIDTH, NAME_SIZE)
+1
View File
@@ -0,0 +1 @@
uid://0krucvyguufx
+21 -5
View File
@@ -1,11 +1,13 @@
class_name Hud class_name Hud
extends CanvasLayer extends CanvasLayer
## Text over the game: the stone count, short messages and a help line. ## Text over the game: the stone count, short messages, a status line for
## playing together and a help line.
const MESSAGE_SECONDS := 2.5 const MESSAGE_SECONDS := 2.5
var _stone := Label.new() var _stone := Label.new()
var _message := Label.new() var _message := Label.new()
var _status := Label.new()
var _help := Label.new() var _help := Label.new()
var _message_left := 0.0 var _message_left := 0.0
@@ -15,9 +17,12 @@ func _ready() -> void:
_stone.add_theme_font_size_override("font_size", 22) _stone.add_theme_font_size_override("font_size", 22)
_message.position = Vector2(16, 48) _message.position = Vector2(16, 48)
_message.add_theme_font_size_override("font_size", 18) _message.add_theme_font_size_override("font_size", 18)
_status.add_theme_font_size_override("font_size", 14)
_status.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT, Control.PRESET_MODE_MINSIZE, 16)
_status.grow_horizontal = Control.GROW_DIRECTION_BEGIN
_help.text = tr("HELP") _help.text = tr("HELP")
_help.add_theme_font_size_override("font_size", 14) _help.add_theme_font_size_override("font_size", 14)
for label: Label in [_stone, _message, _help]: for label: Label in [_stone, _message, _status, _help]:
label.add_theme_color_override("font_outline_color", Color.BLACK) label.add_theme_color_override("font_outline_color", Color.BLACK)
label.add_theme_constant_override("outline_size", 4) label.add_theme_constant_override("outline_size", 4)
add_child(label) add_child(label)
@@ -36,9 +41,10 @@ func show_stone(count: int) -> void:
_stone.text = tr("STONE_COUNT") % count _stone.text = tr("STONE_COUNT") % count
## Shows the text for `key` for a few seconds. ## Shows the text for `key` for a few seconds, with `values` filled into
func flash(key: String) -> void: ## its placeholders.
_message.text = tr(key) func flash(key: String, values: Array = []) -> void:
_message.text = tr(key) % values if not values.is_empty() else tr(key)
_message_left = MESSAGE_SECONDS _message_left = MESSAGE_SECONDS
@@ -48,6 +54,16 @@ func show_paused(paused: bool) -> void:
_message_left = 0.0 _message_left = 0.0
## Shows `text` in the top right corner until it is replaced.
func show_status(text: String) -> void:
_status.text = text
## The text currently shown in the status line. Tests read it.
func status_text() -> String:
return _status.text
## The text currently shown as a message. Tests read it. ## The text currently shown as a message. Tests read it.
func message() -> String: func message() -> String:
return _message.text return _message.text
+65
View File
@@ -0,0 +1,65 @@
class_name Menu
extends CanvasLayer
## The start screen: the player's name, then playing alone, opening the world
## for guests, or joining a host.
signal play_chosen(open: bool)
signal join_chosen(address: String)
var name_field := LineEdit.new()
var address_field := LineEdit.new()
var solo_button := Button.new()
var host_button := Button.new()
var join_button := Button.new()
var _status := Label.new()
func _ready() -> void:
var center := CenterContainer.new()
center.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
add_child(center)
var panel := PanelContainer.new()
center.add_child(panel)
var box := VBoxContainer.new()
box.custom_minimum_size = Vector2(380, 0)
box.add_theme_constant_override("separation", 10)
panel.add_child(box)
var title := Label.new()
title.text = "Grovewright"
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.add_theme_font_size_override("font_size", 32)
name_field.placeholder_text = tr("MENU_NAME")
name_field.max_length = 32
solo_button.text = tr("MENU_SOLO")
host_button.text = tr("MENU_HOST")
var row := HBoxContainer.new()
for button in [solo_button, host_button]:
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(button)
address_field.placeholder_text = tr("MENU_ADDRESS")
join_button.text = tr("MENU_JOIN")
_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_status.custom_minimum_size = Vector2(380, 0)
for control: Control in [title, name_field, row, HSeparator.new(), address_field, join_button, _status]:
box.add_child(control)
solo_button.pressed.connect(func() -> void: play_chosen.emit(false))
host_button.pressed.connect(func() -> void: play_chosen.emit(true))
join_button.pressed.connect(func() -> void: join_chosen.emit(address_field.text.strip_edges()))
address_field.text_submitted.connect(func(_text: String) -> void: join_button.pressed.emit())
## Shows `text` below the buttons.
func show_status(text: String) -> void:
_status.text = text
## The text shown below the buttons. Tests read it.
func status() -> String:
return _status.text
## The name the player typed, without spaces around it.
func player_name() -> String:
return name_field.text.strip_edges()
+1
View File
@@ -0,0 +1 @@
uid://dppcwwh67rtu6
+34
View File
@@ -0,0 +1,34 @@
extends TestCase
func test_a_figure_appears_where_it_is_first_placed() -> void:
var figures := Figures.new()
check_eq(figures.shown_at(2), Vector2.INF, "nobody is shown at first")
figures.place(2, Vector2(10.0, 20.0))
check_eq(figures.shown_at(2), Vector2(10.0, 20.0), "at once at its first position")
figures.free()
func test_a_figure_glides_toward_its_latest_position() -> void:
var figures := Figures.new()
figures.place(1, Vector2.ZERO)
figures.place(1, Vector2(100.0, 0.0))
figures._process(0.02)
var halfway := figures.shown_at(1).x
check(halfway > 0.0 and halfway < 100.0, "part of the way after a frame: %s" % halfway)
for frame in 60:
figures._process(0.02)
check(figures.shown_at(1).distance_to(Vector2(100.0, 0.0)) < 0.01, "there after a second")
figures.free()
func test_figures_can_be_forgotten() -> void:
var figures := Figures.new()
figures.place(1, Vector2.ZERO)
figures.place(3, Vector2.ONE)
figures.forget(1)
check_eq(figures.shown_at(1), Vector2.INF, "one is gone")
check_eq(figures.shown_at(3), Vector2.ONE, "the other stays")
figures.clear()
check_eq(figures.shown_at(3), Vector2.INF, "all are gone")
figures.free()
+1
View File
@@ -0,0 +1 @@
uid://x5pqgp2p1hku
+32
View File
@@ -0,0 +1,32 @@
extends TestCase
func _hud() -> Hud:
I18n.install()
var hud := Hud.new()
tree.root.add_child(hud)
return hud
func _free(hud: Hud) -> void:
tree.root.remove_child(hud)
hud.free()
func test_a_message_can_carry_values() -> void:
var hud := _hud()
hud.flash("PLAYER_JOINED", ["Grace"])
check_eq(hud.message(), tr("PLAYER_JOINED") % "Grace", "the name is filled in")
hud.flash("SAVED")
check_eq(hud.message(), tr("SAVED"), "a message without values")
_free(hud)
func test_the_status_line_stays_until_it_is_replaced() -> void:
var hud := _hud()
hud.show_status(tr("STATUS_HOST") % [24680, 2])
hud.flash("SAVED")
hud._process(10.0)
check_eq(hud.message(), "", "the message is gone after a while")
check_eq(hud.status_text(), tr("STATUS_HOST") % [24680, 2], "the status line is not")
_free(hud)
+1
View File
@@ -0,0 +1 @@
uid://8e4vlt0gnipa
+37
View File
@@ -0,0 +1,37 @@
extends TestCase
func _menu() -> Menu:
I18n.install()
var menu := Menu.new()
tree.root.add_child(menu)
return menu
func _free(menu: Menu) -> void:
tree.root.remove_child(menu)
menu.free()
func test_the_buttons_say_what_the_player_chose() -> void:
var menu := _menu()
var chosen: Array = []
menu.play_chosen.connect(func(open: bool) -> void: chosen.append(["play", open]))
menu.join_chosen.connect(func(address: String) -> void: chosen.append(["join", address]))
menu.address_field.text = " 192.168.178.20:25000 "
menu.solo_button.pressed.emit()
menu.host_button.pressed.emit()
menu.join_button.pressed.emit()
menu.address_field.text_submitted.emit(menu.address_field.text)
check_eq(chosen, [["play", false], ["play", true], ["join", "192.168.178.20:25000"], ["join", "192.168.178.20:25000"]], "in order")
_free(menu)
func test_the_name_and_the_status_line() -> void:
var menu := _menu()
menu.name_field.text = " Ada "
check_eq(menu.player_name(), "Ada", "without the spaces around it")
menu.show_status("Verbinde …")
check_eq(menu.status(), "Verbinde …", "the status is shown")
check_eq(menu.solo_button.text, tr("MENU_SOLO"), "the labels come from the texts")
_free(menu)
+1
View File
@@ -0,0 +1 @@
uid://dh6uogkq28wa4