Diff
1diff --git a/internal/game/orchestrator.go b/internal/game/orchestrator.go
2index a0222dde8221ee36489477110916fccac86be3e1..cd77b9a3f7cfd885d6486a63465edfa08e365ade 100644
3--- a/internal/game/orchestrator.go
4+++ b/internal/game/orchestrator.go
5@@ -176,15 +176,8 @@ func (o *Orchestrator) advance(ctx context.Context, displayAction string) TurnRe
6 return res
7 }
8 res.Reply = reply
9- o.applyReplyAndCompact(ctx, reply)
10- shown, desc := o.handleNewPartner(ctx, reply)
11-
12- if hasSpeech(shown) {
13- o.lastKana = shown.Kana
14- if err := o.speechOut.Speak(ctx, shown.Kana); err != nil {
15- res.SpeakErr = err
16- }
17- }
18+ shown, desc, speakErr := o.applyAndSpeak(ctx, reply)
19+ res.SpeakErr = speakErr
20 o.state.RecordDisplay(displayFromTurn(displayAction, shown, desc, llm.JudgeResult{}, false))
21 return res
22 }
23@@ -255,15 +248,8 @@ func (o *Orchestrator) FinishSpeak(ctx context.Context) TurnResult {
24 res.Reply = reply
25 res.HasJudge = true
26 res.JudgeErr = jerr
27- o.applyReplyAndCompact(ctx, reply)
28- shown, desc := o.handleNewPartner(ctx, reply)
29-
30- if hasSpeech(shown) {
31- o.lastKana = shown.Kana
32- if err := o.speechOut.Speak(ctx, shown.Kana); err != nil {
33- res.SpeakErr = err
34- }
35- }
36+ shown, desc, speakErr := o.applyAndSpeak(ctx, reply)
37+ res.SpeakErr = speakErr
38 judge := llm.JudgeResult{}
39 if jerr == nil {
40 judge = jres
41@@ -272,6 +258,20 @@ func (o *Orchestrator) FinishSpeak(ctx context.Context) TurnResult {
42 return res
43 }
44
45+// applyAndSpeak runs the shared post-reply pipeline: record the reply in state,
46+// handle any new partner, and play speech if present. Returns the shown reply
47+// (after potential new-partner rewrite), its description, and any speech error.
48+func (o *Orchestrator) applyAndSpeak(ctx context.Context, reply llm.GameReply) (llm.GameReply, string, error) {
49+ o.applyReplyAndCompact(ctx, reply)
50+ shown, desc := o.handleNewPartner(ctx, reply)
51+ var speakErr error
52+ if hasSpeech(shown) {
53+ o.lastKana = shown.Kana
54+ speakErr = o.speechOut.Speak(ctx, shown.Kana)
55+ }
56+ return shown, desc, speakErr
57+}
58+
59 // applyReplyAndCompact records the reply in the session and, when it moved the
60 // player, compacts the closed segment into a summary. Compaction failures fall
61 // back to a minimal placeholder so state always stays consistent.