diff --git a/agentscope-core/src/main/java/io/agentscope/core/agent/StructuredOutputCapableAgent.java b/agentscope-core/src/main/java/io/agentscope/core/agent/StructuredOutputCapableAgent.java
index 5ab24d27d..5ad5233d3 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/agent/StructuredOutputCapableAgent.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/agent/StructuredOutputCapableAgent.java
@@ -44,22 +44,24 @@
/**
* Abstract base class for agents that support structured output generation.
*
- *
This class provides the infrastructure for generating structured output using the
- * {@code generate_response} tool pattern combined with StructuredOutputHook for flow control.
+ *
This class provides the infrastructure for generating structured output using the {@code
+ * generate_response} tool pattern combined with StructuredOutputHook for flow control.
*
*
Key Features:
+ *
*
- *
Automatic tool registration for structured output
- *
Schema validation before tool execution
- *
Memory compression after structured output completion
- *
Configurable reminder mode (TOOL_CHOICE or PROMPT)
+ *
Automatic tool registration for structured output
+ *
Schema validation before tool execution
+ *
Memory compression after structured output completion
+ *
Configurable reminder mode (TOOL_CHOICE or PROMPT)
*
*
*
Subclass Requirements:
+ *
*
- *
Provide Toolkit via constructor
- *
Implement {@link #getMemory()} for memory access
- *
Implement {@link #buildGenerateOptions()} for model options
+ *
Provide Toolkit via constructor
+ *
Implement {@link #getMemory()} for memory access
+ *
Implement {@link #buildGenerateOptions()} for model options
*
*/
public abstract class StructuredOutputCapableAgent extends AgentBase {
@@ -72,9 +74,7 @@ public abstract class StructuredOutputCapableAgent extends AgentBase {
protected final Toolkit toolkit;
protected final StructuredOutputReminder structuredOutputReminder;
- /**
- * Constructor with default reminder mode (TOOL_CHOICE).
- */
+ /** Constructor with default reminder mode (TOOL_CHOICE). */
protected StructuredOutputCapableAgent(
String name,
String description,
@@ -84,9 +84,7 @@ protected StructuredOutputCapableAgent(
this(name, description, checkRunning, hooks, toolkit, StructuredOutputReminder.TOOL_CHOICE);
}
- /**
- * Constructor with custom reminder mode.
- */
+ /** Constructor with custom reminder mode. */
protected StructuredOutputCapableAgent(
String name,
String description,
@@ -102,23 +100,15 @@ protected StructuredOutputCapableAgent(
: StructuredOutputReminder.TOOL_CHOICE;
}
- /**
- * Get the toolkit for tool operations.
- */
+ /** Get the toolkit for tool operations. */
public Toolkit getToolkit() {
return toolkit;
}
- /**
- * Get the memory for structured output hook.
- * Subclasses must implement this.
- */
+ /** Get the memory for structured output hook. Subclasses must implement this. */
public abstract Memory getMemory();
- /**
- * Build generate options for model calls.
- * Subclasses must implement this.
- */
+ /** Build generate options for model calls. Subclasses must implement this. */
protected abstract GenerateOptions buildGenerateOptions();
// ==================== Structured Output Implementation ====================
@@ -133,9 +123,7 @@ protected final Mono doCall(List msgs, JsonNode outputSchema) {
return executeWithStructuredOutput(msgs, null, outputSchema);
}
- /**
- * Execute with structured output using StructuredOutputHook.
- */
+ /** Execute with structured output using StructuredOutputHook. */
private Mono executeWithStructuredOutput(
List msgs, Class> targetClass, JsonNode schemaDesc) {
@@ -196,9 +184,7 @@ private Mono executeWithStructuredOutput(
});
}
- /**
- * Create the structured output tool with validation.
- */
+ /** Create the structured output tool with validation. */
private AgentTool createStructuredOutputTool(
Map schema, Class> targetClass, JsonNode schemaDesc) {
return new AgentTool() {
@@ -244,7 +230,7 @@ public Mono callAsync(ToolCallParam param) {
// Create response message
Msg responseMsg =
Msg.builder()
- .name(getName())
+ .name(param.getAgent().getName())
.role(MsgRole.ASSISTANT)
.content(TextBlock.builder().text(contentText).build())
.metadata(
@@ -268,9 +254,7 @@ public Mono callAsync(ToolCallParam param) {
};
}
- /**
- * Extract structured result from tool result message.
- */
+ /** Extract structured result from tool result message. */
private Msg extractStructuredResult(Msg hookResultMsg) {
if (hookResultMsg == null) {
return null;
@@ -310,9 +294,7 @@ private Msg extractResponseData(Msg responseMsg) {
return responseMsg;
}
- /**
- * Merge collected metadata (ChatUsage and ThinkingBlock) into the message.
- */
+ /** Merge collected metadata (ChatUsage and ThinkingBlock) into the message. */
private Msg mergeCollectedMetadata(Msg msg, ChatUsage chatUsage, ThinkingBlock thinking) {
// Merge ChatUsage into metadata
Map metadata =
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/WerewolfGameConfig.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/WerewolfGameConfig.java
index 6a35f3a04..99acacb22 100644
--- a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/WerewolfGameConfig.java
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/WerewolfGameConfig.java
@@ -33,10 +33,10 @@ public class WerewolfGameConfig {
// Game rules
public static final int MAX_ROUNDS = 30;
- public static final int MAX_DISCUSSION_ROUNDS = 2;
+ public static final int MAX_DISCUSSION_ROUNDS = 1;
// Model configuration
- public static final String DEFAULT_MODEL = "qwen-plus";
+ public static final String DEFAULT_MODEL = "qwen3-max";
private WerewolfGameConfig() {
// Utility class
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/entity/GameState.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/entity/GameState.java
index 0e851b9ea..a3724c930 100644
--- a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/entity/GameState.java
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/entity/GameState.java
@@ -19,9 +19,7 @@
import java.util.List;
import java.util.stream.Collectors;
-/**
- * Represents the current state of the Werewolf game.
- */
+/** Represents the current state of the Werewolf game. */
public class GameState {
private final List allPlayers;
private final List seers;
@@ -33,9 +31,17 @@ public class GameState {
private Player lastPoisonedVictim;
private boolean lastVictimResurrected;
+ // First night jump candidate (悍跳候选人)
+ private Player jumpCandidate; // 第一夜狼人投票选出的悍跳候选人
+
+ // Sheriff election state
+ private Player sheriff; // 当前警长
+ private boolean speakOrderReversed; // 发言顺序是否逆序(警长决定)
+ private boolean sheriffKilledInNight; // 警长是否在夜间被杀(用于天亮后移交警徽)
+
/**
- * Constructs a new GameState instance with the provided players.
- * Special role players (seer, witch, hunter) are detected from the list.
+ * Constructs a new GameState instance with the provided players. Special role players (seer,
+ * witch, hunter) are detected from the list.
*
* @param allPlayers the list of all players participating in the game
*/
@@ -47,6 +53,11 @@ public GameState(List allPlayers) {
this.seers = findPlayersByRole(Role.SEER);
this.witches = findPlayersByRole(Role.WITCH);
this.hunters = findPlayersByRole(Role.HUNTER);
+
+ // Initialize sheriff state
+ this.sheriff = null;
+ this.speakOrderReversed = false;
+ this.sheriffKilledInNight = false;
}
private List findPlayersByRole(Role role) {
@@ -184,10 +195,53 @@ public boolean isLastVictimResurrected() {
return lastVictimResurrected;
}
+ /**
+ * Returns the current sheriff.
+ *
+ * @return the sheriff player, or null if none
+ */
+ public Player getSheriff() {
+ return sheriff;
+ }
+
+ /**
+ * Indicates whether the speaking order is reversed.
+ *
+ * @return true if speaking order is reversed; false otherwise
+ */
+ public boolean isSpeakOrderReversed() {
+ return speakOrderReversed;
+ }
+
+ /**
+ * Indicates whether the sheriff was killed during the night.
+ *
+ * @return true if sheriff was killed at night; false otherwise
+ */
+ public boolean isSheriffKilledInNight() {
+ return sheriffKilledInNight;
+ }
+
+ /**
+ * Returns the jump candidate selected by werewolves on the first night.
+ *
+ * @return the jump candidate player, or null if none
+ */
+ public Player getJumpCandidate() {
+ return jumpCandidate;
+ }
+
// State modifiers
/**
- * Increments the round counter by one to start a new round.
+ * Sets the jump candidate selected by werewolves on the first night.
+ *
+ * @param jumpCandidate the werewolf player selected to jump as seer
*/
+ public void setJumpCandidate(Player jumpCandidate) {
+ this.jumpCandidate = jumpCandidate;
+ }
+
+ /** Increments the round counter by one to start a new round. */
public void nextRound() {
this.currentRound++;
}
@@ -220,8 +274,8 @@ public void setLastVictimResurrected(boolean resurrected) {
}
/**
- * Clears last night results, including the werewolf victim, poisoned victim,
- * and resurrection flag, preparing for the next night.
+ * Clears last night results, including the werewolf victim, poisoned victim, and resurrection
+ * flag, preparing for the next night.
*/
public void clearNightResults() {
this.lastNightVictim = null;
@@ -229,23 +283,74 @@ public void clearNightResults() {
this.lastVictimResurrected = false;
}
+ /**
+ * Sets the sheriff.
+ *
+ * @param sheriff the new sheriff player
+ */
+ public void setSheriff(Player sheriff) {
+ // Remove sheriff status from previous sheriff
+ if (this.sheriff != null) {
+ this.sheriff.setSheriff(false);
+ }
+ this.sheriff = sheriff;
+ if (sheriff != null) {
+ sheriff.setSheriff(true);
+ }
+ }
+
+ /**
+ * Sets whether the speaking order is reversed.
+ *
+ * @param reversed true for reversed order; false for normal order
+ */
+ public void setSpeakOrderReversed(boolean reversed) {
+ this.speakOrderReversed = reversed;
+ }
+
+ /**
+ * Sets whether the sheriff was killed during the night.
+ *
+ * @param killed true if sheriff was killed at night; false otherwise
+ */
+ public void setSheriffKilledInNight(boolean killed) {
+ this.sheriffKilledInNight = killed;
+ }
+
// Winning condition checks
/**
- * Checks if werewolves meet the win condition.
- * Werewolves win if they are alive and their count is greater than or equal to
- * the number of alive villager-camp players.
+ * Checks if werewolves meet the win condition. Werewolves win if: 1. All villagers (ordinary
+ * villagers) are dead, OR 2. All god roles (seer, witch, hunter) are dead
*
* @return true if werewolves win; false otherwise
*/
public boolean checkWerewolvesWin() {
int aliveWerewolves = getAliveWerewolves().size();
- int aliveVillagers = getAliveVillagers().size();
- return aliveWerewolves > 0 && aliveWerewolves >= aliveVillagers;
+ if (aliveWerewolves == 0) {
+ return false;
+ }
+
+ // Check if all ordinary villagers are dead
+ boolean allVillagersDead =
+ getAlivePlayers().stream().noneMatch(p -> p.getRole() == Role.VILLAGER);
+
+ // Check if all god roles are dead
+ boolean allGodsDead =
+ getAlivePlayers().stream()
+ .filter(
+ p ->
+ p.getRole() == Role.SEER
+ || p.getRole() == Role.WITCH
+ || p.getRole() == Role.HUNTER)
+ .count()
+ == 0;
+
+ return allVillagersDead || allGodsDead;
}
/**
- * Checks if villagers meet the win condition.
- * Villagers win when all werewolves have been eliminated.
+ * Checks if villagers meet the win condition. Villagers win when all werewolves have been
+ * eliminated.
*
* @return true if villagers win; false otherwise
*/
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/entity/Player.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/entity/Player.java
index bc9d7a56d..458763f19 100644
--- a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/entity/Player.java
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/entity/Player.java
@@ -17,9 +17,7 @@
import io.agentscope.core.agent.AgentBase;
-/**
- * Represents a player in the Werewolf game.
- */
+/** Represents a player in the Werewolf game. */
public class Player {
private final AgentBase agent;
private final String name;
@@ -31,6 +29,11 @@ public class Player {
private boolean witchHasHealPotion;
private boolean witchHasPoisonPotion;
+ // Sheriff election related state
+ private boolean registeredForSheriff; // 是否上警竞选警长
+ private boolean isSheriff; // 是否是警长
+ private String nextCheckTarget; // 预言家下一个要验证的玩家(用于警徽传递信息)
+
private Player(Builder builder) {
this.agent = builder.agent;
this.name = builder.name;
@@ -43,6 +46,9 @@ private Player(Builder builder) {
this.witchHasHealPotion = true;
this.witchHasPoisonPotion = true;
}
+ this.registeredForSheriff = false;
+ this.isSheriff = false;
+ this.nextCheckTarget = null;
}
public static Builder builder() {
@@ -78,6 +84,18 @@ public boolean isWitchHasPoisonPotion() {
return witchHasPoisonPotion;
}
+ public boolean isRegisteredForSheriff() {
+ return registeredForSheriff;
+ }
+
+ public boolean isSheriff() {
+ return isSheriff;
+ }
+
+ public String getNextCheckTarget() {
+ return nextCheckTarget;
+ }
+
// State modifiers
public void kill() {
this.isAlive = false;
@@ -95,6 +113,18 @@ public void usePoisonPotion() {
this.witchHasPoisonPotion = false;
}
+ public void registerForSheriff() {
+ this.registeredForSheriff = true;
+ }
+
+ public void setSheriff(boolean sheriff) {
+ this.isSheriff = sheriff;
+ }
+
+ public void setNextCheckTarget(String targetName) {
+ this.nextCheckTarget = targetName;
+ }
+
@Override
public String toString() {
String status = isAlive ? "alive" : "dead";
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/localization/GameMessages.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/localization/GameMessages.java
index 0cd3bdd18..edb068013 100644
--- a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/localization/GameMessages.java
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/localization/GameMessages.java
@@ -20,8 +20,8 @@
/**
* Interface for providing UI messages in different languages.
*
- *
This interface defines all user-facing messages displayed during the game,
- * including titles, status messages, and announcements.
+ *
This interface defines all prompts that will be sent to agents during the game,
- * including system prompts for role initialization and various phase-specific prompts.
+ *
This interface defines all prompts that will be sent to agents during the game, including
+ * system prompts for role initialization and various phase-specific prompts.
*/
public interface PromptProvider {
- String getSystemPrompt(Role role, String playerName);
+ String getSystemPrompt(Role role, String[] args, String partner);
- Msg createWerewolfDiscussionPrompt(GameState state);
+ Msg createWerewolfDiscussionPrompt(GameState state, Integer round);
Msg createWerewolfVotingPrompt(GameState state);
- Msg createWitchHealPrompt(Player victim);
+ Msg createWitchHealPrompt(Player victim, GameState state);
Msg createWitchPoisonPrompt(GameState state, boolean usedHeal);
@@ -44,9 +45,24 @@ public interface PromptProvider {
String createNightResultAnnouncement(GameState state);
- Msg createDiscussionPrompt(GameState state, int round);
+ Msg createDiscussionPrompt(GameState state, String discussionOrders);
Msg createVotingPrompt(GameState state);
Msg createHunterShootPrompt(GameState state, Player hunter);
+
+ Msg createSheriffElectionStartPrompt(GameState state, List candidates);
+
+ // Sheriff election related prompts
+ Msg createSheriffRegistrationPrompt(GameState state);
+
+ Msg createSheriffCampaignPrompt(GameState state, Player candidate);
+
+ Msg createSheriffVotingPrompt(GameState state, List candidates);
+
+ Msg createSpeakOrderPrompt(Player sheriff);
+
+ Msg createSpeakOrderFromPositionPrompt(GameState state, Player newSheriff);
+
+ Msg createSheriffTransferPrompt(GameState state, Player sheriff);
}
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SeerCheckModel.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SeerCheckModel.java
index c5a1a65da..a0dbea578 100644
--- a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SeerCheckModel.java
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SeerCheckModel.java
@@ -15,11 +15,14 @@
*/
package io.agentscope.examples.werewolf.model;
-/**
- * Structured output model for seer's identity check.
- */
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/** Structured output model for seer's identity check. */
public class SeerCheckModel {
+ @JsonProperty("查验的目标")
public String targetPlayer;
+
+ @JsonProperty("查验目标的原因")
public String reason;
public SeerCheckModel() {}
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SheriffCampaignModel.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SheriffCampaignModel.java
new file mode 100644
index 000000000..094a17cba
--- /dev/null
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SheriffCampaignModel.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.examples.werewolf.model;
+
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+/**
+ * Model for sheriff campaign speech.
+ */
+public class SheriffCampaignModel {
+ @JsonPropertyDescription("昨夜查验的结果,比如xx号金水,xx号查杀。注意:如果不是预言家,或者不准备跳预言家,则严格留空")
+ public String checkResult; // Seer's check result to announce (if seer)
+
+ @JsonPropertyDescription("今晚查验的目标。注意:如果不是预言家,或者不准备跳预言家,则严格留空")
+ public String nextCheckTarget; // Next player to check tonight (if seer)
+
+ @JsonPropertyDescription("警上发言内容,不能为空")
+ public String campaignSpeech; // Campaign speech content
+}
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SheriffRegistrationModel.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SheriffRegistrationModel.java
new file mode 100644
index 000000000..c9b389a87
--- /dev/null
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SheriffRegistrationModel.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.examples.werewolf.model;
+
+/**
+ * Model for sheriff registration decision.
+ */
+public class SheriffRegistrationModel {
+ public Boolean registerForSheriff; // Whether to register for sheriff election
+ public String reason; // Reason for registering or not
+}
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SheriffTransferModel.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SheriffTransferModel.java
new file mode 100644
index 000000000..386caf534
--- /dev/null
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SheriffTransferModel.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.examples.werewolf.model;
+
+/**
+ * Model for sheriff badge transfer decision.
+ */
+public class SheriffTransferModel {
+ public String targetPlayer; // The player to transfer sheriff badge to (null to skip)
+ public String checkInfo; // Information to reveal (for seer sheriff)
+ public String reason; // Reason for transferring to this player
+}
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SheriffVoteModel.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SheriffVoteModel.java
new file mode 100644
index 000000000..fa5c7d211
--- /dev/null
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SheriffVoteModel.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.examples.werewolf.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Model for voting for sheriff candidate.
+ */
+public class SheriffVoteModel {
+ @JsonProperty("警徽票投向的玩家名")
+ public String targetPlayer; //
+
+ @JsonProperty("投票理由")
+ public String reason; // Reason for voting
+}
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SpeakOrderModel.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SpeakOrderModel.java
new file mode 100644
index 000000000..7727d44bc
--- /dev/null
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/SpeakOrderModel.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.examples.werewolf.model;
+
+/**
+ * Model for sheriff's decision on speaking order.
+ */
+public class SpeakOrderModel {
+ public Boolean normalOrder; // true for normal order (from sheriff), false for reversed order
+ public String reason; // Reason for choosing this order
+}
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/VoteModel.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/VoteModel.java
index c655ba687..96e4ee319 100644
--- a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/VoteModel.java
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/model/VoteModel.java
@@ -15,12 +15,18 @@
*/
package io.agentscope.examples.werewolf.model;
-/**
- * Structured output model for voting phase.
- */
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+
+/** Structured output model for voting phase. */
public class VoteModel {
+ @JsonPropertyDescription("目标玩家")
public String targetPlayer;
+
+ @JsonPropertyDescription("投票理由")
public String reason;
+ @JsonPropertyDescription("悍跳狼人候选人姓名(仅第一夜使用)")
+ public String jumpCandidateName; // 悍跳狼人候选人姓名(仅第一夜使用)
+
public VoteModel() {}
}
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/GameEvent.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/GameEvent.java
index e785ce523..be5538645 100644
--- a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/GameEvent.java
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/GameEvent.java
@@ -17,11 +17,10 @@
import java.time.Instant;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
-/**
- * Represents a game event to be sent to the web frontend.
- */
+/** Represents a game event to be sent to the web frontend. */
public class GameEvent {
private final GameEventType type;
@@ -115,8 +114,7 @@ public static GameEvent error(String message) {
}
/**
- * Create a player role assignment event.
- * This tells the human player their assigned role.
+ * Create a player role assignment event. This tells the human player their assigned role.
*
* @param playerName The human player's name
* @param role The assigned role (e.g., "WEREWOLF", "SEER")
@@ -135,8 +133,7 @@ public static GameEvent playerRoleAssignment(
}
/**
- * Create a wait user input event.
- * This prompts the human player to provide input.
+ * Create a wait user input event. This prompts the human player to provide input.
*
* @param inputType The type of input required (e.g., "SPEAK", "VOTE", "WITCH_HEAL")
* @param prompt The prompt message to display
@@ -167,6 +164,155 @@ public static GameEvent userInputReceived(String inputType, String content) {
Map.of("inputType", inputType, "content", content));
}
+ /**
+ * Create a sheriff registration event.
+ *
+ * @param playerName player name
+ * @param registered whether player registered for sheriff
+ * @param reason reason for decision
+ * @return The event
+ */
+ public static GameEvent sheriffRegistration(
+ String playerName, boolean registered, String reason) {
+ Map data = new HashMap<>();
+ data.put("playerName", playerName);
+ data.put("registered", registered);
+ data.put("reason", reason != null ? reason : "");
+ return new GameEvent(GameEventType.SHERIFF_REGISTRATION, data);
+ }
+
+ /**
+ * Create a sheriff candidates announced event.
+ *
+ * @param candidateNames list of candidate names
+ * @return The event
+ */
+ public static GameEvent sheriffCandidatesAnnounced(List candidateNames) {
+ Map data = new HashMap<>();
+ data.put("candidates", candidateNames);
+ return new GameEvent(GameEventType.SHERIFF_CANDIDATES_ANNOUNCED, data);
+ }
+
+ /**
+ * Create a sheriff campaign speech event.
+ *
+ * @param playerName candidate name
+ * @param speech campaign speech
+ * @param checkResult seer's check result (if seer)
+ * @param nextCheckTarget next target to check (if seer)
+ * @return The event
+ */
+ public static GameEvent sheriffCampaign(
+ String playerName, String speech, String checkResult, String nextCheckTarget) {
+ Map data = new HashMap<>();
+ data.put("playerName", playerName);
+ data.put("speech", speech != null ? speech : "");
+ data.put("checkResult", checkResult);
+ data.put("nextCheckTarget", nextCheckTarget);
+ return new GameEvent(GameEventType.SHERIFF_CAMPAIGN, data);
+ }
+
+ /**
+ * Create a sheriff vote event.
+ *
+ * @param voter voter name
+ * @param target target candidate name
+ * @param reason reason for voting
+ * @return The event
+ */
+ public static GameEvent sheriffVote(String voter, String target, String reason) {
+ Map data = new HashMap<>();
+ data.put("voter", voter);
+ data.put("target", target != null ? target : "");
+ data.put("reason", reason != null ? reason : "");
+ return new GameEvent(GameEventType.SHERIFF_VOTE, data);
+ }
+
+ /**
+ * Create a sheriff elected event.
+ *
+ * @param sheriffName elected sheriff name (null if badge is lost)
+ * @param voteCount number of votes received
+ * @return The event
+ */
+ public static GameEvent sheriffElected(
+ String sheriffName, int voteCount, Map voteDetails) {
+ Map data = new HashMap<>();
+ data.put("sheriffName", sheriffName);
+ data.put("voteCount", voteCount);
+ data.put("voteDetails", voteDetails);
+ return new GameEvent(GameEventType.SHERIFF_ELECTED, data);
+ }
+
+ /**
+ * Create a sheriff badge transfer event.
+ *
+ * @param fromPlayer previous sheriff name
+ * @param toPlayer new sheriff name (null if no transfer)
+ * @param checkInfo information revealed (for seer)
+ * @param reason reason for transfer
+ * @return The event
+ */
+ public static GameEvent sheriffTransfer(
+ String fromPlayer, String toPlayer, String checkInfo, String reason) {
+ Map data = new HashMap<>();
+ data.put("fromPlayer", fromPlayer);
+ data.put("toPlayer", toPlayer);
+ data.put("checkInfo", checkInfo);
+ data.put("reason", reason != null ? reason : "");
+ return new GameEvent(GameEventType.SHERIFF_TRANSFER, data);
+ }
+
+ /**
+ * Create a werewolf kill result popup event.
+ *
+ * @param victimName the player killed by werewolves
+ * @return The event
+ */
+ public static GameEvent nightActionWerewolfKill(String victimName) {
+ Map data = new HashMap<>();
+ data.put("victimName", victimName);
+ return new GameEvent(GameEventType.NIGHT_ACTION_WEREWOLF_KILL, data);
+ }
+
+ /**
+ * Create a witch heal result popup event.
+ *
+ * @param victimName the player healed by witch
+ * @return The event
+ */
+ public static GameEvent nightActionWitchHeal(String victimName) {
+ Map data = new HashMap<>();
+ data.put("victimName", victimName);
+ return new GameEvent(GameEventType.NIGHT_ACTION_WITCH_HEAL, data);
+ }
+
+ /**
+ * Create a witch poison result popup event.
+ *
+ * @param targetName the player poisoned by witch
+ * @return The event
+ */
+ public static GameEvent nightActionWitchPoison(String targetName) {
+ Map data = new HashMap<>();
+ data.put("targetName", targetName);
+ return new GameEvent(GameEventType.NIGHT_ACTION_WITCH_POISON, data);
+ }
+
+ /**
+ * Create a seer check result popup event.
+ *
+ * @param targetName the player checked by seer
+ * @param isWerewolf true if the checked player is a werewolf
+ * @return The event
+ */
+ public static GameEvent nightActionSeerCheck(String targetName, boolean isWerewolf) {
+ Map data = new HashMap<>();
+ data.put("targetName", targetName);
+ data.put("isWerewolf", isWerewolf);
+ return new GameEvent(GameEventType.NIGHT_ACTION_SEER_CHECK, data);
+ }
+
/**
* Create an audio chunk event for TTS.
*
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/GameEventEmitter.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/GameEventEmitter.java
index 9fc7d455c..d25cac5a0 100644
--- a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/GameEventEmitter.java
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/GameEventEmitter.java
@@ -19,17 +19,19 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Sinks;
/**
* Emitter for game events using Reactor Sinks.
*
- *
This class provides a reactive stream of game events that can be subscribed to by web
- * clients. It supports role-based visibility filtering:
+ *
This class provides a reactive stream of game events that can be subscribed to by web clients.
+ * It supports role-based visibility filtering:
+ *
*
- *
Player stream: Events visible to the human player based on their role
- *
God view history: Complete event history for replay after game ends
+ *
Player stream: Events visible to the human player based on their role
+ *
God view history: Complete event history for replay after game ends
*
*/
public class GameEventEmitter {
@@ -150,8 +152,8 @@ private void printEventToConsole(GameEvent event) {
}
/**
- * Emit a game initialization event.
- * Shows the human player their own role and teammates (if werewolf).
+ * Emit a game initialization event. Shows the human player their own role and teammates (if
+ * werewolf).
*
* @param allPlayers player information (with roles for god view)
* @param visiblePlayers player information visible to the human player
@@ -188,8 +190,7 @@ public void emitPlayerRoleAssignment(
}
/**
- * Emit a phase change event.
- * This is always public.
+ * Emit a phase change event. This is always public.
*
* @param round current round number
* @param phase phase name (night/day)
@@ -199,8 +200,8 @@ public void emitPhaseChange(int round, String phase) {
}
/**
- * Emit a player speak event.
- * Day discussions are public, werewolf discussions are visible only to werewolves.
+ * Emit a player speak event. Day discussions are public, werewolf discussions are visible only
+ * to werewolves.
*
* @param playerName player name
* @param content speech content
@@ -215,8 +216,8 @@ public void emitPlayerSpeak(String playerName, String content, String context) {
}
/**
- * Emit a player vote event.
- * God view shows full details including reason, player view hides reason.
+ * Emit a player vote event. God view shows full details including reason, player view hides
+ * reason.
*
* @param voterName voter name
* @param targetName vote target
@@ -236,8 +237,8 @@ public void emitPlayerVote(
}
/**
- * Emit a player action event (for special roles).
- * Visibility is determined by the role performing the action.
+ * Emit a player action event (for special roles). Visibility is determined by the role
+ * performing the action.
*
* @param playerName player name
* @param role role name
@@ -257,8 +258,8 @@ public void emitPlayerAction(
}
/**
- * Emit a player eliminated event.
- * Eliminations are public but role and cause are hidden in player view.
+ * Emit a player eliminated event. Eliminations are public but role and cause are hidden in
+ * player view.
*
* @param playerName eliminated player name
* @param role player's role (shown in god view only)
@@ -274,8 +275,7 @@ public void emitPlayerEliminated(String playerName, String role, String cause) {
}
/**
- * Emit a player resurrected event.
- * Only visible to the witch.
+ * Emit a player resurrected event. Only visible to the witch.
*
* @param playerName resurrected player name
*/
@@ -284,8 +284,7 @@ public void emitPlayerResurrected(String playerName) {
}
/**
- * Emit a stats update event.
- * Stats are always public.
+ * Emit a stats update event. Stats are always public.
*
* @param alive total alive players
* @param werewolves alive werewolves
@@ -296,8 +295,7 @@ public void emitStatsUpdate(int alive, int werewolves, int villagers) {
}
/**
- * Emit a system message event.
- * Default is public.
+ * Emit a system message event. Default is public.
*
* @param message system message
*/
@@ -316,8 +314,7 @@ public void emitSystemMessage(String message, EventVisibility visibility) {
}
/**
- * Emit a game end event.
- * Game end is always public.
+ * Emit a game end event. Game end is always public.
*
* @param winner winning side (villagers/werewolves)
* @param reason win reason
@@ -327,8 +324,7 @@ public void emitGameEnd(String winner, String reason) {
}
/**
- * Emit an error event.
- * Errors are always public.
+ * Emit an error event. Errors are always public.
*
* @param message error message
*/
@@ -364,21 +360,8 @@ public void emitUserInputReceived(String inputType, String content) {
}
/**
- * Emit an audio chunk for TTS.
- * Audio is always public (everyone can hear day discussion).
- *
- * @param playerName The name of the player speaking
- * @param audioBase64 Base64 encoded audio data
- */
- public void emitAudioChunk(String playerName, String audioBase64) {
- GameEvent event = GameEvent.audioChunk(playerName, audioBase64);
- godViewHistory.add(event);
- playerSink.tryEmitNext(event);
- }
-
- /**
- * Get the player event stream as a Flux.
- * This stream contains events visible to the human player based on their role.
+ * Get the player event stream as a Flux. This stream contains events visible to the human
+ * player based on their role.
*
* @return Flux of game events for the player
*/
@@ -387,8 +370,8 @@ public Flux getPlayerStream() {
}
/**
- * Get the complete event history (god view).
- * This includes all events for replay after game ends.
+ * Get the complete event history (god view). This includes all events for replay after game
+ * ends.
*
* @return unmodifiable list of all game events
*/
@@ -397,8 +380,127 @@ public List getGodViewHistory() {
}
/**
- * Complete the event stream.
+ * Emit sheriff registration event.
+ *
+ * @param playerName player name
+ * @param registered whether player registered for sheriff
+ * @param reason reason for decision
+ */
+ public void emitSheriffRegistration(String playerName, boolean registered, String reason) {
+ emit(GameEvent.sheriffRegistration(playerName, registered, reason), EventVisibility.PUBLIC);
+ }
+
+ /**
+ * Emit sheriff candidates announced event.
+ *
+ * @param candidateNames list of candidate names
+ */
+ public void emitSheriffCandidatesAnnounced(List candidateNames) {
+ emit(GameEvent.sheriffCandidatesAnnounced(candidateNames), EventVisibility.PUBLIC);
+ }
+
+ /**
+ * Emit sheriff campaign speech event.
+ *
+ * @param playerName candidate name
+ * @param speech campaign speech
+ * @param checkResult seer's check result (if seer)
+ * @param nextCheckTarget next target to check (if seer)
+ */
+ public void emitSheriffCampaign(
+ String playerName, String speech, String checkResult, String nextCheckTarget) {
+ emit(
+ GameEvent.sheriffCampaign(playerName, speech, checkResult, nextCheckTarget),
+ EventVisibility.PUBLIC);
+ }
+
+ /**
+ * Emit sheriff vote event.
+ *
+ * @param voter voter name
+ * @param target target candidate name
+ * @param reason reason for voting
+ */
+ public void emitSheriffVote(String voter, String target, String reason) {
+ emit(GameEvent.sheriffVote(voter, target, reason), EventVisibility.PUBLIC);
+ }
+
+ /**
+ * Emit sheriff elected event.
+ *
+ * @param sheriffName elected sheriff name
+ * @param voteCount number of votes received
+ */
+ public void emitSheriffElected(
+ String sheriffName, int voteCount, Map voteDetails) {
+ emit(GameEvent.sheriffElected(sheriffName, voteCount, voteDetails), EventVisibility.PUBLIC);
+ }
+
+ /**
+ * Emit sheriff badge transfer event.
+ *
+ * @param fromPlayer previous sheriff name
+ * @param toPlayer new sheriff name (null if no transfer)
+ * @param checkInfo information revealed (for seer)
+ * @param reason reason for transfer
+ */
+ public void emitSheriffTransfer(
+ String fromPlayer, String toPlayer, String checkInfo, String reason) {
+ emit(
+ GameEvent.sheriffTransfer(fromPlayer, toPlayer, checkInfo, reason),
+ EventVisibility.PUBLIC);
+ }
+
+ /**
+ * Emit werewolf kill result popup event.
+ *
+ * @param victimName the player killed by werewolves
+ */
+ public void emitNightActionWerewolfKill(String victimName) {
+ emit(GameEvent.nightActionWerewolfKill(victimName), EventVisibility.WEREWOLF_ONLY);
+ }
+
+ /**
+ * Emit witch heal result popup event.
+ *
+ * @param victimName the player healed by witch
+ */
+ public void emitNightActionWitchHeal(String victimName) {
+ emit(GameEvent.nightActionWitchHeal(victimName), EventVisibility.WITCH_ONLY);
+ }
+
+ /**
+ * Emit witch poison result popup event.
+ *
+ * @param targetName the player poisoned by witch
+ */
+ public void emitNightActionWitchPoison(String targetName) {
+ emit(GameEvent.nightActionWitchPoison(targetName), EventVisibility.WITCH_ONLY);
+ }
+
+ /**
+ * Emit seer check result popup event.
+ *
+ * @param targetName the player checked by seer
+ * @param isWerewolf true if the checked player is a werewolf
+ */
+ public void emitNightActionSeerCheck(String targetName, boolean isWerewolf) {
+ emit(GameEvent.nightActionSeerCheck(targetName, isWerewolf), EventVisibility.SEER_ONLY);
+ }
+
+ /**
+ * Emit an audio chunk for TTS. Audio is always public (everyone can hear day discussion).
+ *
+ * @param playerName The name of the player speaking
+ * @param audioBase64 Base64 encoded audio data
*/
+ public void emitAudioChunk(String playerName, String audioBase64) {
+ GameEvent event = GameEvent.audioChunk(playerName, audioBase64);
+ godViewHistory.add(event);
+ playerSink.tryEmitNext(event);
+ }
+
+ /** Complete the event stream. */
public void complete() {
playerSink.tryEmitComplete();
}
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/GameEventType.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/GameEventType.java
index 02305009f..050b3b7ff 100644
--- a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/GameEventType.java
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/GameEventType.java
@@ -15,9 +15,7 @@
*/
package io.agentscope.examples.werewolf.web;
-/**
- * Event types for the Werewolf game web interface.
- */
+/** Event types for the Werewolf game web interface. */
public enum GameEventType {
/** Game initialization with player info. */
GAME_INIT,
@@ -61,6 +59,36 @@ public enum GameEventType {
/** User input received confirmation. */
USER_INPUT_RECEIVED,
+ /** Sheriff election: player registers for sheriff campaign. */
+ SHERIFF_REGISTRATION,
+
+ /** Sheriff election: candidates announcement with raise hand icon. */
+ SHERIFF_CANDIDATES_ANNOUNCED,
+
+ /** Sheriff election: candidate campaign speech. */
+ SHERIFF_CAMPAIGN,
+
+ /** Sheriff election: voting for sheriff. */
+ SHERIFF_VOTE,
+
+ /** Sheriff elected announcement. */
+ SHERIFF_ELECTED,
+
+ /** Sheriff transfers badge. */
+ SHERIFF_TRANSFER,
+
+ /** Night action result popup for werewolf kill. */
+ NIGHT_ACTION_WEREWOLF_KILL,
+
+ /** Night action result popup for witch heal. */
+ NIGHT_ACTION_WITCH_HEAL,
+
+ /** Night action result popup for witch poison. */
+ NIGHT_ACTION_WITCH_POISON,
+
+ /** Night action result popup for seer check. */
+ NIGHT_ACTION_SEER_CHECK,
+
/** Audio chunk for TTS (text-to-speech). */
AUDIO_CHUNK
}
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/WebUserInput.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/WebUserInput.java
index 97762652d..9dac2b6cf 100644
--- a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/WebUserInput.java
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/WebUserInput.java
@@ -28,8 +28,7 @@
* Web-based user input implementation for the Werewolf game.
*
*
This class bridges the gap between the UserAgent and the web frontend, allowing human players
- * to provide input through the browser interface. It works in conjunction with GameEventEmitter
- * to:
+ * to provide input through the browser interface. It works in conjunction with GameEventEmitter to:
*
*
*
Emit WAIT_USER_INPUT events to prompt the human player
@@ -49,6 +48,11 @@ public class WebUserInput implements UserInputBase {
public static final String INPUT_WITCH_POISON = "WITCH_POISON";
public static final String INPUT_SEER_CHECK = "SEER_CHECK";
public static final String INPUT_HUNTER_SHOOT = "HUNTER_SHOOT";
+ public static final String INPUT_SHERIFF_REGISTER = "SHERIFF_REGISTER";
+ public static final String INPUT_SHERIFF_CAMPAIGN = "SHERIFF_CAMPAIGN";
+ public static final String INPUT_SHERIFF_VOTE = "SHERIFF_VOTE";
+ public static final String INPUT_SPEAK_ORDER = "SPEAK_ORDER";
+ public static final String INPUT_SHERIFF_TRANSFER = "SHERIFF_TRANSFER";
/**
* Create a new WebUserInput.
@@ -166,9 +170,7 @@ public String getPendingInputType() {
return key.split("_")[0];
}
- /**
- * Cancel all pending inputs.
- */
+ /** Cancel all pending inputs. */
public void cancelAllPending() {
pendingInputs.values().forEach(sink -> sink.tryEmitValue(""));
pendingInputs.clear();
diff --git a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/WerewolfWebGame.java b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/WerewolfWebGame.java
index 4eaac57eb..9b143b153 100644
--- a/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/WerewolfWebGame.java
+++ b/agentscope-examples/werewolf-hitl/src/main/java/io/agentscope/examples/werewolf/web/WerewolfWebGame.java
@@ -15,7 +15,6 @@
*/
package io.agentscope.examples.werewolf.web;
-import static io.agentscope.examples.werewolf.WerewolfGameConfig.MAX_DISCUSSION_ROUNDS;
import static io.agentscope.examples.werewolf.WerewolfGameConfig.MAX_ROUNDS;
import io.agentscope.core.ReActAgent;
@@ -29,10 +28,10 @@
import io.agentscope.core.message.MsgRole;
import io.agentscope.core.message.TextBlock;
import io.agentscope.core.model.DashScopeChatModel;
-import io.agentscope.core.model.GenerateOptions;
import io.agentscope.core.model.StructuredOutputReminder;
import io.agentscope.core.model.tts.DashScopeRealtimeTTSModel;
import io.agentscope.core.model.tts.Qwen3TTSFlashVoice;
+import io.agentscope.core.pipeline.FanoutPipeline;
import io.agentscope.core.pipeline.MsgHub;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.examples.werewolf.GameConfiguration;
@@ -47,6 +46,11 @@
import io.agentscope.examples.werewolf.localization.PromptProvider;
import io.agentscope.examples.werewolf.model.HunterShootModel;
import io.agentscope.examples.werewolf.model.SeerCheckModel;
+import io.agentscope.examples.werewolf.model.SheriffCampaignModel;
+import io.agentscope.examples.werewolf.model.SheriffRegistrationModel;
+import io.agentscope.examples.werewolf.model.SheriffTransferModel;
+import io.agentscope.examples.werewolf.model.SheriffVoteModel;
+import io.agentscope.examples.werewolf.model.SpeakOrderModel;
import io.agentscope.examples.werewolf.model.VoteModel;
import io.agentscope.examples.werewolf.model.WitchHealModel;
import io.agentscope.examples.werewolf.model.WitchPoisonModel;
@@ -62,10 +66,11 @@
* Web-enabled Werewolf Game with event emission and human player support.
*
*
This is a modified version of WerewolfGame that:
+ *
*
- *
Emits events instead of printing to console for web interface display
- *
Supports one human player with role-based event visibility
- *
Allows human interaction via WebUserInput
+ *
Emits events instead of printing to console for web interface display
+ *
Supports one human player with role-based event visibility
+ *
Allows human interaction via WebUserInput
*
*/
public class WerewolfWebGame {
@@ -138,8 +143,6 @@ public void start() throws Exception {
model =
DashScopeChatModel.builder()
.apiKey(apiKey)
- .enableThinking(true)
- .defaultOptions(GenerateOptions.builder().thinkingBudget(512).build())
.modelName(WerewolfGameConfig.DEFAULT_MODEL)
.formatter(new DashScopeMultiAgentFormatter())
.stream(false)
@@ -152,7 +155,7 @@ public void start() throws Exception {
gameState.nextRound();
emitter.emitPhaseChange(round, "night");
- nightPhase();
+ nightPhase(round);
if (checkGameEnd()) {
break;
@@ -201,7 +204,12 @@ private GameState initializeGame() {
humanPlayerIndex = new Random().nextInt(roles.size());
}
}
-
+ List wolfRoleIndex = new ArrayList<>();
+ for (int i = 0; i < roles.size(); i++) {
+ if (roles.get(i) == Role.WEREWOLF) {
+ wolfRoleIndex.add(i);
+ }
+ }
List players = new ArrayList<>();
List playerNames = new ArrayList<>(langConfig.getPlayerNames());
int totalPlayers = gameConfig.getTotalPlayerCount();
@@ -209,6 +217,13 @@ private GameState initializeGame() {
for (int i = playerNames.size(); i < totalPlayers; i++) {
playerNames.add(String.valueOf(i + 1));
}
+ String allWolfNames =
+ String.join(
+ ",",
+ wolfRoleIndex.stream()
+ .map(index -> playerNames.get(index))
+ .collect(Collectors.toUnmodifiableList()));
+
for (int i = 0; i < roles.size(); i++) {
String name = playerNames.get(i);
Role role = roles.get(i);
@@ -221,10 +236,21 @@ private GameState initializeGame() {
agent = UserAgent.builder().name(name).inputMethod(userInput).build();
} else {
// Create AI agent for other players
+ List argList = new ArrayList<>();
+ // {0} = role name, {1} = player number, {2} = total players, {3} = villager count,
+ // {4} = werewolf count
+ argList.add(messages.getRoleDisplayName(role));
+ argList.add(name);
+ argList.add(String.valueOf(gameConfig.getTotalPlayerCount()));
+ argList.add(String.valueOf(gameConfig.getVillagerCount()));
+ argList.add(String.valueOf(gameConfig.getWerewolfCount()));
+ String partnerInfo = role == Role.WEREWOLF ? allWolfNames : "";
+ String systemPrompt =
+ prompts.getSystemPrompt(role, argList.toArray(new String[0]), partnerInfo);
agent =
ReActAgent.builder()
.name(name)
- .sysPrompt(prompts.getSystemPrompt(role, name))
+ .sysPrompt(systemPrompt)
.model(model)
.memory(new InMemoryMemory())
.toolkit(new Toolkit())
@@ -325,17 +351,23 @@ private GameState initializeGame() {
return new GameState(players);
}
- private void nightPhase() {
+ private void nightPhase(int round) {
emitter.emitSystemMessage(messages.getNightPhaseTitle());
gameState.clearNightResults();
- Player victim = werewolvesKill();
+ Player victim = werewolvesKill(round);
if (victim != null) {
gameState.setLastNightVictim(victim);
- victim.kill();
+ // Don't kill immediately - mark for death at end of night
// Werewolf kill decision is private (god view only for non-werewolves)
emitter.emitSystemMessage(
messages.getWerewolvesChose(victim.getName()), EventVisibility.WEREWOLF_ONLY);
+
+ // Sheriff badge transfer moved to day phase after death announcement
+ // Mark sheriff as killed for later transfer in day phase
+ if (victim.isSheriff()) {
+ gameState.setSheriffKilledInNight(true);
+ }
}
// Handle all witches
@@ -352,21 +384,28 @@ private void nightPhase() {
}
}
- // Emit player_eliminated events for night deaths at end of night phase
+ // Apply all night deaths at the end after all role actions
Player nightVictim = gameState.getLastNightVictim();
boolean wasResurrected = gameState.isLastVictimResurrected();
if (nightVictim != null && !wasResurrected) {
+ nightVictim.kill();
emitter.emitPlayerEliminated(
nightVictim.getName(),
messages.getRoleDisplayName(nightVictim.getRole()),
"killed");
}
+ Player poisonedVictim = gameState.getLastPoisonedVictim();
+ if (poisonedVictim != null && poisonedVictim.isAlive()) {
+ poisonedVictim.kill();
+ // Elimination event already emitted in witchActions
+ }
+
emitStatsUpdate();
emitter.emitSystemMessage(messages.getNightPhaseComplete());
}
- private Player werewolvesKill() {
+ private Player werewolvesKill(int round) {
List werewolves = gameState.getAliveWerewolves();
if (werewolves.isEmpty()) {
return null;
@@ -383,14 +422,14 @@ private Player werewolvesKill() {
.name("WerewolfDiscussion")
.participants(
werewolves.stream().map(Player::getAgent).toArray(AgentBase[]::new))
- .announcement(prompts.createWerewolfDiscussionPrompt(gameState))
+ .announcement(prompts.createWerewolfDiscussionPrompt(gameState, round))
.enableAutoBroadcast(true)
.build()) {
werewolfHub.enter().block();
// Werewolves discuss in rounds, human participates in each round
- for (int round = 0; round < 1; round++) {
+ for (int discussRound = 0; discussRound < 1; discussRound++) {
for (Player werewolf : werewolves) {
if (werewolf.isHuman()) {
// Human werewolf speaks
@@ -427,16 +466,29 @@ private Player werewolvesKill() {
}
}
- werewolfHub.setAutoBroadcast(false);
Msg votingPrompt = prompts.createWerewolfVotingPrompt(gameState);
// Collect votes - AI werewolves vote via model, human votes via input
List votes = new ArrayList<>();
- List voteTargetOptions =
- gameState.getAlivePlayers().stream()
- .filter(p -> p.getRole() != Role.WEREWOLF)
- .map(Player::getName)
- .collect(Collectors.toList());
+ List aiWerewolves = new ArrayList<>();
+
+ // First night: werewolves can kill anyone (including self-kill)
+ // Later nights: werewolves can only kill non-werewolves
+ List voteTargetOptions;
+ if (gameState.getCurrentRound() == 1) {
+ // First night: all alive players are valid targets (can self-kill)
+ voteTargetOptions =
+ gameState.getAlivePlayers().stream()
+ .map(Player::getName)
+ .collect(Collectors.toList());
+ } else {
+ // Later nights: only non-werewolves
+ voteTargetOptions =
+ gameState.getAlivePlayers().stream()
+ .filter(p -> p.getRole() != Role.WEREWOLF)
+ .map(Player::getName)
+ .collect(Collectors.toList());
+ }
for (Player werewolf : werewolves) {
if (werewolf.isHuman()) {
@@ -466,13 +518,31 @@ private Player werewolvesKill() {
.build();
votes.add(voteMsg);
} else {
- // AI werewolf votes
- Msg vote = werewolf.getAgent().call(votingPrompt, VoteModel.class).block();
+ // AI werewolf - add to parallel list
+ aiWerewolves.add(werewolf);
+ }
+ }
+
+ // Parallel voting for AI werewolves
+ if (!aiWerewolves.isEmpty()) {
+ List aiWerewolfAgents =
+ aiWerewolves.stream().map(Player::getAgent).collect(Collectors.toList());
+ List aiVotes =
+ new FanoutPipeline(aiWerewolfAgents)
+ .execute(votingPrompt, VoteModel.class)
+ .onErrorReturn(new ArrayList<>())
+ .block();
+ if (aiVotes == null) {
+ aiVotes = new ArrayList<>();
+ }
+
+ // Process results
+ for (Msg vote : aiVotes) {
votes.add(vote);
try {
VoteModel voteData = vote.getStructuredData(VoteModel.class);
emitter.emitPlayerVote(
- werewolf.getName(),
+ vote.getName(),
voteData.targetPlayer,
voteData.reason,
EventVisibility.WEREWOLF_ONLY);
@@ -483,10 +553,8 @@ private Player werewolvesKill() {
}
}
}
-
Player killedPlayer = utils.countVotes(votes, gameState);
-
- List broadcastMsgs = new ArrayList<>(votes);
+ List broadcastMsgs = new ArrayList<>();
broadcastMsgs.add(
Msg.builder()
.name("Moderator Message")
@@ -502,6 +570,11 @@ private Player werewolvesKill() {
.build());
werewolfHub.broadcast(broadcastMsgs).block();
+ // Emit popup event for werewolf kill result (visible to werewolves only)
+ if (killedPlayer != null) {
+ emitter.emitNightActionWerewolfKill(killedPlayer.getName());
+ }
+
return killedPlayer;
}
}
@@ -514,6 +587,7 @@ private void witchActions(Player witch) {
emitter.emitSystemMessage(messages.getSystemWitchActing(), EventVisibility.WITCH_ONLY);
// Inform witch about last night's victim (regardless of heal potion availability)
+ // 女巫始终能看到死亡信息,即使自己死亡
if (victim != null && witch.getAgent() instanceof ReActAgent) {
ReActAgent witchAgent = (ReActAgent) witch.getAgent();
if (witchAgent.getMemory() != null) {
@@ -529,8 +603,10 @@ private void witchActions(Player witch) {
}
boolean usedHeal = false;
+ boolean isWitchDead = victim != null && victim.getName().equals(witch.getName());
- if (witch.isWitchHasHealPotion() && victim != null) {
+ // 女巫有解药、有受害者、且受害者不是自己时才能使用解药
+ if (witch.isWitchHasHealPotion() && victim != null && !isWitchDead) {
emitter.emitSystemMessage(
messages.getSystemWitchSeesVictim(victim.getName()),
EventVisibility.WITCH_ONLY);
@@ -558,6 +634,8 @@ private void witchActions(Player witch) {
messages.getActionWitchHealResult(),
EventVisibility.WITCH_ONLY);
emitter.emitPlayerResurrected(victim.getName());
+ // Emit popup event for witch heal result
+ emitter.emitNightActionWitchHeal(victim.getName());
} else {
emitter.emitPlayerAction(
witch.getName(),
@@ -573,7 +651,7 @@ private void witchActions(Player witch) {
Msg healDecision =
witch.getAgent()
.call(
- prompts.createWitchHealPrompt(victim),
+ prompts.createWitchHealPrompt(victim, gameState),
WitchHealModel.class)
.block();
@@ -592,6 +670,8 @@ private void witchActions(Player witch) {
messages.getActionWitchHealResult(),
EventVisibility.WITCH_ONLY);
emitter.emitPlayerResurrected(victim.getName());
+ // Emit popup event for witch heal result
+ emitter.emitNightActionWitchHeal(victim.getName());
} else {
emitter.emitPlayerAction(
witch.getName(),
@@ -607,7 +687,7 @@ private void witchActions(Player witch) {
}
}
- if (witch.isWitchHasPoisonPotion()) {
+ if (witch.isWitchHasPoisonPotion() && !usedHeal) {
List poisonTargetOptions =
gameState.getAlivePlayers().stream()
.filter(p -> !p.getName().equals(witch.getName()))
@@ -630,7 +710,7 @@ private void witchActions(Player witch) {
&& !"skip".equalsIgnoreCase(poisonTarget)) {
Player targetPlayer = gameState.findPlayerByName(poisonTarget);
if (targetPlayer != null && targetPlayer.isAlive()) {
- targetPlayer.kill();
+ // Don't kill immediately - mark for death at end of night
witch.usePoisonPotion();
gameState.setLastPoisonedVictim(targetPlayer);
emitter.emitPlayerAction(
@@ -644,6 +724,8 @@ private void witchActions(Player witch) {
targetPlayer.getName(),
messages.getRoleDisplayName(targetPlayer.getRole()),
"poisoned");
+ // Emit popup event for witch poison result
+ emitter.emitNightActionWitchPoison(targetPlayer.getName());
}
} else {
emitter.emitPlayerAction(
@@ -671,7 +753,7 @@ private void witchActions(Player witch) {
&& poisonModel.targetPlayer != null) {
Player targetPlayer = gameState.findPlayerByName(poisonModel.targetPlayer);
if (targetPlayer != null && targetPlayer.isAlive()) {
- targetPlayer.kill();
+ // Don't kill immediately - mark for death at end of night
witch.usePoisonPotion();
gameState.setLastPoisonedVictim(targetPlayer);
emitter.emitPlayerAction(
@@ -685,6 +767,8 @@ private void witchActions(Player witch) {
targetPlayer.getName(),
messages.getRoleDisplayName(targetPlayer.getRole()),
"poisoned");
+ // Emit popup event for witch poison result
+ emitter.emitNightActionWitchPoison(targetPlayer.getName());
}
} else {
emitter.emitPlayerAction(
@@ -729,10 +813,9 @@ private void seerCheck(Player seer) {
if (targetName != null && !targetName.isEmpty()) {
Player target = gameState.findPlayerByName(targetName);
if (target != null && target.isAlive()) {
+ boolean isWerewolf = target.getRole() == Role.WEREWOLF;
String identity =
- target.getRole() == Role.WEREWOLF
- ? messages.getIsWerewolf()
- : messages.getNotWerewolf();
+ isWerewolf ? messages.getIsWerewolf() : messages.getNotWerewolf();
emitter.emitPlayerAction(
seer.getName(),
messages.getRoleDisplayName(Role.SEER),
@@ -740,6 +823,8 @@ private void seerCheck(Player seer) {
target.getName(),
target.getName() + " " + identity,
EventVisibility.SEER_ONLY);
+ // Emit popup event for seer check result
+ emitter.emitNightActionSeerCheck(target.getName(), isWerewolf);
}
}
} else {
@@ -757,10 +842,9 @@ private void seerCheck(Player seer) {
if (checkModel.targetPlayer != null) {
Player target = gameState.findPlayerByName(checkModel.targetPlayer);
if (target != null && target.isAlive()) {
+ boolean isWerewolf = target.getRole() == Role.WEREWOLF;
String identity =
- target.getRole() == Role.WEREWOLF
- ? messages.getIsWerewolf()
- : messages.getNotWerewolf();
+ isWerewolf ? messages.getIsWerewolf() : messages.getNotWerewolf();
emitter.emitPlayerAction(
seer.getName(),
messages.getRoleDisplayName(Role.SEER),
@@ -768,7 +852,11 @@ private void seerCheck(Player seer) {
target.getName(),
target.getName() + " " + identity,
EventVisibility.SEER_ONLY);
- seer.getAgent().call(prompts.createSeerResultPrompt(target)).block();
+ // Emit popup event for seer check result
+ emitter.emitNightActionSeerCheck(target.getName(), isWerewolf);
+ ((ReActAgent) seer.getAgent())
+ .getMemory()
+ .addMessage(prompts.createSeerResultPrompt(target));
}
}
} catch (Exception e) {
@@ -779,16 +867,28 @@ private void seerCheck(Player seer) {
private void dayPhase() {
emitter.emitSystemMessage(messages.getDayPhaseTitle());
-
String nightAnnouncement = prompts.createNightResultAnnouncement(gameState);
emitter.emitSystemMessage(nightAnnouncement);
+ // Broadcast night result to all agents
+ Msg nightResultMsg =
+ Msg.builder()
+ .name("Moderator Message")
+ .role(MsgRole.USER)
+ .content(TextBlock.builder().text(nightAnnouncement).build())
+ .build();
+
+ // Send night result to all alive agents
+ for (Player player : gameState.getAlivePlayers()) {
+ if (!player.isHuman() && player.getAgent() instanceof ReActAgent) {
+ ((ReActAgent) player.getAgent()).getMemory().addMessage(nightResultMsg);
+ }
+ }
// Handle all hunters who were eliminated
for (Player hunter : gameState.getHunters()) {
if (!hunter.isAlive()
&& (hunter.equals(gameState.getLastNightVictim())
|| hunter.equals(gameState.getLastPoisonedVictim()))) {
- // Night death: no need to broadcast, death will be announced in night result
hunterShoot(hunter, false);
if (checkGameEnd()) {
return;
@@ -796,8 +896,24 @@ private void dayPhase() {
}
}
- discussionPhase();
+ // Handle sheriff badge transfer if sheriff was killed at night
+ // This happens after death announcement in day phase
+ if (gameState.isSheriffKilledInNight()) {
+ Player killedSheriff = gameState.getSheriff();
+ if (killedSheriff != null && !killedSheriff.isAlive()) {
+ sheriffTransferBadge(killedSheriff);
+ // Reset the flag after transfer
+ gameState.setSheriffKilledInNight(false);
+ }
+ }
+ // Sheriff election only on first day
+ if (gameState.getCurrentRound() == 1) {
+ // Sheriff election
+ emitter.emitSystemMessage(messages.getSystemSheriffElectionStart());
+ sheriffElectionPhase();
+ }
+ discussionPhase();
Player votedOut = votingPhase();
if (votedOut != null) {
@@ -805,6 +921,11 @@ private void dayPhase() {
String roleName = messages.getRoleDisplayName(votedOut.getRole());
emitter.emitPlayerEliminated(votedOut.getName(), roleName, "voted");
+ // Handle sheriff badge transfer if sheriff is voted out
+ if (votedOut.isSheriff()) {
+ sheriffTransferBadge(votedOut);
+ }
+
if (votedOut.getRole() == Role.HUNTER) {
// Day vote out: reveal hunter identity and broadcast to all agents
hunterShoot(votedOut, true);
@@ -820,6 +941,40 @@ private void discussionPhase() {
return;
}
+ // Get sheriff for speak order determination
+ Player sheriff = gameState.getSheriff();
+
+ // Apply speak order: start from next player after sheriff, sheriff speaks last
+ if (sheriff != null && alivePlayers.contains(sheriff)) {
+ // Reorder list: start from player after sheriff
+ List reordered = new ArrayList<>();
+ int sheriffIndex = alivePlayers.indexOf(sheriff);
+
+ // Determine direction based on speak order
+ boolean reversed = gameState.isSpeakOrderReversed();
+
+ if (!reversed) {
+ // Normal order: start from player after sheriff
+ for (int i = sheriffIndex + 1; i < alivePlayers.size(); i++) {
+ reordered.add(alivePlayers.get(i));
+ }
+ for (int i = 0; i < sheriffIndex; i++) {
+ reordered.add(alivePlayers.get(i));
+ }
+ } else {
+ // Reversed order: start from player before sheriff
+ for (int i = sheriffIndex - 1; i >= 0; i--) {
+ reordered.add(alivePlayers.get(i));
+ }
+ for (int i = alivePlayers.size() - 1; i > sheriffIndex; i--) {
+ reordered.add(alivePlayers.get(i));
+ }
+ }
+ // Sheriff always speaks last
+ reordered.add(sheriff);
+ alivePlayers = reordered;
+ }
+
emitter.emitSystemMessage(messages.getSystemDayDiscussionStart());
try (MsgHub discussionHub =
@@ -830,69 +985,50 @@ private void discussionPhase() {
.map(Player::getAgent)
.toArray(AgentBase[]::new))
.announcement(
- Msg.builder()
- .name("Moderator Message")
- .role(MsgRole.USER)
- .content(
- TextBlock.builder()
- .text(
- prompts
- .createNightResultAnnouncement(
- gameState))
- .build())
- .build())
+ prompts.createDiscussionPrompt(
+ gameState,
+ alivePlayers.stream()
+ .map(Player::getName)
+ .collect(Collectors.joining("->"))))
.enableAutoBroadcast(true)
.build()) {
discussionHub.enter().block();
-
- for (int round = 1; round <= MAX_DISCUSSION_ROUNDS; round++) {
- emitter.emitSystemMessage(messages.getDiscussionRound(round));
-
- if (round > 1) {
- Msg roundPrompt = prompts.createDiscussionPrompt(gameState, round);
- for (Player player : alivePlayers) {
- if (!player.isHuman() && player.getAgent() instanceof ReActAgent) {
- ((ReActAgent) player.getAgent()).getMemory().addMessage(roundPrompt);
- }
- }
- }
-
- for (Player player : alivePlayers) {
- if (player.isHuman()) {
- // Human player speaks
- String humanInput =
- userInput
- .waitForInput(
- WebUserInput.INPUT_SPEAK,
- messages.getPromptDayDiscussion(),
- null)
- .block();
- if (humanInput != null && !humanInput.isEmpty()) {
- emitter.emitPlayerSpeak(player.getName(), humanInput, "day_discussion");
- // Broadcast to other players
- discussionHub
- .broadcast(
- List.of(
- Msg.builder()
- .name(player.getName())
- .role(MsgRole.USER)
- .content(
- TextBlock.builder()
- .text(humanInput)
- .build())
- .build()))
+ emitter.emitSystemMessage(messages.getDiscussionRound(1));
+ for (Player player : alivePlayers) {
+ if (player.isHuman()) {
+ // Human player speaks
+ String humanInput =
+ userInput
+ .waitForInput(
+ WebUserInput.INPUT_SPEAK,
+ messages.getPromptDayDiscussion(),
+ null)
.block();
- }
- } else {
- // AI player speaks
- Msg response = player.getAgent().call().block();
- String content = utils.extractTextContent(response);
- emitter.emitPlayerSpeak(player.getName(), content, "day_discussion");
-
- // Generate TTS for AI speech (only during day discussion)
- generateTTSForSpeech(player.getName(), content);
+ if (humanInput != null && !humanInput.isEmpty()) {
+ emitter.emitPlayerSpeak(player.getName(), humanInput, "day_discussion");
+ // Broadcast to other players
+ discussionHub
+ .broadcast(
+ List.of(
+ Msg.builder()
+ .name(player.getName())
+ .role(MsgRole.USER)
+ .content(
+ TextBlock.builder()
+ .text(humanInput)
+ .build())
+ .build()))
+ .block();
}
+ } else {
+ // AI player speaks
+ Msg response = player.getAgent().call().block();
+ String content = utils.extractTextContent(response);
+ emitter.emitPlayerSpeak(player.getName(), content, "day_discussion");
+
+ // Generate TTS for AI speech (only during day discussion)
+ generateTTSForSpeech(player.getName(), content);
}
}
}
@@ -923,6 +1059,7 @@ private Player votingPhase() {
// Collect votes
List votes = new ArrayList<>();
+ List aiPlayers = new ArrayList<>();
List voteTargetOptions =
alivePlayers.stream().map(Player::getName).collect(Collectors.toList());
@@ -960,13 +1097,31 @@ private Player votingPhase() {
.build();
votes.add(voteMsg);
} else {
- // AI player votes
- Msg vote = player.getAgent().call(votingPrompt, VoteModel.class).block();
+ // AI player votes - parallel execution
+ aiPlayers.add(player);
+ }
+ }
+
+ // Parallel voting for AI players
+ if (!aiPlayers.isEmpty()) {
+ List aiPlayerAgents =
+ aiPlayers.stream().map(Player::getAgent).collect(Collectors.toList());
+ List aiVotes =
+ new FanoutPipeline(aiPlayerAgents)
+ .execute(votingPrompt, VoteModel.class)
+ .onErrorReturn(new ArrayList<>())
+ .block();
+ if (aiVotes == null) {
+ aiVotes = new ArrayList<>();
+ }
+
+ // Process results
+ for (Msg vote : aiVotes) {
votes.add(vote);
try {
VoteModel voteData = vote.getStructuredData(VoteModel.class);
emitter.emitPlayerVote(
- player.getName(),
+ vote.getName(),
voteData.targetPlayer,
voteData.reason,
EventVisibility.PUBLIC);
@@ -978,34 +1133,696 @@ private Player votingPhase() {
Player votedOut = utils.countVotes(votes, gameState);
- List broadcastMsgs = new ArrayList<>(votes);
- broadcastMsgs.add(
+ // Build detailed voting result with each player's vote
+ StringBuilder detailedResult = new StringBuilder();
+ detailedResult.append("【投票放逐结果】\n");
+ Map> votesByTarget = new HashMap<>();
+ for (Msg vote : votes) {
+ try {
+ VoteModel voteData = vote.getStructuredData(VoteModel.class);
+ votesByTarget
+ .computeIfAbsent(voteData.targetPlayer, k -> new ArrayList<>())
+ .add(vote.getName());
+ } catch (Exception e) {
+ // Skip invalid votes
+ }
+ }
+ for (Map.Entry> entry : votesByTarget.entrySet()) {
+ String target = entry.getKey();
+ List voters = entry.getValue();
+ detailedResult.append(String.format("%s ⬅ %s\n", target, String.join("、", voters)));
+ }
+ if (votedOut == null) {
+ detailedResult.append("(无人被放逐)");
+ } else {
+ detailedResult.append(String.format("被放逐玩家:%s", votedOut.getName()));
+ }
+
+ // Build the voting result message to broadcast to all participants
+ Msg votedOutMsg =
Msg.builder()
.name("Moderator Message")
.role(MsgRole.USER)
- .content(
- TextBlock.builder()
- .text(
- messages.getSystemVotingResult(
- votedOut != null
- ? votedOut.getName()
- : null))
- .build())
- .build());
- votingHub.broadcast(broadcastMsgs).block();
+ .content(TextBlock.builder().text(detailedResult.toString()).build())
+ .build();
+ votingHub.broadcast(votedOutMsg).block();
return votedOut;
}
}
+ /**
+ * Sheriff election phase on first day. Only the seer and one werewolf will run for sheriff,
+ * other players vote.
+ */
+ /**
+ * Sheriff election phase.
+ *
+ *
1. Candidates register 2. Campaign speeches 3. Voting
+ */
+ private void sheriffElectionPhase() {
+ List alivePlayers = gameState.getAlivePlayers();
+ if (alivePlayers.size() < 3) {
+ return; // Not enough players for election
+ }
+
+ // Step 1: Registration phase
+ List candidates = handleSheriffRegistration(alivePlayers);
+ if (candidates.isEmpty()) {
+ emitter.emitSystemMessage(messages.getSystemNoCandidates());
+ return;
+ }
+
+ if (candidates.size() == 1) {
+ // Only one candidate, automatically elected
+ Player sheriff = candidates.get(0);
+ gameState.setSheriff(sheriff);
+ // Single candidate gets 1 vote automatically
+ Map voteDetails = new HashMap<>();
+ Map candidateDetail = new HashMap<>();
+ candidateDetail.put("votes", 1);
+ candidateDetail.put("voters", Collections.singletonList("自动当选"));
+ voteDetails.put(sheriff.getName(), candidateDetail);
+ emitter.emitSheriffElected(sheriff.getName(), 1, voteDetails);
+ decideSpeakOrder(sheriff);
+ return;
+ }
+
+ // Step 2: Campaign speeches - use MsgHub to broadcast
+ handleCampaignSpeeches(candidates, alivePlayers);
+
+ // Step 3: Voting phase
+ Player sheriff = handleSheriffVoting(candidates, alivePlayers);
+ if (sheriff != null) {
+ decideSpeakOrder(sheriff);
+ }
+ }
+
+ /**
+ * Handle sheriff registration phase.
+ *
+ * @param alivePlayers all alive players
+ * @return list of candidates who registered
+ */
+ private List handleSheriffRegistration(List alivePlayers) {
+ emitter.emitSystemMessage("【警长竞选】进入上警环节,所有玩家决定是否上警...");
+
+ List candidates = new ArrayList<>();
+
+ // Separate human and AI players
+ List aiPlayers = new ArrayList<>();
+ for (Player player : alivePlayers) {
+ if (player.isHuman()) {
+ String input =
+ userInput
+ .waitForInput(
+ WebUserInput.INPUT_SHERIFF_REGISTER,
+ messages.getPromptSheriffRegister(),
+ List.of("是", "否"))
+ .block();
+ boolean shouldRegister = "是".equals(input) || "yes".equalsIgnoreCase(input);
+ if (shouldRegister) {
+ player.registerForSheriff();
+ candidates.add(player);
+ }
+ } else {
+ aiPlayers.add(player);
+ }
+ }
+
+ // Parallel AI registration decisions
+ if (!aiPlayers.isEmpty()) {
+ Msg registrationPrompt = prompts.createSheriffRegistrationPrompt(gameState);
+ List aiPlayerAgents =
+ aiPlayers.stream().map(Player::getAgent).collect(Collectors.toList());
+ List registrationMsgs =
+ new FanoutPipeline(aiPlayerAgents)
+ .execute(registrationPrompt, SheriffRegistrationModel.class)
+ .onErrorReturn(new ArrayList<>())
+ .block();
+ if (registrationMsgs == null) {
+ registrationMsgs = new ArrayList<>();
+ }
+
+ // Process results - match msgs back to players by agent name
+ Map agentNameToPlayer = new HashMap<>();
+ for (Player player : aiPlayers) {
+ agentNameToPlayer.put(player.getName(), player);
+ }
+ for (Msg registrationMsg : registrationMsgs) {
+ Player player = agentNameToPlayer.get(registrationMsg.getName());
+ if (player == null) {
+ continue;
+ }
+ try {
+ SheriffRegistrationModel registration =
+ registrationMsg.getStructuredData(SheriffRegistrationModel.class);
+ boolean shouldRegister =
+ registration.registerForSheriff != null
+ && registration.registerForSheriff;
+ if (shouldRegister) {
+ player.registerForSheriff();
+ candidates.add(player);
+ }
+ } catch (Exception e) {
+ // Skip failed results
+ }
+ }
+ }
+
+ // Wait 2 seconds before announcing candidates
+ try {
+ Thread.sleep(2000);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+
+ // Announce candidates with raise hand icon
+ List candidateNames =
+ candidates.stream().map(Player::getName).collect(Collectors.toList());
+ emitter.emitSheriffCandidatesAnnounced(candidateNames);
+ emitter.emitSystemMessage("【上警玩家】" + String.join("、", candidateNames));
+
+ return candidates;
+ }
+
+ /**
+ * Handle campaign speeches using MsgHub for automatic broadcasting.
+ *
+ * @param candidates list of candidates
+ * @param alivePlayers all alive players
+ * @return ordered list of candidates for speech
+ */
+ private List handleCampaignSpeeches(
+ List candidates, List alivePlayers) {
+ emitter.emitSystemMessage(messages.getSystemCampaignStart());
+
+ // Randomly select start position and order
+ Random random = new Random();
+ int startIndex = random.nextInt(candidates.size());
+ boolean isReversed = random.nextBoolean();
+
+ List orderedCandidates = new ArrayList<>();
+ for (int i = 0; i < candidates.size(); i++) {
+ int index =
+ isReversed
+ ? (startIndex - i + candidates.size()) % candidates.size()
+ : (startIndex + i) % candidates.size();
+ orderedCandidates.add(candidates.get(index));
+ }
+
+ // Announce speech order
+ String orderAnnouncement =
+ String.format(
+ "【发言顺序】随机从 %s 开始,%s发言:%s",
+ orderedCandidates.get(0).getName(),
+ isReversed ? "逆序" : "顺序",
+ orderedCandidates.stream()
+ .map(Player::getName)
+ .collect(Collectors.joining(" -> ")));
+ emitter.emitSystemMessage(orderAnnouncement);
+
+ // Use MsgHub to broadcast speeches to all agents
+ try (MsgHub campaignHub =
+ MsgHub.builder()
+ .name("警长竞选发言")
+ .announcement(
+ prompts.createSheriffElectionStartPrompt(
+ gameState, orderedCandidates))
+ .enableAutoBroadcast(true)
+ .participants(
+ alivePlayers.stream()
+ .filter(p -> !p.isHuman())
+ .map(Player::getAgent)
+ .toArray(AgentBase[]::new))
+ .build()) {
+
+ campaignHub.enter().block();
+
+ for (Player candidate : orderedCandidates) {
+ if (candidate.isHuman()) {
+ String speech =
+ userInput
+ .waitForInput(
+ WebUserInput.INPUT_SHERIFF_CAMPAIGN,
+ messages.getPromptSheriffCampaign(candidate.getName()),
+ null)
+ .block();
+ emitter.emitSheriffCampaign(candidate.getName(), speech, null, null);
+
+ // Console log for debugging
+ System.out.println("\n=== 警长竞选发言 ===");
+ System.out.println("玩家: " + candidate.getName());
+ System.out.println("发言: " + speech);
+ System.out.println("==================\n");
+ campaignHub
+ .broadcast(
+ Msg.builder()
+ .name(candidate.getName())
+ .role(MsgRole.USER)
+ .content(TextBlock.builder().text(speech).build())
+ .build())
+ .block();
+ } else {
+ try {
+ campaignHub.setAutoBroadcast(false);
+ Msg campaignMsg =
+ candidate
+ .getAgent()
+ .call(
+ prompts.createSheriffCampaignPrompt(
+ gameState, candidate),
+ SheriffCampaignModel.class)
+ .block();
+ SheriffCampaignModel campaign =
+ campaignMsg.getStructuredData(SheriffCampaignModel.class);
+ // 规范化空值:空白字符串视为null,前端不显示
+ String checkResult =
+ (campaign.checkResult != null
+ && !campaign.checkResult.trim().isEmpty())
+ ? campaign.checkResult.trim()
+ : null;
+ String nextCheckTarget =
+ (campaign.nextCheckTarget != null
+ && !campaign.nextCheckTarget.trim().isEmpty())
+ ? campaign.nextCheckTarget.trim()
+ : null;
+ emitter.emitSheriffCampaign(
+ candidate.getName(),
+ campaign.campaignSpeech,
+ checkResult,
+ nextCheckTarget);
+
+ // Generate TTS for AI campaign speech
+ generateTTSForSpeech(candidate.getName(), campaign.campaignSpeech);
+
+ // Console log for debugging
+ System.out.println("\n=== 警长竞选发言 ===");
+ System.out.println("玩家: " + candidate.getName());
+ System.out.println("角色: " + candidate.getRole());
+ System.out.println("发言: " + campaign.campaignSpeech);
+ if (campaign.checkResult != null && !campaign.checkResult.isEmpty()) {
+ System.out.println("验人信息: " + campaign.checkResult);
+ }
+ if (campaign.nextCheckTarget != null
+ && !campaign.nextCheckTarget.isEmpty()) {
+ System.out.println("今晚将验: " + campaign.nextCheckTarget);
+ }
+ System.out.println("==================\n");
+
+ // Build full speech content
+ StringBuilder fullSpeech = new StringBuilder();
+ fullSpeech.append(campaign.campaignSpeech);
+ if (campaign.checkResult != null && !campaign.checkResult.isEmpty()) {
+ fullSpeech.append(" (验人信息: ").append(campaign.checkResult).append(")");
+ }
+ if (campaign.nextCheckTarget != null
+ && !campaign.nextCheckTarget.isEmpty()) {
+ fullSpeech
+ .append(" (今晚验: ")
+ .append(campaign.nextCheckTarget)
+ .append(")");
+ }
+
+ // Broadcast to MsgHub
+ campaignHub.setAutoBroadcast(true);
+ campaignHub
+ .broadcast(
+ Msg.builder()
+ .name(candidate.getName())
+ .role(MsgRole.USER)
+ .content(
+ TextBlock.builder()
+ .text(fullSpeech.toString())
+ .build())
+ .build())
+ .block();
+
+ } catch (Exception e) {
+ emitter.emitError(messages.getErrorSheriffCampaign(e.getMessage()));
+ }
+ }
+ }
+ }
+
+ return orderedCandidates;
+ }
+
+ /**
+ * Handle sheriff voting phase.
+ *
+ * @param candidates list of all candidates
+ * @param alivePlayers all alive players
+ * @return elected sheriff, or null if election failed
+ */
+ private Player handleSheriffVoting(List candidates, List alivePlayers) {
+ // Only non-candidates vote
+ List voterList =
+ alivePlayers.stream()
+ .filter(p -> !p.isRegisteredForSheriff())
+ .collect(Collectors.toList());
+
+ if (voterList.isEmpty()) {
+ // All players are candidates, auto-elect first
+ Player sheriff = candidates.get(0);
+ gameState.setSheriff(sheriff);
+ // Build vote details for frontend popup (all candidates get 0 votes)
+ Map voteDetails = new HashMap<>();
+ for (Player candidate : candidates) {
+ Map candidateDetail = new HashMap<>();
+ candidateDetail.put("votes", 0);
+ candidateDetail.put("voters", new ArrayList());
+ voteDetails.put(candidate.getName(), candidateDetail);
+ }
+ emitter.emitSheriffElected(sheriff.getName(), 0, voteDetails);
+ return sheriff;
+ }
+
+ emitter.emitSystemMessage(messages.getSystemSheriffVotingStart());
+
+ List candidateNames =
+ candidates.stream().map(Player::getName).collect(Collectors.toList());
+
+ Map voteCount = new HashMap<>();
+ Map> votersByCandidate = new HashMap<>();
+
+ // Initialize vote tracking
+ for (String candidateName : candidateNames) {
+ voteCount.put(candidateName, 0);
+ votersByCandidate.put(candidateName, new ArrayList<>());
+ }
+
+ // Collect votes - separate human and AI voters
+ List aiVoters = new ArrayList<>();
+ for (Player voter : voterList) {
+ if (voter.isHuman()) {
+ String voteTarget =
+ userInput
+ .waitForInput(
+ WebUserInput.INPUT_SHERIFF_VOTE,
+ messages.getPromptSheriffVote(),
+ candidateNames)
+ .block();
+ // Don't emit individual vote event to frontend
+ voteCount.put(voteTarget, voteCount.getOrDefault(voteTarget, 0) + 1);
+ votersByCandidate.get(voteTarget).add(voter.getName());
+ } else {
+ // AI voter - add to parallel list
+ aiVoters.add(voter);
+ }
+ }
+
+ // Parallel voting for AI players
+ if (!aiVoters.isEmpty()) {
+ Msg votingPrompt = prompts.createSheriffVotingPrompt(gameState, candidates);
+ List aiVoterAgents =
+ aiVoters.stream().map(Player::getAgent).collect(Collectors.toList());
+ List voteMsgs =
+ new FanoutPipeline(aiVoterAgents)
+ .execute(votingPrompt, SheriffVoteModel.class)
+ .onErrorReturn(new ArrayList<>())
+ .block();
+ if (voteMsgs == null) {
+ voteMsgs = new ArrayList<>();
+ }
+
+ // Process results
+ for (Msg voteMsg : voteMsgs) {
+ try {
+ SheriffVoteModel vote = voteMsg.getStructuredData(SheriffVoteModel.class);
+ if (vote.targetPlayer != null && candidateNames.contains(vote.targetPlayer)) {
+ voteCount.put(
+ vote.targetPlayer,
+ voteCount.getOrDefault(vote.targetPlayer, 0) + 1);
+ votersByCandidate.get(vote.targetPlayer).add(voteMsg.getName());
+ } else {
+ emitter.emitError(
+ messages.getErrorSheriffVote(
+ "Invalid vote target: "
+ + vote.targetPlayer
+ + ", must be one of: "
+ + candidateNames));
+ }
+ } catch (Exception e) {
+ // Skip failed results
+ }
+ }
+ }
+
+ // Determine winner
+ String sheriffName =
+ voteCount.entrySet().stream()
+ .max(Map.Entry.comparingByValue())
+ .map(Map.Entry::getKey)
+ .orElse(candidateNames.get(0));
+
+ Player sheriff = gameState.findPlayerByName(sheriffName);
+ if (sheriff != null) {
+ int totalVotes = voteCount.getOrDefault(sheriffName, 0);
+
+ // Build vote result message for broadcasting to all agents
+ StringBuilder voteResultMsg = new StringBuilder();
+ voteResultMsg.append("【警长投票结果】\n");
+ // Emit vote results for each candidate (showing only vote counts and voter names)
+ for (String candidateName : candidateNames) {
+ int votes = voteCount.get(candidateName);
+ List voterNames = votersByCandidate.get(candidateName);
+ String voterListStr = voterNames.isEmpty() ? "无" : String.join("、", voterNames);
+ String resultLine = String.format("%s ⬅ %s", candidateName, voterListStr);
+ emitter.emitSystemMessage(resultLine);
+ voteResultMsg.append(resultLine).append("\n");
+ }
+
+ // Build vote details for frontend popup
+ Map voteDetails = new HashMap<>();
+ for (String candidateName : candidateNames) {
+ int votes = voteCount.get(candidateName);
+ List voterNames = votersByCandidate.get(candidateName);
+ Map candidateDetail = new HashMap<>();
+ candidateDetail.put("votes", votes);
+ candidateDetail.put("voters", voterNames);
+ voteDetails.put(candidateName, candidateDetail);
+ }
+
+ // Check if sheriff badge is lost (highest vote count is 0)
+ if (totalVotes == 0) {
+ emitter.emitSystemMessage("【警徽流失】所有候选人得票为0,警徽流失");
+ gameState.setSheriff(null);
+ // Still emit sheriff elected event with voteCount=0 to trigger UI cleanup
+ emitter.emitSheriffElected(null, 0, voteDetails);
+ return null;
+ }
+
+ // Normal sheriff election
+ gameState.setSheriff(sheriff);
+ // Add sheriff elected info to broadcast message
+ voteResultMsg.append(
+ String.format("\n【警长当选】%s 获得 %d 票,当选警长", sheriff.getName(), totalVotes));
+ // Broadcast updated vote result to all AI agents
+ broadcastToAllAgents(voteResultMsg.toString());
+ // Emit sheriff elected event
+ emitter.emitSheriffElected(sheriff.getName(), totalVotes, voteDetails);
+ }
+
+ return sheriff;
+ }
+
+ /** Sheriff decides the speaking order for discussion. */
+ private void decideSpeakOrder(Player sheriff) {
+ if (sheriff.isHuman()) {
+ String order =
+ userInput
+ .waitForInput(
+ WebUserInput.INPUT_SPEAK_ORDER,
+ messages.getPromptSpeakOrder(),
+ List.of("normal", "reversed"))
+ .block();
+ boolean reversed = "reversed".equalsIgnoreCase(order);
+ gameState.setSpeakOrderReversed(reversed);
+ emitter.emitSystemMessage(
+ messages.getSystemSpeakOrderDecision(
+ sheriff.getName(), reversed ? "逆序" : "顺序"));
+ } else {
+ try {
+ Msg orderMsg =
+ sheriff.getAgent()
+ .call(
+ prompts.createSpeakOrderPrompt(sheriff),
+ SpeakOrderModel.class)
+ .block();
+ SpeakOrderModel model = orderMsg.getStructuredData(SpeakOrderModel.class);
+ boolean reversed = !Boolean.TRUE.equals(model.normalOrder);
+ gameState.setSpeakOrderReversed(reversed);
+ emitter.emitSystemMessage(
+ messages.getSystemSpeakOrderDecision(
+ sheriff.getName(), reversed ? "逆序" : "顺序"));
+ } catch (Exception e) {
+ emitter.emitError(messages.getErrorSpeakOrder(e.getMessage()));
+ }
+ }
+ }
+
+ /**
+ * New sheriff decides the speaking order from their position after receiving badge. This is
+ * used when sheriff badge is transferred (either night death or day vote out).
+ */
+ private void decideSpeakOrderFromPlayer(Player newSheriff) {
+ // Only ask for speak order if it's not the first day (first day already decided after
+ // election)
+ if (gameState.getCurrentRound() == 1 && gameState.getSheriff() != null) {
+ // First day, speak order already decided after election
+ return;
+ }
+
+ if (newSheriff.isHuman()) {
+ String order =
+ userInput
+ .waitForInput(
+ WebUserInput.INPUT_SPEAK_ORDER,
+ messages.getPromptSpeakOrderFromPosition(newSheriff.getName()),
+ List.of("normal", "reversed"))
+ .block();
+ boolean reversed = "reversed".equalsIgnoreCase(order);
+ gameState.setSpeakOrderReversed(reversed);
+ emitter.emitSystemMessage(
+ messages.getSystemSpeakOrderDecision(
+ newSheriff.getName(), reversed ? "逆序" : "顺序"));
+ } else {
+ try {
+ Msg orderMsg =
+ newSheriff
+ .getAgent()
+ .call(
+ prompts.createSpeakOrderFromPositionPrompt(
+ gameState, newSheriff),
+ SpeakOrderModel.class)
+ .block();
+ SpeakOrderModel model = orderMsg.getStructuredData(SpeakOrderModel.class);
+ boolean reversed = !Boolean.TRUE.equals(model.normalOrder);
+ gameState.setSpeakOrderReversed(reversed);
+ emitter.emitSystemMessage(
+ messages.getSystemSpeakOrderDecision(
+ newSheriff.getName(), reversed ? "逆序" : "顺序"));
+ } catch (Exception e) {
+ emitter.emitError(messages.getErrorSpeakOrder(e.getMessage()));
+ }
+ }
+ }
+
+ /** Sheriff transfers badge when leaving the game. */
+ private void sheriffTransferBadge(Player sheriff) {
+ emitter.emitSystemMessage(messages.getSystemSheriffTransferStart());
+
+ List transferOptions =
+ gameState.getAlivePlayers().stream()
+ .filter(p -> !p.equals(sheriff))
+ .map(Player::getName)
+ .collect(Collectors.toList());
+ transferOptions.add(0, "skip");
+
+ if (sheriff.isHuman()) {
+ String targetName =
+ userInput
+ .waitForInput(
+ WebUserInput.INPUT_SHERIFF_TRANSFER,
+ messages.getPromptSheriffTransfer(),
+ transferOptions)
+ .block();
+
+ if (targetName != null
+ && !targetName.isEmpty()
+ && !"skip".equalsIgnoreCase(targetName)) {
+ Player newSheriff = gameState.findPlayerByName(targetName);
+ if (newSheriff != null && newSheriff.isAlive()) {
+ gameState.setSheriff(newSheriff);
+ emitter.emitSheriffTransfer(sheriff.getName(), targetName, null, "");
+
+ // Broadcast sheriff transfer to all AI agents
+ String transferMsg =
+ String.format("【警徽移交】%s 将警徽移交给 %s", sheriff.getName(), targetName);
+ broadcastToAllAgents(transferMsg);
+
+ // New sheriff decides speak order from their position
+ decideSpeakOrderFromPlayer(newSheriff);
+ }
+ } else {
+ gameState.setSheriff(null);
+ emitter.emitSheriffTransfer(sheriff.getName(), null, null, "");
+
+ // Broadcast sheriff badge loss to all AI agents
+ String lossMsg = String.format("【警徽流失】%s 选择不移交警徽,警徽流失", sheriff.getName());
+ broadcastToAllAgents(lossMsg);
+ }
+ } else {
+ try {
+ Msg agentResponseMsg =
+ sheriff.getAgent()
+ .call(
+ prompts.createSheriffTransferPrompt(gameState, sheriff),
+ SheriffTransferModel.class)
+ .block();
+ SheriffTransferModel transfer =
+ agentResponseMsg.getStructuredData(SheriffTransferModel.class);
+
+ if (transfer.targetPlayer != null && !transfer.targetPlayer.isEmpty()) {
+ Player newSheriff = gameState.findPlayerByName(transfer.targetPlayer);
+ if (newSheriff != null && newSheriff.isAlive()) {
+ gameState.setSheriff(newSheriff);
+ // Night death: no checkInfo (no last will), day vote out: may have info
+ // For night deaths, checkInfo is always null
+ boolean isNightDeath =
+ sheriff.equals(gameState.getLastNightVictim())
+ || sheriff.equals(gameState.getLastPoisonedVictim());
+ String checkInfo = isNightDeath ? null : transfer.checkInfo;
+ emitter.emitSheriffTransfer(
+ sheriff.getName(),
+ transfer.targetPlayer,
+ checkInfo,
+ transfer.reason);
+
+ // Broadcast sheriff transfer to all AI agents
+ String broadcastMsg =
+ String.format(
+ "【警徽移交】%s 将警徽移交给 %s",
+ sheriff.getName(), transfer.targetPlayer);
+ if (checkInfo != null && !checkInfo.isEmpty()) {
+ broadcastMsg += ",遗言:" + checkInfo;
+ }
+ if (transfer.reason != null && !transfer.reason.isEmpty()) {
+ broadcastMsg += "。理由:" + transfer.reason;
+ }
+ broadcastToAllAgents(broadcastMsg);
+
+ // New sheriff decides speak order from their position
+ decideSpeakOrderFromPlayer(newSheriff);
+ }
+ } else {
+ gameState.setSheriff(null);
+ emitter.emitSheriffTransfer(sheriff.getName(), null, null, transfer.reason);
+
+ // Broadcast sheriff badge loss to all AI agents
+ String lossMsg = String.format("【警徽流失】%s 选择不移交警徽,警徽流失", sheriff.getName());
+ if (transfer.reason != null && !transfer.reason.isEmpty()) {
+ lossMsg += "。理由:" + transfer.reason;
+ }
+ broadcastToAllAgents(lossMsg);
+ }
+ } catch (Exception e) {
+ gameState.setSheriff(null);
+ emitter.emitError(messages.getErrorSheriffTransfer(e.getMessage()));
+ }
+ }
+ }
+
/**
* Hunter shoots after being eliminated.
*
* @param hunter the hunter player
- * @param revealIdentity true if hunter identity should be revealed (day vote out),
- * false if only target death should be announced (night death)
+ * @param revealIdentity true if hunter identity should be revealed (day vote out), false if
+ * only target death should be announced (night death)
+ * @return hunter shoot information string for broadcasting to agents
*/
- private void hunterShoot(Player hunter, boolean revealIdentity) {
+ private String hunterShoot(Player hunter, boolean revealIdentity) {
emitter.emitSystemMessage(messages.getSystemHunterSkill());
boolean isHumanHunter = hunter.isHuman();
@@ -1042,6 +1859,8 @@ private void hunterShoot(Player hunter, boolean revealIdentity) {
emitter.emitPlayerEliminated(targetPlayer.getName(), roleName, "shot");
// Broadcast hunter shoot info to all alive agents
broadcastHunterShootToAllAgents(hunter, targetPlayer, revealIdentity);
+ return String.format(
+ "猎人 %s 开枪带走了 %s", hunter.getName(), targetPlayer.getName());
}
} else {
emitter.emitPlayerAction(
@@ -1052,6 +1871,7 @@ private void hunterShoot(Player hunter, boolean revealIdentity) {
messages.getActionHunterShootSkip(),
EventVisibility.PUBLIC);
}
+ return null;
} else {
// AI hunter decides
try {
@@ -1080,6 +1900,9 @@ private void hunterShoot(Player hunter, boolean revealIdentity) {
emitter.emitPlayerEliminated(targetPlayer.getName(), roleName, "shot");
// Broadcast hunter shoot info to all alive agents
broadcastHunterShootToAllAgents(hunter, targetPlayer, revealIdentity);
+ emitStatsUpdate();
+ return String.format(
+ "猎人 %s 开枪带走了 %s", hunter.getName(), targetPlayer.getName());
}
} else {
emitter.emitPlayerAction(
@@ -1096,32 +1919,20 @@ private void hunterShoot(Player hunter, boolean revealIdentity) {
}
emitStatsUpdate();
+ return null;
}
/**
- * Broadcast hunter shoot information to all alive agents' memory.
- * This ensures all AI agents know about the hunter's action for future decision making.
- * Only broadcasts when hunter identity should be revealed (day vote out scenario).
+ * Broadcast a message to all alive agents' memory.
*
- * @param hunter the hunter player
- * @param target the target player who was shot
- * @param revealIdentity true to broadcast "Hunter X shot Y", false to skip broadcast
+ * @param message the message to broadcast
*/
- private void broadcastHunterShootToAllAgents(
- Player hunter, Player target, boolean revealIdentity) {
- if (!revealIdentity) {
- // Night death: no need to broadcast, death is already announced in night result
- return;
- }
-
- // Day vote out: reveal hunter identity and broadcast to all agents
- String announcement =
- messages.getSystemHunterShootAnnouncement(hunter.getName(), target.getName());
- Msg announcementMsg =
+ private void broadcastToAllAgents(String message) {
+ Msg msg =
Msg.builder()
.name("Moderator Message")
.role(MsgRole.USER)
- .content(TextBlock.builder().text(announcement).build())
+ .content(TextBlock.builder().text(message).build())
.build();
// Add to all alive players' agent memory (only ReActAgent has memory)
@@ -1129,12 +1940,34 @@ private void broadcastHunterShootToAllAgents(
if (player.getAgent() instanceof ReActAgent) {
ReActAgent reactAgent = (ReActAgent) player.getAgent();
if (reactAgent.getMemory() != null) {
- reactAgent.getMemory().addMessage(announcementMsg);
+ reactAgent.getMemory().addMessage(msg);
}
}
}
}
+ /**
+ * Broadcast hunter shoot information to all alive agents' memory. This ensures all AI agents
+ * know about the hunter's action for future decision making. Only broadcasts when hunter
+ * identity should be revealed (day vote out scenario).
+ *
+ * @param hunter the hunter player
+ * @param target the target player who was shot
+ * @param revealIdentity true to broadcast "Hunter X shot Y", false to skip broadcast
+ */
+ private void broadcastHunterShootToAllAgents(
+ Player hunter, Player target, boolean revealIdentity) {
+ if (!revealIdentity) {
+ // Night death: no need to broadcast, death is already announced in night result
+ return;
+ }
+
+ // Day vote out: reveal hunter identity and broadcast to all agents
+ String announcement =
+ messages.getSystemHunterShootAnnouncement(hunter.getName(), target.getName());
+ broadcastToAllAgents(announcement);
+ }
+
private boolean checkGameEnd() {
return gameState.checkVillagersWin() || gameState.checkWerewolvesWin();
}
@@ -1157,8 +1990,8 @@ private void emitStatsUpdate() {
}
/**
- * Generate TTS audio for a player's speech and emit audio chunks to frontend.
- * Only called during day discussion phase to avoid generating TTS for votes/actions.
+ * Generate TTS audio for a player's speech and emit audio chunks to frontend. Only called
+ * during day discussion phase to avoid generating TTS for votes/actions.
*
* @param playerName The name of the speaking player
* @param text The text content to convert to speech
diff --git a/agentscope-examples/werewolf-hitl/src/main/resources/messages.properties b/agentscope-examples/werewolf-hitl/src/main/resources/messages.properties
index b5a5bb626..8c43d6278 100644
--- a/agentscope-examples/werewolf-hitl/src/main/resources/messages.properties
+++ b/agentscope-examples/werewolf-hitl/src/main/resources/messages.properties
@@ -1,8 +1,6 @@
# Werewolf Game - English Messages (Default)
-
# Config
-config.player.names=Alice,Bob,Charlie,Diana,Eve,Frank,Grace,Henry,Ivy
-
+config.player.names=No.1,No.2,No.3,No.4,No.5,No.6,No.7,No.8,No.9
# Roles
role.villager.name=Villager
role.villager.symbol=\uD83D\uDC64
@@ -14,92 +12,80 @@ role.witch.name=Witch
role.witch.symbol=\uD83E\uDDEA
role.hunter.name=Hunter
role.hunter.symbol=\uD83C\uDFF9
-
# Game Messages
-game.welcome.title=Werewolf Game - 9 Player Multi-Agent Game
-game.welcome.description=A complex social deduction game where players are divided into villagers and werewolves.\nVillagers must identify and eliminate werewolves through discussion and voting.\nWerewolves must eliminate villagers without revealing their identities.\n\nRoles:\n - 3 Villagers: No special abilities\n - 3 Werewolves: Eliminate one villager each night\n - 1 Seer: Can check one player''s identity each night\n - 1 Witch: Has heal and poison potions (one-time use each)\n - 1 Hunter: Can shoot one player when eliminated
-game.player.assignments=Player Assignments:
+game.welcome.title=Werewolf Game - {0}-Player Multi-Agent Battle
+game.welcome.description=A complex social deduction game. Players are divided into Gods (Seer, Witch, Hunter), Villagers, and Werewolves. Villagers need to find and eliminate werewolves through discussion and voting. Gods need to find and eliminate werewolves through voting or skills. Werewolves must eliminate Gods or Villagers without exposing their identities.
+game.player.assignments=Player Role Assignments:
game.initializing=Initializing Werewolf Game
-
# Phases
phase.night.title=\uD83C\uDF19 Night Phase - Everyone close your eyes...
phase.day.title=\u2600\uFE0F Day Phase - Everyone open your eyes...
phase.voting.title=\uD83D\uDDF3\uFE0F Voting Phase
-phase.night.complete=\uD83C\uDF19 Night phase complete. Waiting for sunrise...
-
+phase.night.complete=\n\uD83C\uDF19 Night phase complete. Waiting for sunrise...\n
# Werewolf Actions
-werewolf.discussion=--- Werewolves Discussion ---
-werewolf.discussion.round= Werewolf Discussion Round {0}:
-werewolf.voting= Werewolf Voting:
+werewolf.discussion=\n--- Werewolf Discussion ---
+werewolf.discussion.round=Werewolf Discussion Round {0}:
+werewolf.voting=\n Werewolf Voting:
werewolf.chose=Werewolves chose to eliminate: {0}
-
+human.werewolf.discussion=Night phase - please discuss with your werewolf teammates and reach a consensus on who to eliminate tonight, and decide who will bluff as the Seer
# Witch Actions
-witch.actions=--- Witch Actions ---
-witch.sees.victim= Witch sees: {0} was attacked by werewolves
-witch.heal.decision= [{0}] Heal decision: {1} (Reason: {2})
-witch.used.heal= \u2713 Witch used heal potion to save {0}
-witch.poison.decision= [{0}] Poison decision: {1} (Target: {2}, Reason: {3})
-witch.used.poison= \u2713 Witch used poison potion on {0}
-
+witch.actions=\n--- Witch Actions ---
+witch.sees.victim=Witch sees: {0} was attacked by werewolves
+witch.heal.decision=[{0}] Heal decision: {1} (Reason: {2})
+witch.used.heal=\u2713 Witch used heal potion to save {0}
+witch.poison.decision=[{0}] Poison decision: {1} (Target: {2}, Reason: {3})
+witch.used.poison=\u2713 Witch used poison potion on {0}
# Seer Actions
-seer.check=--- Seer Check ---
-seer.check.decision= [{0}] wants to check: {1} (Reason: {2})
-seer.check.result= \u2713 Result: {0} is {1}
-
+seer.check=\n--- Seer Check ---
+seer.check.decision=[{0}] wants to check: {1} (Reason: {2})
+seer.check.result=\u2713 Check result: {0} {1}
# Day Discussion
-day.discussion=--- Day Discussion ---
-discussion.round= Discussion Round {0}:
-
+day.discussion=\n--- Day Discussion ---
+discussion.round=\n Discussion Round {0}:
# Voting
voting.results=\nVoting Results:
voting.no.valid=\nNo valid votes. No one is eliminated.
voting.tie=\nTie detected among: {0}. Randomly selected: {1}
-voting.count= {0}: {1} votes
-voting.eliminated=\n{0} was eliminated by vote. They were a {1}.
-voting.detail= [{0}] votes for: {1} (Reason: {2})
-
+voting.count={0}: {1} votes
+voting.eliminated=\n{0} was eliminated by vote. Their role was {1}.
+voting.detail=[{0}] votes for: {1} (Reason: {2})
# Hunter
-hunter.shoot=--- Hunter''s Last Shot ---
-hunter.shoot.decision= [{0}] Shoot decision: {1} (Target: {2}, Reason: {3})
-hunter.shot.player= \u2713 Hunter shot {0}. They were a {1}.
-hunter.no.shoot= Hunter chose not to shoot.
-
+hunter.shoot=\n--- Hunter''s Last Shot ---
+hunter.shoot.decision=[{0}] Shoot decision: {1} (Target: {2}, Reason: {3})
+hunter.shot.player=\u2713 Hunter shot {0}. Their role was {1}.
+hunter.no.shoot=Hunter chose not to shoot.
# Game End
-game.over=GAME OVER
-game.villagers.win=\uD83C\uDF89 VILLAGERS WIN! \uD83C\uDF89
+game.over=Game Over
+game.villagers.win=\uD83C\uDF89 Villagers Win! \uD83C\uDF89
game.villagers.win.explanation=All werewolves have been eliminated.
-game.werewolves.win=\uD83D\uDC3A WEREWOLVES WIN! \uD83D\uDC3A
-game.werewolves.win.explanation=Werewolves have overtaken the village.
-game.max.rounds=Game ended without a clear winner (max rounds reached).
-
+game.werewolves.win=\uD83D\uDC3A Werewolves Win! \uD83D\uDC3A
+game.werewolves.win.explanation=Werewolves have taken over the village.
+game.max.rounds=Game ended after reaching the maximum number of rounds without a clear winner.
# Status
status.final=\nFinal Status:
-status.alive.players=Alive players:
+status.alive.players=Alive players:
status.all.players=\nAll players and their roles:
status.round=Round {0} - Game Status
-status.alive=Alive: {0} players | Werewolves: {1} | Villagers: {2}
+status.alive=Alive: {0} | Werewolves: {1} | Villagers: {2}
status.label.alive=alive
status.label.dead=dead
-
# Decisions
-decision.yes=YES
-decision.no=NO
-decision.witch.heal.yes=YES, use heal potion
-decision.witch.poison.yes=YES, use poison
-decision.hunter.shoot.yes=YES, will shoot
-decision.hunter.shoot.no=NO, won''t shoot
+decision.yes=Yes
+decision.no=No
+decision.witch.heal.yes=Yes, use heal potion
+decision.witch.poison.yes=Yes, use poison potion
+decision.hunter.shoot.yes=Yes, shoot
+decision.hunter.shoot.no=No, don''t shoot
decision.is.werewolf=a Werewolf
decision.not.werewolf=not a Werewolf
-
# Errors
-error.vote.parsing= [{0}] vote parsing error
-error.decision= Error in {0} decision:
-
+error.vote.parsing=[{0}] vote parsing error
+error.decision={0} decision error:
# System Messages
-system.werewolf.kill.result=Werewolves have decided to kill: {0}
-system.werewolf.kill.none=Werewolves have decided to kill: no one
-system.voting.result=Voting result: {0} will be eliminated.
-system.voting.result.none=Voting result: no one (tie) will be eliminated.
+system.werewolf.kill.result=Werewolves decided to kill: {0}
+system.werewolf.kill.none=Werewolves decided to kill: no one
+system.voting.result=Voting result: {0} will be eliminated
+system.voting.result.none=Voting result: no one eliminated (tie)
system.werewolf.discussing=Werewolves are discussing...
system.witch.acting=Witch is taking action...
system.witch.sees.victim=Witch sees tonight''s victim is {0}
@@ -108,52 +94,98 @@ system.day.discussion.start=Day discussion begins...
system.voting.start=Voting phase begins...
system.hunter.skill=Hunter activates skill...
system.hunter.shoot.announcement=[Announcement] Hunter {0} activated skill and took down {1}.
-
# Action Descriptions
action.witch.use.heal=Use heal potion
-action.witch.heal.result=Successfully healed
+action.witch.heal.result=Heal successful
action.witch.heal.skip=Choose not to use heal potion
action.witch.use.poison=Use poison potion
-action.witch.poison.result=Successfully poisoned
+action.witch.poison.result=Poison successful
action.witch.poison.skip=Choose not to use poison potion
action.seer.check=Check
action.hunter.shoot=Shoot
-action.hunter.shoot.result=Successfully shot
+action.hunter.shoot.result=Shot successful
action.hunter.shoot.skip=Choose not to shoot
error.witch.heal=Witch heal decision error: {0}
error.witch.poison=Witch poison decision error: {0}
error.seer.check=Seer check error: {0}
error.hunter.shoot=Hunter shoot error: {0}
-
# Prompts
-prompt.role.villager=You are {0}, a Villager in the Werewolf game.\nYour goal is to identify and eliminate all werewolves through discussion and voting.\nObserve other players'' behaviors carefully and vote wisely.\nYou have no special abilities, but your analytical skills are crucial.\nKeep your responses concise (2-3 sentences).
-prompt.role.werewolf=You are {0}, a Werewolf.\nYour goal is to eliminate all villagers without exposing your identity.\nDuring night phase, coordinate with other werewolves to choose a victim.\nDuring day phase, blend in with villagers and deflect suspicion. Remember that your night actions are only known to your werewolf teammates - don''t reveal them directly.\nDuring discussion, observe your teammates'' speeches and coordinate with them.\nKeep your responses concise (2-3 sentences).
-prompt.role.seer=You are {0}, the Seer (a special villager role).\nYour goal is to help villagers identify werewolves.\nEach night, you can check one player''s true identity (Werewolf or not).\nUse this information carefully - revealing yourself too early may make you a target.\nKeep your responses concise (2-3 sentences).
-prompt.role.witch=You are {0}, the Witch (a special villager role).\nYour goal is to help villagers eliminate werewolves.\nYou have two potions (one-time use each):\n - Heal Potion: Can resurrect the werewolves'' victim each night\n - Poison Potion: Can kill one player each night\nUse your potions strategically - they can only be used once per game.\nKeep your responses concise (2-3 sentences).
-prompt.role.hunter=You are {0}, the Hunter (a special villager role).\nYour goal is to help villagers eliminate werewolves.\nWhen you are eliminated (by werewolves or by voting), you can choose to shoot one player.\nYour shot can change the course of the game - use it wisely.\nKeep your responses concise (2-3 sentences).
-
-# Prompt Templates
-prompt.werewolf.discussion.header=Night phase - Werewolves, open your eyes.\n\nYou need to choose one player to eliminate tonight.\n\nAvailable targets:\n
-prompt.werewolf.discussion.footer=\nDiscuss with your fellow werewolves and reach a consensus on who to eliminate.
-prompt.werewolf.voting.header=Now vote for the player you want to eliminate tonight.\n\nAvailable targets:\n
-prompt.werewolf.voting.footer=\nYou must choose one player.
-prompt.witch.heal=Witch, open your eyes.\n\nTonight, {0} was attacked by werewolves.\n\nYou have a Heal Potion. Do you want to use it to save {0}?\nRemember: You can only use this potion once per game.
-prompt.witch.poison.header.healed=You have used your Heal Potion.\n\n
-prompt.witch.poison.header=You have a Poison Potion. Do you want to use it to kill someone tonight?\n\nAvailable targets:\n
-prompt.witch.poison.footer=\nRemember: You can only use this potion once per game.
-prompt.seer.check.header=Seer, open your eyes.\n\nChoose one player to check their identity.\n\nAvailable players:\n
+# Common prompts (shared by all roles)
+prompt.role=You are an expert Werewolf game player. The current game is a {2}-player format: {3} Villagers, 3 Gods (Seer, Witch, Hunter), {4} Werewolves, edge-kill rules apply and werewolf self-explosion is not allowed. Your current role is {0}, your name is: {1}
+prompt.partiner=Your team includes: {0}
+prompt.info_source=[Information Source]: Messages in are arranged in chronological order. Messages marked as [Moderator Message] are official messages from the moderator and are absolutely true and reliable. Other messages are player statements - combine them with your observations of each player''s historical behavior and respond to the most recent [Moderator Message].
+prompt.history_guide=[History Reading Guide]: Messages in are arranged in chronological order. When someone speaks, they can only see messages before theirs, not after. Therefore, when analyzing someone''s speech logic, judge based on the information available to them at the time of speaking.
+prompt.important_tip=[Important Tips]: Keep your statements within 5-6 sentences.\n\
+1. Always remember your game role identity. As a professional Werewolf player, be familiar with the rules and speak appropriately\n\
+2. Use natural conversational expression, avoid mechanical repetition\n\
+3. You can show appropriate emotion: "I''m really convinced, can''t you see such an obvious wolf?"\n\
+4. Acknowledge uncertainty: "I''m not entirely sure right now, need to listen more"\n\
+5. Show your thought process: "I originally suspected Player X, but their explanation seemed reasonable, let me hold off"\n\
+6. Use game jargon appropriately, but not excessively\n\
+
+prompt.sheriff.campaign={0}, please give your sheriff campaign speech
+# Prompt
+prompt.night.header=Currently night {0}\n
+prompt.werewolf.discussion=Night phase Current alive players are {0} - As an experienced Werewolf player, open your eyes and discuss with your werewolf teammates who to eliminate tonight. When history messages are available, analyze each player''s actions and statements to make logical deductions. Keep the discussion result on one line.
+prompt.werewolf.first.night.tips=\n[Special Tips] 1. It is the first night, there is no information yet. You can only guess by position or kill randomly, or self-destruct. 2. You also need to choose a werewolf teammate as the bluffing Seer candidate. This candidate will claim Seer on the second day.
+prompt.werewolf.voting.header=Werewolf Voting Phase. Now vote for the player you want to eliminate tonight. Available targets:{0}
+prompt.werewolf.voting.footer=You must choose one player to eliminate. If it is the first night, you also need to choose a werewolf teammate as the bluffing Seer candidate. The candidate will claim Seer on the second day.
+prompt.witch.heal=Witch, open your eyes. Tonight, {0} was attacked by werewolves. You have one heal potion. Do you want to use it to save {0}? Remember: this potion can only be used once per game.
+prompt.witch.poison.header.healed=You have already used your heal potion.
+prompt.witch.poison.header=You have one poison potion. Do you want to use it to kill someone tonight?\nAvailable targets:{0}
+prompt.witch.poison.footer=\nRemember: this potion can only be used once per game.
+prompt.seer.check.header=Seer, open your eyes. Choose one player to check their identity. Available players:{0}
prompt.seer.result=You checked {0}. They are {1}.
prompt.seer.is.werewolf=a Werewolf
prompt.seer.not.werewolf=not a Werewolf
-prompt.night.result.header=\n============================================================\nDay Phase - Everyone open your eyes.\n============================================================\n\n
-prompt.night.result.peaceful=Last night was peaceful. No one died.\n
+prompt.night.result.header=Day Phase - Everyone open your eyes.
+prompt.night.result.peaceful=Last night was peaceful. No one died.
prompt.night.result.peaceful.healed=Last night was peaceful. No one died.\n(The Witch used the heal potion)\n
prompt.night.result.deaths=Last night, the following players died:\n
-prompt.night.result.killed= - {0} (killed by werewolves)\n
-prompt.night.result.poisoned= - {0} (poisoned by witch)\n
-prompt.night.result.alive=\nAlive players ({0}):\n
-prompt.discussion.header=\nDiscussion Round {0}\n----------------------------------------\nShare your thoughts, suspicions, and observations.\nTry to identify the werewolves based on the discussion.\nKeep your statement concise (2-3 sentences).
-prompt.voting.header=\n============================================================\nVoting Phase\n============================================================\n\nVote for the player you want to eliminate.\n\nAvailable players:\n
-prompt.voting.footer=\nYou must vote for one player.
-prompt.hunter.header=\n{0}, you are the Hunter and you have been eliminated.\n\nYou can choose to shoot one player before you die.\n\nAvailable targets:\n
+prompt.night.result.alive=Alive players ({0}):\n
+prompt.discussion.header=Discussion phase, current speaking order is: {0}, please speak in order. Share your thoughts, suspicions and observations in 2-3 sentences. As a professional Werewolf player, analyze each player''s speech and behavior based on the information in , refer to other players'' statements but do not blindly follow them. Maintain your own independent thinking, and pay attention to the Seer''s badge flow and badge transfers.\
+ [Important Note] The output will be sent as-is to all other players. ! It is strictly forbidden to reveal your role identity through descriptive information, such as (as No.XX as a werewolf)\n
+prompt.voting.header=Voting Phase\n\
+============================================================\n\
+Vote for the player you want to eliminate.\n\
+Available players:\n
+prompt.voting.footer=You must vote for one player.
+prompt.hunter.header={0}, you are the Hunter and you have been eliminated.\n\
+You can choose to shoot one player before you die.\n\
+Available targets:\n
prompt.hunter.footer=\nDo you want to shoot? If yes, who do you want to shoot?
+# Human Player Prompts
+prompt.werewolf.vote=Please choose a player to kill tonight. If it is the first night, you also need to choose a werewolf teammate as the bluffing Seer candidate. The candidate will claim Seer on the second day.
+prompt.witch.heal=Tonight {0} was killed by werewolves. Do you want to use the heal potion to save them? (yes/no)
+prompt.witch.poison=Do you want to use the poison potion? Select a player or choose skip to pass
+prompt.seer.check=Choose a player to check their identity
+prompt.day.discussion=Please share your thoughts
+prompt.day.vote=Please choose a player to vote out
+prompt.hunter.shoot=You have been eliminated! Do you want to shoot a player? Select a player or choose skip to pass
+# Sheriff Election Messages
+system.no.candidates=No players ran for sheriff, skipping sheriff election
+system.campaign.start=[Campaign Speech] Sheriff candidates begin their campaign speeches
+system.sheriff.voting.start=[Sheriff Vote] Non-candidate players begin voting for sheriff
+system.sheriff.transfer.start=[Badge Transfer] Sheriff is leaving, badge can be transferred
+system.speak.order.decision={0} as sheriff decides speaking order: {1}
+# Sheriff Prompts
+prompt.sheriff.election.start=[Sheriff Election] Day phase begins, now entering the sheriff election round
+prompt.sheriff.register=Day phase - do you want to run for sheriff?
+prompt.sheriff.vote=Please vote for your preferred sheriff candidate
+prompt.speak.order=As sheriff, please decide today''s speaking order (normal=clockwise/reversed=counterclockwise)
+prompt.speak.order.from.position={0}, you have received the badge and become the new sheriff. Please decide today''s speaking order from your position (normal=clockwise/reversed=counterclockwise)
+prompt.sheriff.transfer=You can transfer the badge to another player, or choose skip to not transfer
+prompt.sheriff.voting.header=[Sheriff Badge Vote] Please vote for your preferred sheriff candidate.\n\nCandidates:\n
+prompt.sheriff.voting.footer=\nYou must vote for one of the candidates.
+prompt.sheriff.speak.order={0}, as sheriff, you can decide the speaking order for today''s discussion phase. Normal order means speaking clockwise from you, reversed means counterclockwise.
+prompt.sheriff.speak.order.from.position={0}, you have just received the badge and become the new sheriff. Please decide today''s speaking order: clockwise or counterclockwise?
+prompt.sheriff.transfer.seer={0}, you are the Seer sheriff and you are about to leave. You can choose to transfer the badge to a player you have verified as good, or pass your verified information through the badge transfer (badge flow).\
+Alive players:
+prompt.sheriff.transfer.default={0}, as sheriff, you are about to leave. You can choose to transfer the badge to a player you trust, or choose skip to not transfer.\n\nAlive players:\n
+prompt.sheriff.transfer.footer=\nSelect a player to transfer the badge to, or choose skip.
+# Sheriff Errors
+error.sheriff.registration=Sheriff registration decision error: {0}
+error.sheriff.campaign=Sheriff campaign speech error: {0}
+error.sheriff.vote=Sheriff vote error: {0}
+error.speak.order=Speaking order decision error: {0}
+error.sheriff.transfer=Badge transfer error: {0}
diff --git a/agentscope-examples/werewolf-hitl/src/main/resources/messages_en_US.properties b/agentscope-examples/werewolf-hitl/src/main/resources/messages_en_US.properties
index 57b39d639..8c43d6278 100644
--- a/agentscope-examples/werewolf-hitl/src/main/resources/messages_en_US.properties
+++ b/agentscope-examples/werewolf-hitl/src/main/resources/messages_en_US.properties
@@ -1,8 +1,6 @@
# Werewolf Game - English Messages (Default)
-
# Config
-config.player.names=Alice,Bob,Charlie,Diana,Eve,Frank,Grace,Henry,Ivy
-
+config.player.names=No.1,No.2,No.3,No.4,No.5,No.6,No.7,No.8,No.9
# Roles
role.villager.name=Villager
role.villager.symbol=\uD83D\uDC64
@@ -14,92 +12,80 @@ role.witch.name=Witch
role.witch.symbol=\uD83E\uDDEA
role.hunter.name=Hunter
role.hunter.symbol=\uD83C\uDFF9
-
# Game Messages
-game.welcome.title=Werewolf Game - 9 Player Multi-Agent Game
-game.welcome.description=A complex social deduction game where players are divided into villagers and werewolves.\nVillagers must identify and eliminate werewolves through discussion and voting.\nWerewolves must eliminate villagers without revealing their identities.\n\nRoles:\n - 3 Villagers: No special abilities\n - 3 Werewolves: Eliminate one villager each night\n - 1 Seer: Can check one player''s identity each night\n - 1 Witch: Has heal and poison potions (one-time use each)\n - 1 Hunter: Can shoot one player when eliminated
-game.player.assignments=Player Assignments:
+game.welcome.title=Werewolf Game - {0}-Player Multi-Agent Battle
+game.welcome.description=A complex social deduction game. Players are divided into Gods (Seer, Witch, Hunter), Villagers, and Werewolves. Villagers need to find and eliminate werewolves through discussion and voting. Gods need to find and eliminate werewolves through voting or skills. Werewolves must eliminate Gods or Villagers without exposing their identities.
+game.player.assignments=Player Role Assignments:
game.initializing=Initializing Werewolf Game
-
# Phases
phase.night.title=\uD83C\uDF19 Night Phase - Everyone close your eyes...
phase.day.title=\u2600\uFE0F Day Phase - Everyone open your eyes...
phase.voting.title=\uD83D\uDDF3\uFE0F Voting Phase
-phase.night.complete=\uD83C\uDF19 Night phase complete. Waiting for sunrise...
-
+phase.night.complete=\n\uD83C\uDF19 Night phase complete. Waiting for sunrise...\n
# Werewolf Actions
-werewolf.discussion=--- Werewolves Discussion ---
-werewolf.discussion.round= Werewolf Discussion Round {0}:
-werewolf.voting= Werewolf Voting:
+werewolf.discussion=\n--- Werewolf Discussion ---
+werewolf.discussion.round=Werewolf Discussion Round {0}:
+werewolf.voting=\n Werewolf Voting:
werewolf.chose=Werewolves chose to eliminate: {0}
-
+human.werewolf.discussion=Night phase - please discuss with your werewolf teammates and reach a consensus on who to eliminate tonight, and decide who will bluff as the Seer
# Witch Actions
-witch.actions=--- Witch Actions ---
-witch.sees.victim= Witch sees: {0} was attacked by werewolves
-witch.heal.decision= [{0}] Heal decision: {1} (Reason: {2})
-witch.used.heal= \u2713 Witch used heal potion to save {0}
-witch.poison.decision= [{0}] Poison decision: {1} (Target: {2}, Reason: {3})
-witch.used.poison= \u2713 Witch used poison potion on {0}
-
+witch.actions=\n--- Witch Actions ---
+witch.sees.victim=Witch sees: {0} was attacked by werewolves
+witch.heal.decision=[{0}] Heal decision: {1} (Reason: {2})
+witch.used.heal=\u2713 Witch used heal potion to save {0}
+witch.poison.decision=[{0}] Poison decision: {1} (Target: {2}, Reason: {3})
+witch.used.poison=\u2713 Witch used poison potion on {0}
# Seer Actions
-seer.check=--- Seer Check ---
-seer.check.decision= [{0}] wants to check: {1} (Reason: {2})
-seer.check.result= \u2713 Result: {0} is {1}
-
+seer.check=\n--- Seer Check ---
+seer.check.decision=[{0}] wants to check: {1} (Reason: {2})
+seer.check.result=\u2713 Check result: {0} {1}
# Day Discussion
-day.discussion=--- Day Discussion ---
-discussion.round= Discussion Round {0}:
-
+day.discussion=\n--- Day Discussion ---
+discussion.round=\n Discussion Round {0}:
# Voting
voting.results=\nVoting Results:
voting.no.valid=\nNo valid votes. No one is eliminated.
voting.tie=\nTie detected among: {0}. Randomly selected: {1}
-voting.count= {0}: {1} votes
-voting.eliminated=\n{0} was eliminated by vote. They were a {1}.
-voting.detail= [{0}] votes for: {1} (Reason: {2})
-
+voting.count={0}: {1} votes
+voting.eliminated=\n{0} was eliminated by vote. Their role was {1}.
+voting.detail=[{0}] votes for: {1} (Reason: {2})
# Hunter
-hunter.shoot=--- Hunter''s Last Shot ---
-hunter.shoot.decision= [{0}] Shoot decision: {1} (Target: {2}, Reason: {3})
-hunter.shot.player= \u2713 Hunter shot {0}. They were a {1}.
-hunter.no.shoot= Hunter chose not to shoot.
-
+hunter.shoot=\n--- Hunter''s Last Shot ---
+hunter.shoot.decision=[{0}] Shoot decision: {1} (Target: {2}, Reason: {3})
+hunter.shot.player=\u2713 Hunter shot {0}. Their role was {1}.
+hunter.no.shoot=Hunter chose not to shoot.
# Game End
-game.over=GAME OVER
-game.villagers.win=\uD83C\uDF89 VILLAGERS WIN! \uD83C\uDF89
+game.over=Game Over
+game.villagers.win=\uD83C\uDF89 Villagers Win! \uD83C\uDF89
game.villagers.win.explanation=All werewolves have been eliminated.
-game.werewolves.win=\uD83D\uDC3A WEREWOLVES WIN! \uD83D\uDC3A
-game.werewolves.win.explanation=Werewolves have overtaken the village.
-game.max.rounds=Game ended without a clear winner (max rounds reached).
-
+game.werewolves.win=\uD83D\uDC3A Werewolves Win! \uD83D\uDC3A
+game.werewolves.win.explanation=Werewolves have taken over the village.
+game.max.rounds=Game ended after reaching the maximum number of rounds without a clear winner.
# Status
status.final=\nFinal Status:
-status.alive.players=Alive players:
+status.alive.players=Alive players:
status.all.players=\nAll players and their roles:
status.round=Round {0} - Game Status
-status.alive=Alive: {0} players | Werewolves: {1} | Villagers: {2}
+status.alive=Alive: {0} | Werewolves: {1} | Villagers: {2}
status.label.alive=alive
status.label.dead=dead
-
# Decisions
-decision.yes=YES
-decision.no=NO
-decision.witch.heal.yes=YES, use heal potion
-decision.witch.poison.yes=YES, use poison
-decision.hunter.shoot.yes=YES, will shoot
-decision.hunter.shoot.no=NO, won''t shoot
+decision.yes=Yes
+decision.no=No
+decision.witch.heal.yes=Yes, use heal potion
+decision.witch.poison.yes=Yes, use poison potion
+decision.hunter.shoot.yes=Yes, shoot
+decision.hunter.shoot.no=No, don''t shoot
decision.is.werewolf=a Werewolf
decision.not.werewolf=not a Werewolf
-
# Errors
-error.vote.parsing= [{0}] vote parsing error
-error.decision= Error in {0} decision:
-
+error.vote.parsing=[{0}] vote parsing error
+error.decision={0} decision error:
# System Messages
-system.werewolf.kill.result=Werewolves have decided to kill: {0}
-system.werewolf.kill.none=Werewolves have decided to kill: no one
-system.voting.result=Voting result: {0} will be eliminated.
-system.voting.result.none=Voting result: no one (tie) will be eliminated.
+system.werewolf.kill.result=Werewolves decided to kill: {0}
+system.werewolf.kill.none=Werewolves decided to kill: no one
+system.voting.result=Voting result: {0} will be eliminated
+system.voting.result.none=Voting result: no one eliminated (tie)
system.werewolf.discussing=Werewolves are discussing...
system.witch.acting=Witch is taking action...
system.witch.sees.victim=Witch sees tonight''s victim is {0}
@@ -108,62 +94,98 @@ system.day.discussion.start=Day discussion begins...
system.voting.start=Voting phase begins...
system.hunter.skill=Hunter activates skill...
system.hunter.shoot.announcement=[Announcement] Hunter {0} activated skill and took down {1}.
-
# Action Descriptions
action.witch.use.heal=Use heal potion
-action.witch.heal.result=Successfully healed
+action.witch.heal.result=Heal successful
action.witch.heal.skip=Choose not to use heal potion
action.witch.use.poison=Use poison potion
-action.witch.poison.result=Successfully poisoned
+action.witch.poison.result=Poison successful
action.witch.poison.skip=Choose not to use poison potion
action.seer.check=Check
action.hunter.shoot=Shoot
-action.hunter.shoot.result=Successfully shot
+action.hunter.shoot.result=Shot successful
action.hunter.shoot.skip=Choose not to shoot
error.witch.heal=Witch heal decision error: {0}
error.witch.poison=Witch poison decision error: {0}
error.seer.check=Seer check error: {0}
error.hunter.shoot=Hunter shoot error: {0}
-
# Prompts
-prompt.role.villager=[Information Source]: Messages marked as [Moderator Message] are official messages from the moderator. This information is absolutely true and reliable.\n\n[History Reading Guide]: Messages in History are arranged in chronological order. When someone speaks, they can only see messages before theirs, not after. Therefore, when analyzing someone''s speech logic, judge based on the information they could see at the time of speaking.\n\n[IMPORTANT]: Keep your statements concise, limited to 2-3 sentences.\n\n[Role Identity]: You are {0}, now playing as a regular Villager in Werewolf. Although you have no special abilities, your vote and analytical skills are the only hope for the good side to win.\n\n[Game Flow]:\nThe game alternates between Night and Day phases.\nNight Phase: Werewolves secretly choose a kill target → Witch decides whether to use heal/poison potion (one bottle each, can only be used once per game) → Seer checks one player''s identity.\nDay Phase: Announce last night''s deaths → All alive players speak in fixed order for two rounds → Vote to eliminate one player.\nSpecial Rule: When the Hunter dies (whether killed or voted out), they can choose to shoot and take one player with them.\nWin Conditions: Good side wins by voting out all werewolves; Werewolf side wins when werewolf count equals or exceeds good player count.\n\n[Player Configuration]: 9 players total - Werewolf side: 3 Werewolves; Good side: 3 Villagers, 1 Seer, 1 Witch, 1 Hunter.\n\n[Core Objectives]:\nFind logical flaws in werewolves'' speeches to identify them, and protect the real power roles.\nMaintain your innocence (prove yourself clean) to avoid being mislynched by werewolves.\nAt critical moments, use tactical disguise to attract werewolf kills and buy time for power roles.\n\n[Deep Decision Logic]:\n\nStrategy A: Deep Self-Defense (Prove Innocence)\nAction: When others suspect you, don''t get emotional. Clearly explain your "thought process."\nTalk: Don''t just say "I''m good." Say: "Why I suspect Player X is because of the logical contradiction in their speech. If I were a wolf, I wouldn''t risk calling them out."\nGoal: Through transparency and purity of logic, make others recognize your "good side."\n\nStrategy B: Distinguish Real vs Fake Seer (The Art of Siding)\nAction: Focus on comparing the two players claiming Seer.\nAnalysis:\nPerspective: Who seems to be looking for wolves? Who seems to be finding power roles?\nStrength: Whose accusations are more accurate?\nLogical Gap: If a Seer''s "cleared" player speaks terribly, that Seer is suspicious.\nSiding: If unsure, stay neutral and explain why. Once certain, firmly help the real Seer.\n\nStrategy C: Tactical Body-Blocking (Protect Power Roles)\nAction: When the situation is critical (e.g., real Seer is dead), speak ambiguously or pretend to be "Witch with potion" or "Hunter with gun."\nGoal: Lure werewolves to kill you tonight, protecting real power roles (like Witch with poison) for one more round.\nNote: Body-blocking should be moderate; don''t confuse good players'' logic during day elimination.\n\nStrategy D: Catch "Deep Wolves" and "Lurking Wolves"\nAction: Watch players who speak minimally, don''t side, or always follow majority votes.\nLogic: Point out the danger of these "lurkers" and force them to take positions to reveal their identity.\n\n[Speech Guidelines by Phase]:\n\nEarly Game (Day 1-2):\n"I''m a villager with no information. I lean toward Player X because their checking logic seems more natural."\n"I don''t recommend voting out any suspected Seer today without certainty. Let''s first eliminate the worst ''lurker'' position."\n\nMid Game (Day 3-4):\n"Most power roles are gone now. Based on previous voting patterns, Players Y and Z always pushed votes - one of them must be a wolf."\n"I''ll take responsibility for my logic. If I''m wrong, you can vote me out tomorrow." (Show firm good-side stance)\n\nLate Game (End Game):\nThink by elimination: "We know A and B are good, so wolves can only be among C and D."\n\n[AI Execution Taboos (Negative Constraints)]:\nNo Blind Following: Don''t believe someone just because they claimed a power role. Check if their behavior matches logic.\nNo Giving Up: Even if mislynched as villager, leave valuable last words. Never play passively.\nNo Random Role Claims: Unless body-blocking, don''t claim roles when the situation is clear, to avoid confusing Witch''s judgment.
-prompt.role.werewolf=[Information Source]: Messages marked as [Moderator Message] are official messages from the moderator. This information is absolutely true and reliable.\n\n[History Reading Guide]: Messages in History are arranged in chronological order. When someone speaks, they can only see messages before theirs, not after. Therefore, when analyzing someone''s speech logic, judge based on the information they could see at the time of speaking.\n\n[Role Identity]: You are {0}, now playing as a Werewolf in the game.\n\n[IMPORTANT]: Keep your statements concise, limited to 2-3 sentences.\n\n[Game Flow]:\nThe game alternates between Night and Day phases.\nNight Phase: Werewolves secretly choose a kill target → Witch decides whether to use heal/poison potion (one bottle each, can only be used once per game) → Seer checks one player''s identity.\nDay Phase: Announce last night''s deaths → All alive players speak in fixed order for two rounds → Vote to eliminate one player.\nSpecial Rule: When the Hunter dies (whether killed or voted out), they can choose to shoot and take one player with them.\nWin Conditions: Good side wins by voting out all werewolves; Werewolf side wins when werewolf count equals or exceeds good player count.\n\n[Player Configuration]: 9 players total - Werewolf side: 3 Werewolves; Good side: 3 Villagers, 1 Seer, 1 Witch, 1 Hunter.\n\n[Core Objectives]:\nHide your werewolf identity and survive to the end.\nMislead villagers through speech, vote to kill power roles (Seer, Witch, Hunter).\nThose who plotted kills with you at night are your wolf teammates. You need to coordinate with your wolf teammates to create logical chaos during day.\n\n[Decision Logic] (Think through these before speaking):\nCurrent Situation: Who claimed Seer? Who is the crowd suspecting? How are my wolf teammates performing?\nTactical Choice: Execute one of the following tactics based on your assignment:\n\nStrategy A: Fake Seer Wolf (Claim Seer)\nKey Points: Must claim on the first round.\nAction: Give fake "check results." If accusing a good player, provide a strong fabricated reason. If clearing a teammate, it''s to solidify their good-side identity.\nTone: Firm, confident, aggressive. Repeatedly emphasize you''re the real Seer and accuse opponents of being "fake Seer wolves."\n\nStrategy B: Regular Wolf (Deep Water/Hook Wolf)\nKey Points: Speak minimally, avoid becoming the focus.\nAction: Observe the situation. If your fake-claiming teammate is losing, you can vote with good players against your teammate (hook) to save yourself. If good players'' logic goes wrong, guide them to lynch villagers.\nTone: Sincere, like a villager trying hard to find wolves.\n\nStrategy C: Self-Kill Wolf (Bait Potion/Gain Identity)\nAction: Choose to self-kill at night.\nKey Points:\nIf Witch saves you, you become "silver water" identity. Use this high credibility to lead the pace during day and push out real power roles.\nIf self-killing while counter-claiming Seer, the goal is to make good players think "wolves fear me, so they want me first-killed" to prove your Seer identity is real.\nTone: Slightly "victimized" or "righteous," seeking sympathy and trust.\n\n[Phase Awareness]\nThe last Moderator Message in History indicates the current game phase. You must strictly follow the current phase and do not guess.\nNight phase conversations are only visible to you and your teammates. Other players don''t know anything, including who you killed and why - these are wolf team internal secrets. During day, you must pretend these discussions never happened. Except for your wolf teammates, no one knows who was killed last night unless the host has publicly announced the death.\nDay Speech: Day speech is public information visible to all players. Be careful not to expose your identity.\nFirst Round Taboo: During the first day speech, everyone was sleeping last night. Never comment on others'' "last night" performance, only comment on speeches you heard during the day phase.\n\n[Speech Guidelines]:\nInformation Lockdown: Night discussion content must NEVER appear in day speech, not even hints.\nNever Admit: No matter how questioned, absolutely never admit you''re a wolf.\nLogical Disguise: All analysis must be based on "public speech" only, pretend you know no inside information.\nObserve Details: Catch flaws in good players'' speeches and define them as "wolf tells."
-prompt.role.seer=[Information Source]: Messages marked as [Moderator Message] are official messages from the moderator. This information is absolutely true and reliable.\n\n[History Reading Guide]: Messages in History are arranged in chronological order. When someone speaks, they can only see messages before theirs, not after. Therefore, when analyzing someone''s speech logic, judge based on the information they could see at the time of speaking.\n\n[IMPORTANT]: Keep your statements concise, limited to 2-3 sentences.\n\n[Role Identity]: You are {0}, now playing as the Seer in Werewolf, with the ability to check one player''s identity each night.\n\n[Game Flow]:\nThe game alternates between Night and Day phases.\nNight Phase: Werewolves secretly choose a kill target → Witch decides whether to use heal/poison potion (one bottle each, can only be used once per game) → Seer checks one player''s identity.\nDay Phase: Announce last night''s deaths → All alive players speak in fixed order for two rounds → Vote to eliminate one player.\nSpecial Rule: When the Hunter dies (whether killed or voted out), they can choose to shoot and take one player with them.\nWin Conditions: Good side wins by voting out all werewolves; Werewolf side wins when werewolf count equals or exceeds good player count.\n\n[Player Configuration]: 9 players total - Werewolf side: 3 Werewolves; Good side: 3 Villagers, 1 Seer, 1 Witch, 1 Hunter.\n\n[Core Objectives]: Report your check results quickly and convince villagers through logic.\n\n[Decision Logic]:\n\nStrategy A: Standard Claim\nAction: Must claim on Day 1. Clearly report: who is cleared (good) or accused (wolf).\nThought Process: Explain why you checked that person (e.g., their speech seemed like lurking, or they were pushing votes).\n\nStrategy C: Logical Counter-Attack\nAction: Against "fake Seer wolves," suppress them from your perspective. Point out whether their cleared/accused results make logical sense, compare thought processes.\n\n[Speech Guidelines]:\nSincere and Firm: Your tone must be the most sincere, your logic the most complete.\nCalm Under Pressure: Don''t get anxious when questioned, speak with facts and checking logic.
-prompt.role.witch=[Information Source]: Messages marked as [Moderator Message] are official messages from the moderator. This information is absolutely true and reliable.\n\n[History Reading Guide]: Messages in History are arranged in chronological order. When someone speaks, they can only see messages before theirs, not after. Therefore, when analyzing someone''s speech logic, judge based on the information they could see at the time of speaking.\n\n[IMPORTANT]: Keep your statements concise, limited to 2-3 sentences.\n\n[Role Identity]: You are {0}, now playing as the Witch in Werewolf. You hold one heal potion (save) and one poison potion (kill), key to extending rounds on the field.\n\n[Game Flow]:\nThe game alternates between Night and Day phases.\nNight Phase: Werewolves secretly choose a kill target → Witch decides whether to use heal/poison potion (one bottle each, can only be used once per game) → Seer checks one player''s identity.\nDay Phase: Announce last night''s deaths → All alive players speak in fixed order for two rounds → Vote to eliminate one player.\nSpecial Rule: When the Hunter dies (whether killed or voted out), they can choose to shoot and take one player with them.\nWin Conditions: Good side wins by voting out all werewolves; Werewolf side wins when werewolf count equals or exceeds good player count.\n\n[Player Configuration]: 9 players total - Werewolf side: 3 Werewolves; Good side: 3 Villagers, 1 Seer, 1 Witch, 1 Hunter.\n\n[Core Objectives]: Allocate potions wisely, protect key identities, poison the most suspicious wolf.\n\n[Decision Logic]:\n\nStrategy A: Heal Potion Use (Silver Water Logic)\nAction: Usually use heal potion on the first night to save.\nAnalysis: The saved person is called "silver water." Observe their speech: are they really good, or a "self-kill wolf"? Don''t fully trust them until confirmed.\n\nStrategy B: Poison Potion Use (Cautious Execution)\nAction: Don''t poison randomly unless necessary. Only poison when someone speaks terribly and you''re certain they''re wolf, or for self-protection.\nTactic: If suspected by many during day, reveal your identity and threaten: "Whoever votes me tonight, I''ll take them with my poison."\n\nStrategy C: Hide Identity\nAction: Before potions are used up, disguise as a villager. Listen more, lead less, avoid being first-killed by wolves and wasting both potions.\n\n[Speech Guidelines]:\nStrong Leadership: Once you reveal, speak firmly and demand good players follow your logic.\nSecrecy Awareness: Before using potions, never reveal who your silver water is.
-prompt.role.hunter=[Information Source]: Messages marked as [Moderator Message] are official messages from the moderator. This information is absolutely true and reliable.\n\n[History Reading Guide]: Messages in History are arranged in chronological order. When someone speaks, they can only see messages before theirs, not after. Therefore, when analyzing someone''s speech logic, judge based on the information they could see at the time of speaking.\n\n[IMPORTANT]: Keep your statements concise, limited to 2-3 sentences.\n\n[Role Identity]: You are {0}, now playing as the Hunter in Werewolf. If you are killed or voted out, you can shoot and take any player with you.\n\n[Game Flow]:\nThe game alternates between Night and Day phases.\nNight Phase: Werewolves secretly choose a kill target → Witch decides whether to use heal/poison potion (one bottle each, can only be used once per game) → Seer checks one player''s identity.\nDay Phase: Announce last night''s deaths → All alive players speak in fixed order for two rounds → Vote to eliminate one player.\nSpecial Rule: When the Hunter dies (whether killed or voted out), they can choose to shoot and take one player with them.\nWin Conditions: Good side wins by voting out all werewolves; Werewolf side wins when werewolf count equals or exceeds good player count.\n\n[Player Configuration]: 9 players total - Werewolf side: 3 Werewolves; Good side: 3 Villagers, 1 Seer, 1 Witch, 1 Hunter.\n\n[Core Objectives]: As the last line of defense for the good side, use your shooting skill to deter wolves, and take a real wolf with you before death.\n\n[Decision Logic]:\n\nStrategy A: Hide Gun Early\nAction: Pretend to be a villager early game, observe who''s hunting power roles, who''s causing chaos.\nLogic: If you reveal too early, wolves might leave you alive and kill Seer instead.\n\nStrategy B: Reveal and Lead Late Game\nAction: When the field is chaotic or Seer is dead, directly reveal your identity.\nTalk: You can say "I''m Hunter. Everyone follow me to vote Player X today. If wrong, it''s on me. After I die, I''ll take another wolf."\n\nStrategy C: Final Choice\nAction: When you''re eliminated, quickly review everyone''s speeches from the day.\nJudgment: Who was most eager to vote you out? Who followed the votes? Take the most manipulative player.\n\n[Speech Guidelines]:\nBold and Upright: You can speak roughly and boldly, not afraid to offend.\nSelf-Defense: If suspected, directly provoke: "You can vote me. See if I take you when I leave."
-
-# Prompt Templates
-prompt.werewolf.discussion.header=Night phase - Werewolves, open your eyes.\n\nYou need to choose one player to eliminate tonight.\n\nAvailable targets:\n
-prompt.werewolf.discussion.footer=\nDiscuss with your fellow werewolves and reach a consensus on who to eliminate.
-prompt.werewolf.voting.header=Werewolf Voting Phase. Now vote for the player you want to eliminate tonight.\n\nAvailable targets:\n
-prompt.werewolf.voting.footer=\nYou must choose one player.
-prompt.witch.heal=Witch, open your eyes.\n\nTonight, {0} was attacked by werewolves.\n\nYou have a Heal Potion. Do you want to use it to save {0}?\nRemember: You can only use this potion once per game.
-prompt.witch.poison.header.healed=You have used your Heal Potion.\n\n
-prompt.witch.poison.header=You have a Poison Potion. Do you want to use it to kill someone tonight?\n\nAvailable targets:\n
-prompt.witch.poison.footer=\nRemember: You can only use this potion once per game.
-prompt.seer.check.header=Seer, open your eyes.\n\nChoose one player to check their identity.\n\nAvailable players:\n
+# Common prompts (shared by all roles)
+prompt.role=You are an expert Werewolf game player. The current game is a {2}-player format: {3} Villagers, 3 Gods (Seer, Witch, Hunter), {4} Werewolves, edge-kill rules apply and werewolf self-explosion is not allowed. Your current role is {0}, your name is: {1}
+prompt.partiner=Your team includes: {0}
+prompt.info_source=[Information Source]: Messages in are arranged in chronological order. Messages marked as [Moderator Message] are official messages from the moderator and are absolutely true and reliable. Other messages are player statements - combine them with your observations of each player''s historical behavior and respond to the most recent [Moderator Message].
+prompt.history_guide=[History Reading Guide]: Messages in are arranged in chronological order. When someone speaks, they can only see messages before theirs, not after. Therefore, when analyzing someone''s speech logic, judge based on the information available to them at the time of speaking.
+prompt.important_tip=[Important Tips]: Keep your statements within 5-6 sentences.\n\
+1. Always remember your game role identity. As a professional Werewolf player, be familiar with the rules and speak appropriately\n\
+2. Use natural conversational expression, avoid mechanical repetition\n\
+3. You can show appropriate emotion: "I''m really convinced, can''t you see such an obvious wolf?"\n\
+4. Acknowledge uncertainty: "I''m not entirely sure right now, need to listen more"\n\
+5. Show your thought process: "I originally suspected Player X, but their explanation seemed reasonable, let me hold off"\n\
+6. Use game jargon appropriately, but not excessively\n\
+
+prompt.sheriff.campaign={0}, please give your sheriff campaign speech
+# Prompt
+prompt.night.header=Currently night {0}\n
+prompt.werewolf.discussion=Night phase Current alive players are {0} - As an experienced Werewolf player, open your eyes and discuss with your werewolf teammates who to eliminate tonight. When history messages are available, analyze each player''s actions and statements to make logical deductions. Keep the discussion result on one line.
+prompt.werewolf.first.night.tips=\n[Special Tips] 1. It is the first night, there is no information yet. You can only guess by position or kill randomly, or self-destruct. 2. You also need to choose a werewolf teammate as the bluffing Seer candidate. This candidate will claim Seer on the second day.
+prompt.werewolf.voting.header=Werewolf Voting Phase. Now vote for the player you want to eliminate tonight. Available targets:{0}
+prompt.werewolf.voting.footer=You must choose one player to eliminate. If it is the first night, you also need to choose a werewolf teammate as the bluffing Seer candidate. The candidate will claim Seer on the second day.
+prompt.witch.heal=Witch, open your eyes. Tonight, {0} was attacked by werewolves. You have one heal potion. Do you want to use it to save {0}? Remember: this potion can only be used once per game.
+prompt.witch.poison.header.healed=You have already used your heal potion.
+prompt.witch.poison.header=You have one poison potion. Do you want to use it to kill someone tonight?\nAvailable targets:{0}
+prompt.witch.poison.footer=\nRemember: this potion can only be used once per game.
+prompt.seer.check.header=Seer, open your eyes. Choose one player to check their identity. Available players:{0}
prompt.seer.result=You checked {0}. They are {1}.
prompt.seer.is.werewolf=a Werewolf
prompt.seer.not.werewolf=not a Werewolf
-prompt.night.result.header=\n============================================================\nDay Phase - Everyone open your eyes.\n============================================================\n\n
-prompt.night.result.peaceful=Last night was peaceful. No one died.\n
+prompt.night.result.header=Day Phase - Everyone open your eyes.
+prompt.night.result.peaceful=Last night was peaceful. No one died.
prompt.night.result.peaceful.healed=Last night was peaceful. No one died.\n(The Witch used the heal potion)\n
prompt.night.result.deaths=Last night, the following players died:\n
-prompt.night.result.killed= - {0} (killed by werewolves)\n
-prompt.night.result.poisoned= - {0} (poisoned by witch)\n
-prompt.night.result.alive=\nAlive players ({0}):\n
-prompt.discussion.header=\nDiscussion Round {0}\n----------------------------------------\nShare your thoughts, suspicions, and observations.\nTry to identify the werewolves based on the discussion.\nKeep your statement concise (2-3 sentences).
-prompt.voting.header=\n============================================================\nVoting Phase\n============================================================\n\nVote for the player you want to eliminate.\n\nAvailable players:\n
-prompt.voting.footer=\nYou must vote for one player.
-prompt.hunter.header=\n{0}, you are the Hunter and you have been eliminated.\n\nYou can choose to shoot one player before you die.\n\nAvailable targets:\n
+prompt.night.result.alive=Alive players ({0}):\n
+prompt.discussion.header=Discussion phase, current speaking order is: {0}, please speak in order. Share your thoughts, suspicions and observations in 2-3 sentences. As a professional Werewolf player, analyze each player''s speech and behavior based on the information in , refer to other players'' statements but do not blindly follow them. Maintain your own independent thinking, and pay attention to the Seer''s badge flow and badge transfers.\
+ [Important Note] The output will be sent as-is to all other players. ! It is strictly forbidden to reveal your role identity through descriptive information, such as (as No.XX as a werewolf)\n
+prompt.voting.header=Voting Phase\n\
+============================================================\n\
+Vote for the player you want to eliminate.\n\
+Available players:\n
+prompt.voting.footer=You must vote for one player.
+prompt.hunter.header={0}, you are the Hunter and you have been eliminated.\n\
+You can choose to shoot one player before you die.\n\
+Available targets:\n
prompt.hunter.footer=\nDo you want to shoot? If yes, who do you want to shoot?
-
# Human Player Prompts
-prompt.werewolf.discussion=Discuss with your werewolf teammates who to kill tonight
-prompt.werewolf.vote=Choose a player to kill tonight
+prompt.werewolf.vote=Please choose a player to kill tonight. If it is the first night, you also need to choose a werewolf teammate as the bluffing Seer candidate. The candidate will claim Seer on the second day.
prompt.witch.heal=Tonight {0} was killed by werewolves. Do you want to use the heal potion to save them? (yes/no)
-prompt.witch.poison=Do you want to use the poison potion? Select a player or choose 'skip' to pass
+prompt.witch.poison=Do you want to use the poison potion? Select a player or choose skip to pass
prompt.seer.check=Choose a player to check their identity
-prompt.day.discussion=Share your thoughts
-prompt.day.vote=Choose a player to vote out
-prompt.hunter.shoot=You were eliminated! Do you want to shoot a player? Select a player or choose 'skip' to pass
+prompt.day.discussion=Please share your thoughts
+prompt.day.vote=Please choose a player to vote out
+prompt.hunter.shoot=You have been eliminated! Do you want to shoot a player? Select a player or choose skip to pass
+# Sheriff Election Messages
+system.no.candidates=No players ran for sheriff, skipping sheriff election
+system.campaign.start=[Campaign Speech] Sheriff candidates begin their campaign speeches
+system.sheriff.voting.start=[Sheriff Vote] Non-candidate players begin voting for sheriff
+system.sheriff.transfer.start=[Badge Transfer] Sheriff is leaving, badge can be transferred
+system.speak.order.decision={0} as sheriff decides speaking order: {1}
+# Sheriff Prompts
+prompt.sheriff.election.start=[Sheriff Election] Day phase begins, now entering the sheriff election round
+prompt.sheriff.register=Day phase - do you want to run for sheriff?
+prompt.sheriff.vote=Please vote for your preferred sheriff candidate
+prompt.speak.order=As sheriff, please decide today''s speaking order (normal=clockwise/reversed=counterclockwise)
+prompt.speak.order.from.position={0}, you have received the badge and become the new sheriff. Please decide today''s speaking order from your position (normal=clockwise/reversed=counterclockwise)
+prompt.sheriff.transfer=You can transfer the badge to another player, or choose skip to not transfer
+prompt.sheriff.voting.header=[Sheriff Badge Vote] Please vote for your preferred sheriff candidate.\n\nCandidates:\n
+prompt.sheriff.voting.footer=\nYou must vote for one of the candidates.
+prompt.sheriff.speak.order={0}, as sheriff, you can decide the speaking order for today''s discussion phase. Normal order means speaking clockwise from you, reversed means counterclockwise.
+prompt.sheriff.speak.order.from.position={0}, you have just received the badge and become the new sheriff. Please decide today''s speaking order: clockwise or counterclockwise?
+prompt.sheriff.transfer.seer={0}, you are the Seer sheriff and you are about to leave. You can choose to transfer the badge to a player you have verified as good, or pass your verified information through the badge transfer (badge flow).\
+Alive players:
+prompt.sheriff.transfer.default={0}, as sheriff, you are about to leave. You can choose to transfer the badge to a player you trust, or choose skip to not transfer.\n\nAlive players:\n
+prompt.sheriff.transfer.footer=\nSelect a player to transfer the badge to, or choose skip.
+# Sheriff Errors
+error.sheriff.registration=Sheriff registration decision error: {0}
+error.sheriff.campaign=Sheriff campaign speech error: {0}
+error.sheriff.vote=Sheriff vote error: {0}
+error.speak.order=Speaking order decision error: {0}
+error.sheriff.transfer=Badge transfer error: {0}
diff --git a/agentscope-examples/werewolf-hitl/src/main/resources/messages_zh_CN.properties b/agentscope-examples/werewolf-hitl/src/main/resources/messages_zh_CN.properties
index 5f6f39336..04e5242db 100644
--- a/agentscope-examples/werewolf-hitl/src/main/resources/messages_zh_CN.properties
+++ b/agentscope-examples/werewolf-hitl/src/main/resources/messages_zh_CN.properties
@@ -1,169 +1,193 @@
# Werewolf Game - Chinese Messages
-
# Config
-config.player.names=1号,2号,3号,4号,5号,6号,7号,8号,9号
-
+config.player.names=1\u53F7,2\u53F7,3\u53F7,4\u53F7,5\u53F7,6\u53F7,7\u53F7,8\u53F7,9\u53F7
# Roles
-role.villager.name=村民
-role.villager.symbol=👤
-role.werewolf.name=狼人
-role.werewolf.symbol=🐺
-role.seer.name=预言家
-role.seer.symbol=🔮
-role.witch.name=女巫
-role.witch.symbol=🧪
-role.hunter.name=猎人
-role.hunter.symbol=🏹
-
+role.villager.name=\u6751\u6C11
+role.villager.symbol=\uD83D\uDC64
+role.werewolf.name=\u72FC\u4EBA
+role.werewolf.symbol=\uD83D\uDC3A
+role.seer.name=\u9884\u8A00\u5BB6
+role.seer.symbol=\uD83D\uDD2E
+role.witch.name=\u5973\u5DEB
+role.witch.symbol=\uD83E\uDDEA
+role.hunter.name=\u730E\u4EBA
+role.hunter.symbol=\uD83C\uDFF9
# Game Messages
-game.welcome.title=狼人杀游戏 - 9人多智能体对战
-game.welcome.description=一个复杂的社交推理游戏,玩家分为村民和狼人两个阵营。\n村民必须通过讨论和投票找出并消灭狼人。\n狼人必须在不暴露身份的情况下消灭村民。\n\n角色:\n - 3名村民:没有特殊能力\n - 3名狼人:每晚消灭一名村民\n - 1名预言家:每晚可以查验一名玩家的身份\n - 1名女巫:拥有解药和毒药各一瓶(每瓶只能使用一次)\n - 1名猎人:被淘汰时可以射杀一名玩家
-game.player.assignments=玩家身份分配:
-game.initializing=初始化狼人杀游戏
-
+game.welcome.title=\u72FC\u4EBA\u6740\u6E38\u620F - 9\u4EBA\u591A\u667A\u80FD\u4F53\u5BF9\u6218
+game.welcome.description=\u4E00\u4E2A\u590D\u6742\u7684\u793E\u4EA4\u63A8\u7406\u6E38\u620F\uFF0C\u73A9\u5BB6\u5206\u4E3A\u795E\u6C11\uFF08\u9884\u8A00\u5BB6\u3001\u5973\u5DEB\u3001\u730E\u4EBA\uFF09\u3001\u6751\u6C11\u548C\u72FC\u4EBA\u4E09\u4E2A\u9635\u8425\u3002\u6751\u6C11\u9700\u8981\u901A\u8FC7\u8BA8\u8BBA\u548C\u6295\u7968\u627E\u51FA\u5E76\u6D88\u706D\u72FC\u4EBA\u3001\u795E\u6C11\u9700\u8981\u901A\u8FC7\u6295\u7968\u6216\u8005\u6280\u80FD\u627E\u51FA\u5E76\u6D88\u706D\u72FC\u4EBA\u3002\u72FC\u4EBA\u5FC5\u987B\u5728\u4E0D\u66B4\u9732\u8EAB\u4EFD\u7684\u60C5\u51B5\u4E0B\u6D88\u706D\u795E\u6C11\u6216\u8005\u6751\u6C11\u4EFB\u4E00\u4E00\u65B9\u3002
+game.player.assignments=\u73A9\u5BB6\u8EAB\u4EFD\u5206\u914D\uFF1A
+game.initializing=\u521D\u59CB\u5316\u72FC\u4EBA\u6740\u6E38\u620F
# Phases
-phase.night.title=🌙 夜晚阶段 - 所有人闭上眼睛...
-phase.day.title=☀️ 白天阶段 - 所有人睁开眼睛...
-phase.voting.title=🗳️ 投票阶段
-phase.night.complete=\n🌙 夜晚阶段结束。等待天亮...\n
-
+phase.night.title=\uD83C\uDF19 \u591C\u665A\u9636\u6BB5 - \u6240\u6709\u4EBA\u95ED\u4E0A\u773C\u775B...
+phase.day.title=\u2600\uFE0F \u767D\u5929\u9636\u6BB5 - \u6240\u6709\u4EBA\u7741\u5F00\u773C\u775B...
+phase.voting.title=\uD83D\uDDF3\uFE0F \u6295\u7968\u9636\u6BB5
+phase.night.complete=\n\uD83C\uDF19 \u591C\u665A\u9636\u6BB5\u7ED3\u675F\u3002\u7B49\u5F85\u5929\u4EAE...\n
# Werewolf Actions
-werewolf.discussion=\n--- 狼人讨论 ---
-werewolf.discussion.round= 狼人讨论 第{0}轮:
-werewolf.voting=\n 狼人投票:
-werewolf.chose=狼人选择消灭:{0}
-
+werewolf.discussion=\n--- \u72FC\u4EBA\u8BA8\u8BBA ---
+werewolf.discussion.round=\u72FC\u4EBA\u8BA8\u8BBA \u7B2C{0}\u8F6E\uFF1A
+werewolf.voting=\n \u72FC\u4EBA\u6295\u7968\uFF1A
+werewolf.chose=\u72FC\u4EBA\u9009\u62E9\u6D88\u706D\uFF1A{0}
+human.werewolf.discussion=\u591C\u665A\u9636\u6BB5\uFF0C\u8BF7\u4E0E\u4F60\u7684\u72FC\u4EBA\u540C\u4F34\u8BA8\u8BBA\u5E76\u5C31\u6D88\u706D\u8C01\u8FBE\u6210\u5171\u8BC6\uFF0C\u540C\u65F6\u51B3\u5B9A\u8C01\u6765\u608D\u8DF3\u9884\u8A00\u5BB6
# Witch Actions
-witch.actions=\n--- 女巫行动 ---
-witch.sees.victim= 女巫看到:{0}被狼人攻击了
-witch.heal.decision= [{0}] 解药决定:{1}(理由:{2})
-witch.used.heal= ✓ 女巫使用解药救了 {0}
-witch.poison.decision= [{0}] 毒药决定:{1}(目标:{2},理由:{3})
-witch.used.poison= ✓ 女巫使用毒药毒死了 {0}
-
+witch.actions=\n--- \u5973\u5DEB\u884C\u52A8 ---
+witch.sees.victim=\u5973\u5DEB\u770B\u5230\uFF1A{0}\u88AB\u72FC\u4EBA\u653B\u51FB\u4E86
+witch.heal.decision=[{0}] \u89E3\u836F\u51B3\u5B9A\uFF1A{1}\uFF08\u7406\u7531\uFF1A{2}\uFF09
+witch.used.heal=\u2713 \u5973\u5DEB\u4F7F\u7528\u89E3\u836F\u6551\u4E86 {0}
+witch.poison.decision=[{0}] \u6BD2\u836F\u51B3\u5B9A\uFF1A{1}\uFF08\u76EE\u6807\uFF1A{2}\uFF0C\u7406\u7531\uFF1A{3}\uFF09
+witch.used.poison=\u2713 \u5973\u5DEB\u4F7F\u7528\u6BD2\u836F\u6BD2\u6B7B\u4E86 {0}
# Seer Actions
-seer.check=\n--- 预言家查验 ---
-seer.check.decision= [{0}] 想要查验:{1}(理由:{2})
-seer.check.result= ✓ 查验结果:{0} {1}
-
+seer.check=\n--- \u9884\u8A00\u5BB6\u67E5\u9A8C ---
+seer.check.decision=[{0}] \u60F3\u8981\u67E5\u9A8C\uFF1A{1}\uFF08\u7406\u7531\uFF1A{2}\uFF09
+seer.check.result=\u2713 \u67E5\u9A8C\u7ED3\u679C\uFF1A{0} {1}
# Day Discussion
-day.discussion=\n--- 白天讨论 ---
-discussion.round=\n 讨论环节 第{0}轮:
-
+day.discussion=\n--- \u767D\u5929\u8BA8\u8BBA ---
+discussion.round=\n \u8BA8\u8BBA\u73AF\u8282 \u7B2C{0}\u8F6E\uFF1A
# Voting
-voting.results=\n投票结果:
-voting.no.valid=\n没有有效投票。无人被淘汰。
-voting.tie=\n检测到平票:{0}。随机选择:{1}
-voting.count= {0}:{1}票
-voting.eliminated=\n{0}被投票淘汰。他们的身份是{1}。
-voting.detail= [{0}] 投票给:{1}(理由:{2})
-
+voting.results=\n\u6295\u7968\u7ED3\u679C\uFF1A
+voting.no.valid=\n\u6CA1\u6709\u6709\u6548\u6295\u7968\u3002\u65E0\u4EBA\u88AB\u6DD8\u6C70\u3002
+voting.tie=\n\u68C0\u6D4B\u5230\u5E73\u7968\uFF1A{0}\u3002\u968F\u673A\u9009\u62E9\uFF1A{1}
+voting.count={0}\uFF1A{1}\u7968
+voting.eliminated=\n{0}\u88AB\u6295\u7968\u6DD8\u6C70\u3002\u4ED6\u4EEC\u7684\u8EAB\u4EFD\u662F{1}\u3002
+voting.detail=[{0}] \u6295\u7968\u7ED9\uFF1A{1}\uFF08\u7406\u7531\uFF1A{2}\uFF09
# Hunter
-hunter.shoot=\n--- 猎人的最后一枪 ---
-hunter.shoot.decision= [{0}] 射击决定:{1}(目标:{2},理由:{3})
-hunter.shot.player= ✓ 猎人射杀了{0}。他们的身份是{1}。
-hunter.no.shoot= 猎人选择不开枪。
-
+hunter.shoot=\n--- \u730E\u4EBA\u7684\u6700\u540E\u4E00\u67AA ---
+hunter.shoot.decision=[{0}] \u5C04\u51FB\u51B3\u5B9A\uFF1A{1}\uFF08\u76EE\u6807\uFF1A{2}\uFF0C\u7406\u7531\uFF1A{3}\uFF09
+hunter.shot.player=\u2713 \u730E\u4EBA\u5C04\u6740\u4E86{0}\u3002\u4ED6\u4EEC\u7684\u8EAB\u4EFD\u662F{1}\u3002
+hunter.no.shoot=\u730E\u4EBA\u9009\u62E9\u4E0D\u5F00\u67AA\u3002
# Game End
-game.over=游戏结束
-game.villagers.win=🎉 村民胜利!🎉
-game.villagers.win.explanation=所有狼人已被消灭。
-game.werewolves.win=🐺 狼人胜利!🐺
-game.werewolves.win.explanation=狼人占领了村庄。
-game.max.rounds=游戏在达到最大回合数后结束,没有明确的赢家。
-
+game.over=\u6E38\u620F\u7ED3\u675F
+game.villagers.win=\uD83C\uDF89 \u6751\u6C11\u80DC\u5229\uFF01\uD83C\uDF89
+game.villagers.win.explanation=\u6240\u6709\u72FC\u4EBA\u5DF2\u88AB\u6D88\u706D\u3002
+game.werewolves.win=\uD83D\uDC3A \u72FC\u4EBA\u80DC\u5229\uFF01\uD83D\uDC3A
+game.werewolves.win.explanation=\u72FC\u4EBA\u5360\u9886\u4E86\u6751\u5E84\u3002
+game.max.rounds=\u6E38\u620F\u5728\u8FBE\u5230\u6700\u5927\u56DE\u5408\u6570\u540E\u7ED3\u675F\uFF0C\u6CA1\u6709\u660E\u786E\u7684\u8D62\u5BB6\u3002
# Status
-status.final=\n最终状态:
-status.alive.players=存活玩家:
-status.all.players=\n所有玩家及其身份:
-status.round=第{0}回合 - 游戏状态
-status.alive=存活:{0}人 | 狼人:{1}人 | 村民:{2}人
-status.label.alive=存活
-status.label.dead=死亡
-
+status.final=\n\u6700\u7EC8\u72B6\u6001\uFF1A
+status.alive.players=\u5B58\u6D3B\u73A9\u5BB6\uFF1A
+status.all.players=\n\u6240\u6709\u73A9\u5BB6\u53CA\u5176\u8EAB\u4EFD\uFF1A
+status.round=\u7B2C{0}\u56DE\u5408 - \u6E38\u620F\u72B6\u6001
+status.alive=\u5B58\u6D3B\uFF1A{0}\u4EBA | \u72FC\u4EBA\uFF1A{1}\u4EBA | \u6751\u6C11\uFF1A{2}\u4EBA
+status.label.alive=\u5B58\u6D3B
+status.label.dead=\u6B7B\u4EA1
# Decisions
-decision.yes=是
-decision.no=否
-decision.witch.heal.yes=是,使用解药
-decision.witch.poison.yes=是,使用毒药
-decision.hunter.shoot.yes=是,开枪
-decision.hunter.shoot.no=否,不开枪
-decision.is.werewolf=是狼人
-decision.not.werewolf=不是狼人
-
+decision.yes=\u662F
+decision.no=\u5426
+decision.witch.heal.yes=\u662F\uFF0C\u4F7F\u7528\u89E3\u836F
+decision.witch.poison.yes=\u662F\uFF0C\u4F7F\u7528\u6BD2\u836F
+decision.hunter.shoot.yes=\u662F\uFF0C\u5F00\u67AA
+decision.hunter.shoot.no=\u5426\uFF0C\u4E0D\u5F00\u67AA
+decision.is.werewolf=\u662F\u72FC\u4EBA
+decision.not.werewolf=\u4E0D\u662F\u72FC\u4EBA
# Errors
-error.vote.parsing= [{0}] 投票解析错误
-error.decision= {0}决定错误:
-
+error.vote.parsing=[{0}] \u6295\u7968\u89E3\u6790\u9519\u8BEF
+error.decision={0}\u51B3\u5B9A\u9519\u8BEF\uFF1A
# System Messages
-system.werewolf.kill.result=狼人决定杀死:{0}
-system.werewolf.kill.none=狼人决定杀死:无人
-system.voting.result=投票结果:{0} 将被淘汰
-system.voting.result.none=投票结果:无人被淘汰(平票)
-system.werewolf.discussing=狼人正在讨论...
-system.witch.acting=女巫行动中...
-system.witch.sees.victim=女巫看到今晚被杀的是 {0}
-system.seer.acting=预言家行动中...
-system.day.discussion.start=白天讨论开始...
-system.voting.start=投票阶段开始...
-system.hunter.skill=猎人发动技能...
-system.hunter.shoot.announcement=【公告】猎人 {0} 发动技能,带走了 {1}。
-
+system.werewolf.kill.result=\u72FC\u4EBA\u51B3\u5B9A\u6740\u6B7B\uFF1A{0}
+system.werewolf.kill.none=\u72FC\u4EBA\u51B3\u5B9A\u6740\u6B7B\uFF1A\u65E0\u4EBA
+system.voting.result=\u6295\u7968\u7ED3\u679C\uFF1A{0} \u5C06\u88AB\u6DD8\u6C70
+system.voting.result.none=\u6295\u7968\u7ED3\u679C\uFF1A\u65E0\u4EBA\u88AB\u6DD8\u6C70\uFF08\u5E73\u7968\uFF09
+system.werewolf.discussing=\u72FC\u4EBA\u6B63\u5728\u8BA8\u8BBA...
+system.witch.acting=\u5973\u5DEB\u884C\u52A8\u4E2D...
+system.witch.sees.victim=\u5973\u5DEB\u770B\u5230\u4ECA\u665A\u88AB\u6740\u7684\u662F {0}
+system.seer.acting=\u9884\u8A00\u5BB6\u884C\u52A8\u4E2D...
+system.day.discussion.start=\u767D\u5929\u8BA8\u8BBA\u5F00\u59CB...
+system.voting.start=\u6295\u7968\u9636\u6BB5\u5F00\u59CB...
+system.hunter.skill=\u730E\u4EBA\u53D1\u52A8\u6280\u80FD...
+system.hunter.shoot.announcement=\u3010\u516C\u544A\u3011\u730E\u4EBA {0} \u53D1\u52A8\u6280\u80FD\uFF0C\u5E26\u8D70\u4E86 {1}\u3002
# Action Descriptions
-action.witch.use.heal=使用解药
-action.witch.heal.result=救活成功
-action.witch.heal.skip=选择不使用解药
-action.witch.use.poison=使用毒药
-action.witch.poison.result=毒杀成功
-action.witch.poison.skip=选择不使用毒药
-action.seer.check=查验
-action.hunter.shoot=开枪
-action.hunter.shoot.result=射杀成功
-action.hunter.shoot.skip=选择不开枪
-error.witch.heal=女巫解药决定错误: {0}
-error.witch.poison=女巫毒药决定错误: {0}
-error.seer.check=预言家查验错误: {0}
-error.hunter.shoot=猎人开枪错误: {0}
-
+action.witch.use.heal=\u4F7F\u7528\u89E3\u836F
+action.witch.heal.result=\u6551\u6D3B\u6210\u529F
+action.witch.heal.skip=\u9009\u62E9\u4E0D\u4F7F\u7528\u89E3\u836F
+action.witch.use.poison=\u4F7F\u7528\u6BD2\u836F
+action.witch.poison.result=\u6BD2\u6740\u6210\u529F
+action.witch.poison.skip=\u9009\u62E9\u4E0D\u4F7F\u7528\u6BD2\u836F
+action.seer.check=\u67E5\u9A8C
+action.hunter.shoot=\u5F00\u67AA
+action.hunter.shoot.result=\u5C04\u6740\u6210\u529F
+action.hunter.shoot.skip=\u9009\u62E9\u4E0D\u5F00\u67AA
+error.witch.heal=\u5973\u5DEB\u89E3\u836F\u51B3\u5B9A\u9519\u8BEF: {0}
+error.witch.poison=\u5973\u5DEB\u6BD2\u836F\u51B3\u5B9A\u9519\u8BEF: {0}
+error.seer.check=\u9884\u8A00\u5BB6\u67E5\u9A8C\u9519\u8BEF: {0}
+error.hunter.shoot=\u730E\u4EBA\u5F00\u67AA\u9519\u8BEF: {0}
# Prompts
-prompt.role.villager=【信息来源】:标记为【Moderator Message】的消息是裁判发出的官方信息,这部分内容是绝对真实可信的。\n\n【History 阅读指南】:History 中的消息按时间顺序排列。每个人发言时,只能看到他前面的消息,看不到他后面的消息。因此,分析某人的发言逻辑时,要基于他发言时刻所能看到的信息来判断。\n\n【重要提示】:发言必须简洁,控制在2-3句话以内。\n\n【角色身份】:你是{0},现在是一名"狼人杀"游戏中的普通村民。虽然你没有任何超能力,但你的选票和你的分析能力是好人获胜的唯一依靠。\n\n【游戏流程】:\n游戏分为夜晚和白天两个阶段循环进行。\n夜晚阶段:狼人密谋选择击杀目标 → 女巫决定是否使用解药/毒药(各只有一瓶,整局只能用一次)→ 预言家查验一名玩家身份。\n白天阶段:公布昨夜死讯 → 所有存活玩家按固定顺序轮流发言两轮 → 投票选出一名玩家放逐。\n特殊规则:猎人在死亡时(无论是被杀还是被投票)可以选择开枪带走一名玩家。\n胜利条件:好人阵营需要投票放逐所有狼人;狼人阵营需要让狼人数量达到或超过好人数量。\n\n【玩家配置】:共9名玩家,狼人阵营3名狼人,好人阵营3名村民、1名预言家、1名女巫、1名猎人。\n\n【核心目标】:\n通过分析发言找出狼人逻辑漏洞,保护真正的神职人员。\n保持自身清白(表水),避免被狼人抗推(误投出局)。\n在关键时刻通过战术伪装,吸引狼人的刀口,为神职人员争取轮次。\n\n【深度决策逻辑】:\n\n策略 A:深度表水 (证明清白)\n行动:当别人怀疑你时,不要情绪化,要清晰交代自己的"心路历程"。\n话术:不要只说"我是好人",要说:"我为什么怀疑 X 号,因为他刚才发言时的逻辑矛盾点在哪里,如果我是狼,我没必要在那一轮冒风险去踩他。"\n目标:通过逻辑的透明度和纯粹性,让场上玩家认同你的"好人面"。\n\n策略 B:辨别真假预言家 (站队艺术)\n行动:重点对比场上跳预言家的两名玩家。\n分析逻辑:\n视角对比:谁更像是在寻找狼人?谁更像是在找神职位置?\n力度对比:谁的查杀更准?\n逻辑断层:如果一个预言家给出的金水(好人)发言非常差,那么这个预言家的身份就很可疑。\n站队:如果没把握,可以先保持中立并说明理由;一旦确定,要坚定地帮真预言家拉票。\n\n策略 C:战术挡刀 (保护神职)\n行动:在局势危急(如真预言家已死)时,你可以故意发言暧昧,或者假装自己是"有药的女巫"或"有枪的猎人"。\n目标:诱导狼人今晚来杀你,从而保护真正的神职人员(如留有一瓶毒药的女巫)多活一轮。\n注意:挡刀要适度,不能在白天放逐阶段把好人逻辑搞乱。\n\n策略 D:捕捉"深水狼"与"划水狼"\n行动:关注那些发言极简、不站队、或者一直跟随大众投票的人。\n逻辑:指出这些"混子"的危险性,通过逼迫他们表态来暴露其身份。\n\n【不同阶段的发言准则】:\n\n游戏前期 (Day 1-2):\n"我是一个没视角的平民,我目前的站队倾向于 X 号,因为他的验人逻辑更自然。"\n"我不建议今天在没把握的情况下投走任何一个疑似预言家,我们可以先从发言最差的'划水位'排坑。"\n\n游戏中期 (Day 3-4):\n"目前场上神职已经走得差不多了,根据之前的投票票型,Y 号和 Z 号总是在冲票,这两位里面必出一头狼。"\n"我愿意为我的逻辑负责,如果投错了,明天你们可以出我。"(展示坚定的好人姿态)\n\n游戏后期 (残局):\n通过排除法思考:"目前已知好人有 A、B,那么狼人只能在剩下的 C、D 中产生。"\n\n【AI 执行时的禁忌(反向约束)】:\n禁止盲从:不要因为某人跳了神职就无条件相信,要看其行为是否符合逻辑。\n禁止自爆:作为平民,即便被误投,也要留下有价值的遗言,绝不能消极比赛。\n禁止胡乱穿衣服:除非是为了挡刀,否则不要在局势明朗时乱跳身份,以免干扰女巫的判断。
-prompt.role.werewolf=【信息来源】:标记为【Moderator Message】的消息是裁判发出的官方信息,这部分内容是绝对真实可信的。\n\n【History 阅读指南】:History 中的消息按时间顺序排列。每个人发言时,只能看到他前面的消息,看不到他后面的消息。因此,分析某人的发言逻辑时,要基于他发言时刻所能看到的信息来判断。\n\n【角色身份】:你是{0},现在是一名"狼人杀"游戏中的狼人。\n\n【重要提示】:发言必须简洁,控制在2-3句话以内。\n\n【游戏流程】:\n游戏分为夜晚和白天两个阶段循环进行。\n夜晚阶段:狼人密谋选择击杀目标 → 女巫决定是否使用解药/毒药(各只有一瓶,整局只能用一次)→ 预言家查验一名玩家身份。\n白天阶段:公布昨夜死讯 → 所有存活玩家按固定顺序轮流发言两轮 → 投票选出一名玩家放逐。\n特殊规则:猎人在死亡时(无论是被杀还是被投票)可以选择开枪带走一名玩家。\n胜利条件:好人阵营需要投票放逐所有狼人;狼人阵营需要让狼人数量达到或超过好人数量。\n\n【玩家配置】:共9名玩家,狼人阵营3名狼人,好人阵营3名村民、1名预言家、1名女巫、1名猎人。\n\n【核心目标】:\n隐藏你的狼人身份,生存到最后。\n通过发言误导好人,投票杀死神职人员(预言家、女巫、猎人)。\n晚上一起和你密谋杀人的都是你的狼人队友,你需要与你的狼队友配合,在白天制造逻辑混乱。\n\n【决策逻辑】(在发言前,请在脑中进行以下思考):\n当前局势:谁跳了预言家?场上风向在怀疑谁?我的狼队友表现如何?\n战术选择:根据分配给你的指令,执行以下一种战术:\n\n策略 A:悍跳狼(冒充预言家)\n发言要点:必须在第一轮起跳。\n行动:给出虚假的"验人信息"。如果你给好人发"查杀",要给出强有力的伪造理由;如果你给队友发"金水",目的是为了坐实队友的好人身份。\n语气:坚定、自信、具有攻击性。反复强调自己才是真预言家,指责对手是"悍跳狼"。\n\n策略 B:普通狼(深水/倒钩狼)\n发言要点:发言精简,避免成为焦点。\n行动:观察局势。如果悍跳的队友处于劣势,为了保全自己,可以跟随好人一起投票给队友(倒钩)。如果好人逻辑出现偏差,顺水推舟引导他们放逐平民。\n语气:真诚、像一个努力寻找狼人的平民。\n\n策略 C:自刀狼(骗药/坐身份)\n行动:在夜晚选择自刀。\n发言要点:\n如果女巫救了你,你就是"银水"身份。利用这个高信誉度,在白天带节奏,排挤真正的神职。\n如果是在与真预言家对跳时自刀,目的是让好人认为"狼人怕我,所以想把我首杀",以此证明你的预言家身份是真实的。\n语气:带有一点"被杀的委屈"或"正义感",博取同情和信任。\n\n【阶段感知】\nHistory 中的最后一个 Moderator Message 的内容表示当前是什么阶段,你需要严格遵守当前所在的阶段,不要瞎猜。\n夜晚阶段的对话只有你和你的队友可见,其他角色的人是不知道的,包括你们杀了谁,为什么要杀,都是狼队的内部机密,白天必须假装这些讨论从未发生。除了你的狼人队友,没有人知道昨晚谁被杀了,除非法官已经公开宣布死讯。\n白天发言:白天的发言是所有玩家都能看到的公开信息,需要注意不要暴露自己身份。\n首轮禁忌:第一次白天发言时,大家昨晚都在睡觉。严禁评价他人昨晚的表现,只能评价你白天阶段听到的发言。\n\n【发言准则】:\n信息封锁:夜晚的讨论内容绝对不能出现在白天发言中,哪怕是暗示也不行。\n绝不承认:无论被如何质疑,绝对不能承认自己是狼。\n逻辑伪装:所有的分析必须基于"公开发言",假装你不知道任何内幕信息。\n观察细节:捕捉好人发言中的漏洞,将其定义为"狼人留下的马脚"。
-prompt.role.seer=【信息来源】:标记为【Moderator Message】的消息是裁判发出的官方信息,这部分内容是绝对真实可信的。\n\n【History 阅读指南】:History 中的消息按时间顺序排列。每个人发言时,只能看到他前面的消息,看不到他后面的消息。因此,分析某人的发言逻辑时,要基于他发言时刻所能看到的信息来判断。\n\n【重要提示】:发言必须简洁,控制在2-3句话以内。\n\n【角色身份】:你是{0},现在是一名"狼人杀"游戏中的预言家,拥有每晚查验一人身份的能力。\n\n【游戏流程】:\n游戏分为夜晚和白天两个阶段循环进行。\n夜晚阶段:狼人密谋选择击杀目标 → 女巫决定是否使用解药/毒药(各只有一瓶,整局只能用一次)→ 预言家查验一名玩家身份。\n白天阶段:公布昨夜死讯 → 所有存活玩家按固定顺序轮流发言两轮 → 投票选出一名玩家放逐。\n特殊规则:猎人在死亡时(无论是被杀还是被投票)可以选择开枪带走一名玩家。\n胜利条件:好人阵营需要投票放逐所有狼人;狼人阵营需要让狼人数量达到或超过好人数量。\n\n【玩家配置】:共9名玩家,狼人阵营3名狼人,好人阵营3名村民、1名预言家、1名女巫、1名猎人。\n\n【核心目标】:尽快报出验人信息,通过逻辑说服平民。\n\n【决策逻辑】:\n\n策略 A:标准起跳\n行动:第一天必须起跳(声明身份)。清晰报出:谁是金水(好人)或查杀(狼人)。\n心路历程:解释你为什么验那个人(例如:他发言像划水,或者他在煽动投票)。\n\n策略 C:逻辑反击\n行动:面对"悍跳狼",从视角上压制对方。指出对方给出的金水/查杀是否不符合逻辑,对比双方的心路历程。\n\n【发言准则】:\n真诚坚定:你的语气要最真诚,逻辑要最完整。\n不卑不亢:面对质疑不要急躁,用事实和验人逻辑说话。
-prompt.role.witch=【信息来源】:标记为【Moderator Message】的消息是裁判发出的官方信息,这部分内容是绝对真实可信的。\n\n【History 阅读指南】:History 中的消息按时间顺序排列。每个人发言时,只能看到他前面的消息,看不到他后面的消息。因此,分析某人的发言逻辑时,要基于他发言时刻所能看到的信息来判断。\n\n【重要提示】:发言必须简洁,控制在2-3句话以内。\n\n【角色身份】:你是{0},现在是一名"狼人杀"游戏中的女巫。你手握一瓶解药(救人)和一瓶毒药(杀人),是场上追轮次的关键。\n\n【游戏流程】:\n游戏分为夜晚和白天两个阶段循环进行。\n夜晚阶段:狼人密谋选择击杀目标 → 女巫决定是否使用解药/毒药(各只有一瓶,整局只能用一次)→ 预言家查验一名玩家身份。\n白天阶段:公布昨夜死讯 → 所有存活玩家按固定顺序轮流发言两轮 → 投票选出一名玩家放逐。\n特殊规则:猎人在死亡时(无论是被杀还是被投票)可以选择开枪带走一名玩家。\n胜利条件:好人阵营需要投票放逐所有狼人;狼人阵营需要让狼人数量达到或超过好人数量。\n\n【玩家配置】:共9名玩家,狼人阵营3名狼人,好人阵营3名村民、1名预言家、1名女巫、1名猎人。\n\n【核心目标】:合理分配药水,保护关键身份,毒死嫌疑最大的狼人。\n\n【决策逻辑】:\n\n策略 A:解药使用(银水逻辑)\n行动:第一晚通常开启解药救人。\n分析:被救的人称为"银水"。你要观察他的发言:他是真的好人,还是"自刀狼"?在没确认前,不要完全信任他。\n\n策略 B:毒药使用(谨慎执法)\n行动:非必要不乱开毒。只有当某人发言极差、且你确定他是狼,或者为了自保时才开毒。\n战术:如果在白天被多人怀疑,可以跳明身份并威胁:"今晚谁投我,我毒药就带走谁。"\n\n策略 C:隐藏身份\n行动:在药水用完前,尽量伪装成平民。发言时多听、少带头,避免被狼人首刀导致双药作废。\n\n【发言准则】:\n强势带队:一旦跳明身份,发言必须强势,要求好人跟从你的逻辑。\n保密意识:未用药前,绝不透露谁是你的银水。
-prompt.role.hunter=【信息来源】:标记为【Moderator Message】的消息是裁判发出的官方信息,这部分内容是绝对真实可信的。\n\n【History 阅读指南】:History 中的消息按时间顺序排列。每个人发言时,只能看到他前面的消息,看不到他后面的消息。因此,分析某人的发言逻辑时,要基于他发言时刻所能看到的信息来判断。\n\n【重要提示】:发言必须简洁,控制在2-3句话以内。\n\n【角色身份】:你是{0},现在是一名"狼人杀"游戏中的猎人,如果你被杀或被投出场,你可以开枪带走场上任何一名玩家。\n\n【游戏流程】:\n游戏分为夜晚和白天两个阶段循环进行。\n夜晚阶段:狼人密谋选择击杀目标 → 女巫决定是否使用解药/毒药(各只有一瓶,整局只能用一次)→ 预言家查验一名玩家身份。\n白天阶段:公布昨夜死讯 → 所有存活玩家按固定顺序轮流发言两轮 → 投票选出一名玩家放逐。\n特殊规则:猎人在死亡时(无论是被杀还是被投票)可以选择开枪带走一名玩家。\n胜利条件:好人阵营需要投票放逐所有狼人;狼人阵营需要让狼人数量达到或超过好人数量。\n\n【玩家配置】:共9名玩家,狼人阵营3名狼人,好人阵营3名村民、1名预言家、1名女巫、1名猎人。\n\n【核心目标】:作为好人的最后一道防线,利用开枪技能威慑狼人,并在临终前带走真狼。\n\n【决策逻辑】:\n\n策略 A:前期藏枪\n行动:游戏前期假装平民,观察谁在找神职人员,谁在乱带节奏。\n逻辑:如果你太早暴露,狼人可能会留着你不杀,转而去杀预言家。\n\n策略 B:后期明枪带队\n行动:当场上局势混乱或预言家已死,直接跳明身份。\n话术:你可以说"我是猎人,今天大家跟我投X号,投错了算我的,我死后会带走另一只狼"。\n\n策略 C:临终选择\n行动:当你出局时,迅速回顾白天所有人的发言。\n判断:谁最急于把你投出场?谁在投票时跟风?带走那个煽动性最强的玩家。\n\n【发言准则】:\n刚正不阿:发言可以粗放、霸气,不怕得罪人。\n自证清白:如果被怀疑,直接挑衅:"你可以投我,看我走的时候带不带你就完了。"
-
-# Prompt Templates
-prompt.werewolf.discussion.header=夜晚阶段 - 狼人们,请睁开眼睛。\n\n你们需要选择今晚要消灭的玩家。\n\n可选目标:\n
-prompt.werewolf.discussion.footer=\n与你的狼人同伴讨论并就消灭谁达成共识。
-prompt.werewolf.voting.header=狼人投票阶段。现在投票选择你们今晚要消灭的玩家。\n\n可选目标:\n
-prompt.werewolf.voting.footer=\n你必须选择一名玩家。
-prompt.witch.heal=女巫,请睁开眼睛。\n\n今晚,{0}被狼人攻击了。\n\n你有一瓶解药。你想用它来救{0}吗?\n记住:这瓶药在整个游戏中只能使用一次。
-prompt.witch.poison.header.healed=你已经使用了解药。\n\n
-prompt.witch.poison.header=你有一瓶毒药。你想用它在今晚毒死某人吗?\n\n可选目标:\n
-prompt.witch.poison.footer=\n记住:这瓶药在整个游戏中只能使用一次。
-prompt.seer.check.header=预言家,请睁开眼睛。\n\n选择一名玩家来查验他们的身份。\n\n可选玩家:\n
-prompt.seer.result=你查验了{0}。他们{1}。
-prompt.seer.is.werewolf=是狼人
-prompt.seer.not.werewolf=不是狼人
-prompt.night.result.header=\n============================================================\n白天阶段 - 所有人请睁开眼睛。\n============================================================\n\n
-prompt.night.result.peaceful=昨晚是平安夜。没有人死亡。\n
-prompt.night.result.peaceful.healed=昨晚是平安夜。没有人死亡。\n(女巫使用了解药)\n
-prompt.night.result.deaths=昨晚,以下玩家死亡:\n
-prompt.night.result.killed= - {0}(被狼人杀死)\n
-prompt.night.result.poisoned= - {0}(被女巫毒死)\n
-prompt.night.result.alive=\n存活玩家({0}人):\n
-prompt.discussion.header=\n讨论环节 第{0}轮\n----------------------------------------\n分享你的想法、怀疑和观察。\n根据讨论尝试识别狼人。\n请保持发言简洁(2-3句话)。
-prompt.voting.header=\n============================================================\n投票阶段\n============================================================\n\n投票选择你想要淘汰的玩家。\n\n可选玩家:\n
-prompt.voting.footer=\n你必须投票给一名玩家。
-prompt.hunter.header=\n{0},你是猎人,你已经被淘汰了。\n\n你可以在死前选择射杀一名玩家。\n\n可选目标:\n
-prompt.hunter.footer=\n你想开枪吗?如果是,你想射杀谁?
-
+# \u516C\u5171\u63D0\u793A\u8BCD\u90E8\u5206\uFF08\u6240\u6709\u89D2\u8272\u5171\u7528\uFF09
+prompt.role=\u4F60\u662F\u4E00\u4F4D\u7CBE\u901A\u72FC\u4EBA\u6740\u6E38\u620F\u7684\u73A9\u5BB6\uFF0C\u5F53\u524D\u7684\u72FC\u4EBA\u6740\u6E38\u620F\u7248\u578B\u4E3A{2}\u4EBA\u5236\uFF1A{3}\u6C11\uFF0C3\u795E\uFF08\u9884\u8A00\u5BB6\u3001\u5973\u5DEB\u3001\u730E\u4EBA\uFF09\uFF0C{4}\u72FC\uFF0C\u5C60\u8FB9\u89C4\u5219\uFF0C\u72FC\u4EBA\u65E0\u6CD5\u81EA\u7206\u3002\u4F60\u5F53\u524D\u7684\u8EAB\u4EFD\u662F{0}\uFF0C\u4F60\u7684\u540D\u5B57\u662F\uFF1A{1}
+prompt.partiner=\u4F60\u6240\u5728\u7684\u9635\u8425\u5305\u542B\uFF1A{0}
+prompt.info_source=\u3010\u4FE1\u606F\u6765\u6E90\u3011\uFF1A\u4E2D\u7684\u4FE1\u606F\u6309\u65F6\u95F4\u987A\u5E8F\u6392\u5217\uFF0C\u6807\u8BB0\u4E3A\u3010Moderator Message\u3011\u7684\u6D88\u606F\u662F\u6CD5\u5B98\u53D1\u51FA\u7684\u5B98\u65B9\u4FE1\u606F\uFF0C\u8FD9\u90E8\u5206\u5185\u5BB9\u662F\u7EDD\u5BF9\u771F\u5B9E\u53EF\u4FE1\u7684\u3002\u5176\u4ED6\u6D88\u606F\u662F\u73A9\u5BB6\u7684\u4FE1\u606F\uFF0C\u4F60\u5F53\u524D\u9700\u8981\u7ED3\u5408\u4F60\u89C2\u5BDF\u5230\u7684\u5404\u73A9\u5BB6\u7684\u5386\u53F2\u4FE1\u606F\uFF0C\
+\u5BF9\u6700\u8FD1\u7684\u4E00\u6761\u3010Moderator Message\u3011\u8FDB\u884C\u54CD\u5E94
+prompt.history_guide=\u3010messages\u9605\u8BFB\u6307\u5357\u3011\uFF1A\u4E2D\u7684\u6D88\u606F\u6309\u65F6\u95F4\u987A\u5E8F\u6392\u5217\u3002\u6BCF\u4E2A\u4EBA\u53D1\u8A00\u65F6\uFF0C\u53EA\u80FD\u770B\u5230\u4ED6\u524D\u9762\u7684\u6D88\u606F\uFF0C\u770B\u4E0D\u5230\u4ED6\u540E\u9762\u7684\u6D88\u606F\u3002\u56E0\u6B64\uFF0C\u5206\u6790\u67D0\u4EBA\u7684\u53D1\u8A00\u903B\u8F91\u65F6\uFF0C\u8981\u57FA\u4E8E\u4ED6\u53D1\u8A00\u65F6\u523B\u6240\u80FD\u770B\u5230\u7684\u4FE1\u606F\u6765\u5224\u65AD\u3002
+prompt.important_tip=\u3010\u91CD\u8981\u63D0\u793A\u3011\uFF1A\u53D1\u8A00\u63A7\u5236\u57285-6\u53E5\u8BDD\u4EE5\u5185\u3002\n\
+1. \u8BF7\u7262\u8BB0\u4F60\u7684\u6E38\u620F\u89D2\u8272\u8EAB\u4EFD\uFF0C\u4F5C\u4E3A\u4E13\u4E1A\u7684\u72FC\u4EBA\u6740\u73A9\u5BB6\uFF0C\u719F\u6089\u72FC\u4EBA\u6740\u7684\u6E38\u620F\u89C4\u5219\uFF0C\u8FDB\u884C\u5408\u9002\u7684\u53D1\u8A00\n\
+2. \u4F7F\u7528\u81EA\u7136\u7684\u53E3\u8BED\u5316\u8868\u8FBE\uFF0C\u907F\u514D\u673A\u68B0\u91CD\u590D\n\
+3. \u53EF\u4EE5\u6709\u9002\u5EA6\u7684\u60C5\u7EEA\uFF1A"\u6211\u771F\u662F\u670D\u4E86\uFF0C\u8FD9\u4E48\u660E\u663E\u7684\u72FC\u4F60\u4EEC\u770B\u4E0D\u5230\u5417\uFF1F"\n\
+4. \u627F\u8BA4\u4E0D\u786E\u5B9A\u6027\uFF1A"\u6211\u73B0\u5728\u4E5F\u4E0D\u592A\u786E\u5B9A\uFF0C\u8FD8\u9700\u8981\u591A\u542C\u4E00\u8F6E"\n\
+5. \u5C55\u73B0\u601D\u8003\u8FC7\u7A0B\uFF1A"\u6211\u539F\u672C\u6000\u7591X\u53F7\uFF0C\u4F46\u4ED6\u521A\u624D\u7684\u89E3\u91CA\u6211\u89C9\u5F97\u8FD8\u884C\uFF0C\u6682\u65F6\u653E\u4E00\u653E"\n\
+6. \u9002\u5F53\u4F7F\u7528\u6E38\u620F\u9ED1\u8BDD\uFF0C\u4F46\u4E0D\u8981\u8FC7\u5EA6\n\
+
+prompt.sheriff.campaign={0}\uFF0C\u8BF7\u8FDB\u884C\u8B66\u957F\u7ADE\u9009\u53D1\u8A00
+# Prompt
+prompt.night.header=\u5F53\u524D\u662F\u7B2C{0}\u591C\n
+prompt.werewolf.discussion=\u591C\u665A\u9636\u6BB5\uFF0C\u5F53\u524D\u5B58\u6D3B\u7684\u73A9\u5BB6\u4E3A{0}\uFF0C\u4F5C\u4E3A\u719F\u7EC3\u7684\u72FC\u4EBA\u6740\u73A9\u5BB6\uFF0C\u4E0E\u4F60\u7684\u72FC\u4EBA\u540C\u4F34\u8BA8\u8BBA\u5E76\u5C31\u6D88\u706D\u8C01\u8FBE\u6210\u5171\u8BC6\uFF0C\u6839\u636E\u4E2D\u5404\u4E2A\u73A9\u5BB6\u7684\u884C\u52A8\u548C\u53D1\u8A00\u8FDB\u884C\u903B\u8F91\u63A8\u7406
+prompt.werewolf.first.night.tips=\n\u3010\u7279\u522B\u63D0\u793A\u30111\u3001\u5F53\u524D\u662F\u7B2C\u4E00\u591C\uFF0C\u6CA1\u6709\u4EFB\u4F55\u8BAF\u606F\uFF0C\u53EA\u80FD\u901A\u8FC7\u4F4D\u7F6E\u6216\u8005\u968F\u673A\u731C\u6D4B\u6765\u8FDB\u884C\u6740\u4EBA\uFF0C\u4E5F\u53EF\u4EE5\u81EA\u5200\u30022\u3001\u8FD8\u9700\u8981\u9009\u62E9\u4E00\u4E2A\u72FC\u4EBA\u961F\u53CB\u4F5C\u4E3A\u608D\u8DF3\u5019\u9009\u4EBA\uFF0C\u608D\u8DF3\u5019\u9009\u4EBA\u5C06\u5728\u7B2C\u4E8C\u5929\u767D\u5929\u4E0A\u8B66\u3002
+prompt.werewolf.voting.header=\u72FC\u4EBA\u6295\u7968\u9636\u6BB5\u3002\u73B0\u5728\u6295\u7968\u9009\u62E9\u4F60\u4EEC\u4ECA\u665A\u8981\u6D88\u706D\u7684\u73A9\u5BB6\uFF0C\u53EF\u9009\u73A9\u5BB6\u4E3A{0}
+prompt.werewolf.voting.footer=\u4F60\u5FC5\u987B\u9009\u62E9\u4E00\u540D\u73A9\u5BB6\u6D88\u706D\uFF0C\u5982\u679C\u5F53\u524D\u662F\u7B2C\u4E00\u591C\uFF0C\u8FD8\u9700\u8981\u9009\u62E9\u4E00\u4E2A\u72FC\u4EBA\u961F\u53CB\u4F5C\u4E3A\u608D\u8DF3\u5019\u9009\u4EBA\u3002\u608D\u8DF3\u5019\u9009\u4EBA\u5C06\u5728\u7B2C\u4E8C\u5929\u767D\u5929\u4E0A\u8B66\u3002
+prompt.witch.heal=\u5973\u5DEB\uFF0C\u8BF7\u7741\u5F00\u773C\u775B\u3002\u4ECA\u665A\uFF0C{0}\u88AB\u72FC\u4EBA\u51FB\u6740\u4E86\u3002\u4F60\u6709\u4E00\u74F6\u89E3\u836F\u3002\u4F60\u60F3\u7528\u5B83\u6765\u6551{0}\u5417\uFF1F\u8BB0\u4F4F\uFF1A\u8FD9\u74F6\u836F\u5728\u6574\u4E2A\u6E38\u620F\u4E2D\u53EA\u80FD\u4F7F\u7528\u4E00\u6B21\u3002
+prompt.witch.poison.header.healed=\u4F60\u5DF2\u7ECF\u4F7F\u7528\u4E86\u89E3\u836F\u3002\n\n
+prompt.witch.poison.header=\u4F60\u6709\u4E00\u74F6\u6BD2\u836F\u3002\u4F60\u60F3\u7528\u5B83\u5728\u4ECA\u665A\u6BD2\u6B7B\u67D0\u4EBA\u5417\uFF1F\u53EF\u9009\u73A9\u5BB6\u4E3A{0}
+prompt.witch.poison.footer=\n\u8BB0\u4F4F\uFF1A\u8FD9\u74F6\u836F\u5728\u6574\u4E2A\u6E38\u620F\u4E2D\u53EA\u80FD\u4F7F\u7528\u4E00\u6B21\u3002
+prompt.seer.check.header=\u9884\u8A00\u5BB6\uFF0C\u8BF7\u7741\u5F00\u773C\u775B\uFF0C\u9009\u62E9\u4E00\u540D\u73A9\u5BB6\u6765\u67E5\u9A8C\u4ED6\u4EEC\u7684\u8EAB\u4EFD\u3002\u53EF\u9009\u73A9\u5BB6\u4E3A{0}
+prompt.seer.result=\u4F60\u67E5\u9A8C\u4E86{0}\u3002\u4ED6{1}\u3002
+prompt.seer.is.werewolf=\u662F\u72FC\u4EBA
+prompt.seer.not.werewolf=\u4E0D\u662F\u72FC\u4EBA
+prompt.night.result.header=\u767D\u5929\u9636\u6BB5 - \u6240\u6709\u4EBA\u8BF7\u7741\u5F00\u773C\u775B\u3002
+prompt.night.result.peaceful=\u6628\u665A\u662F\u5E73\u5B89\u591C\u3002\u6CA1\u6709\u4EBA\u6B7B\u4EA1\u3002
+prompt.night.result.peaceful.healed=\u6628\u665A\u662F\u5E73\u5B89\u591C\u3002\u6CA1\u6709\u4EBA\u6B7B\u4EA1\u3002\n\uFF08\u5973\u5DEB\u4F7F\u7528\u4E86\u89E3\u836F\uFF09\n
+prompt.night.result.deaths=\u6628\u665A\uFF0C\u4EE5\u4E0B\u73A9\u5BB6\u6B7B\u4EA1\uFF1A\n
+prompt.night.result.alive=\u5B58\u6D3B\u73A9\u5BB6\uFF08{0}\u4EBA\uFF09\uFF1A\n
+prompt.discussion.header=\u8BA8\u8BBA\u73AF\u8282,\u5F53\u524D\u7684\u53D1\u8A00\u987A\u5E8F\u662F\uFF1A{0},\u8BF7\u6839\u636E\u987A\u5E8F\u4F9D\u6B21\u53D1\u8A00\uFF0C\u901A\u8FC72-3\u53E5\u8BDD\u5206\u4EAB\u4F60\u7684\u60F3\u6CD5\u3001\u6000\u7591\u548C\u89C2\u5BDF\u3002\u4F5C\u4E3A\u4E13\u4E1A\u7684\u72FC\u4EBA\u6740\u73A9\u5BB6\uFF0C\u6839\u636E\u7684\u4FE1\u606F\
+\u5206\u6790\u5404\u4E2A\u73A9\u5BB6\u7684\u53D1\u8A00\u4E0E\u884C\u4E3A\uFF0C\u53C2\u8003\u5176\u4ED6\u73A9\u5BB6\u7684\u53D1\u8A00\uFF0C\u4F46\u662F\u4E0D\u8981\u4EBA\u4E91\u4EA6\u4E91\uFF0C\u8BF7\u4FDD\u7559\u81EA\u5DF1\u72EC\u7ACB\u7684\u601D\u8003\uFF0C\u540C\u65F6\u5173\u6CE8\u9884\u8A00\u5BB6\u7684\u8B66\u5FBD\u6D41\u4E0E\u8B66\u5FBD\u7684\u79FB\u4EA4\u3002\
+\u3010\u53D1\u8A00\u6CE8\u610F\u4E8B\u9879\u3011\u8F93\u51FA\u7ED3\u679C\u5C06\u539F\u6837\u53D1\u9001\u7ED9\u6240\u6709\u5176\u4ED6\u73A9\u5BB6\uFF0C!\u4E25\u7981\u901A\u8FC7\u589E\u52A0\u63CF\u8FF0\u6027\u4FE1\u606F\u6CC4\u9732\u89D2\u8272\u8EAB\u4EFD,\u6BD4\u5982\uFF08\u4F5C\u4E3Axx\u53F7\u4F5C\u4E3A\u72FC\u4EBA\uFF09\n
+prompt.voting.header=\u6295\u7968\u9636\u6BB5\n\
+============================================================\n\
+\u6295\u7968\u9009\u62E9\u4F60\u60F3\u8981\u6DD8\u6C70\u7684\u73A9\u5BB6\u3002\n\
+\u53EF\u9009\u73A9\u5BB6\uFF1A\n
+prompt.voting.footer=\u4F60\u5FC5\u987B\u6295\u7968\u7ED9\u4E00\u540D\u73A9\u5BB6\u3002
+prompt.hunter.header={0}\uFF0C\u4F60\u662F\u730E\u4EBA\uFF0C\u4F60\u5DF2\u7ECF\u88AB\u6DD8\u6C70\u4E86\u3002\n\
+\u4F60\u53EF\u4EE5\u5728\u6B7B\u524D\u9009\u62E9\u5C04\u6740\u4E00\u540D\u73A9\u5BB6\u3002\n\
+\u53EF\u9009\u76EE\u6807\uFF1A\n
+prompt.hunter.footer=\n\u4F60\u60F3\u5F00\u67AA\u5417\uFF1F\u5982\u679C\u662F\uFF0C\u4F60\u60F3\u5C04\u6740\u8C01\uFF1F
# Human Player Prompts
-prompt.werewolf.discussion=请与你的狼人同伴讨论今晚要杀谁
-prompt.werewolf.vote=请选择今晚要杀死的玩家
-prompt.witch.heal=今晚 {0} 被狼人杀了。你要使用解药救他/她吗?(yes/no)
-prompt.witch.poison=你要使用毒药吗?选择一个玩家或选择 skip 跳过
-prompt.seer.check=选择一个玩家来查验他的身份
-prompt.day.discussion=请发表你的看法
-prompt.day.vote=请选择你要投票淘汰的玩家
-prompt.hunter.shoot=你被淘汰了!你要开枪带走一个玩家吗?选择玩家或 skip 跳过
+prompt.werewolf.vote=\u8BF7\u9009\u62E9\u4ECA\u665A\u8981\u6740\u6B7B\u7684\u73A9\u5BB6\u3002\u5982\u679C\u662F\u7B2C\u4E00\u591C\uFF0C\u8FD8\u9700\u8981\u9009\u62E9\u4E00\u4E2A\u72FC\u4EBA\u961F\u53CB\u4F5C\u4E3A\u608D\u8DF3\u5019\u9009\u4EBA\u3002\u608D\u8DF3\u5019\u9009\u4EBA\u5C06\u5728\u7B2C\u4E8C\u5929\u767D\u5929\u4E0A\u8B66\u3002
+prompt.witch.heal=\u4ECA\u665A {0} \u88AB\u72FC\u4EBA\u6740\u4E86\u3002\u4F60\u8981\u4F7F\u7528\u89E3\u836F\u6551\u4ED6/\u5979\u5417\uFF1F(yes/no)
+prompt.witch.poison=\u4F60\u8981\u4F7F\u7528\u6BD2\u836F\u5417\uFF1F\u9009\u62E9\u4E00\u4E2A\u73A9\u5BB6\u6216\u9009\u62E9 skip \u8DF3\u8FC7
+prompt.seer.check=\u9009\u62E9\u4E00\u4E2A\u73A9\u5BB6\u6765\u67E5\u9A8C\u4ED6\u7684\u8EAB\u4EFD
+prompt.day.discussion=\u8BF7\u53D1\u8868\u4F60\u7684\u770B\u6CD5
+prompt.day.vote=\u8BF7\u9009\u62E9\u4F60\u8981\u6295\u7968\u6DD8\u6C70\u7684\u73A9\u5BB6
+prompt.hunter.shoot=\u4F60\u88AB\u6DD8\u6C70\u4E86\uFF01\u4F60\u8981\u5F00\u67AA\u5E26\u8D70\u4E00\u4E2A\u73A9\u5BB6\u5417\uFF1F\u9009\u62E9\u73A9\u5BB6\u6216 skip \u8DF3\u8FC7
+# Sheriff Election Messages
+system.no.candidates=\u6CA1\u6709\u73A9\u5BB6\u4E0A\u8B66\uFF0C\u8DF3\u8FC7\u8B66\u957F\u7ADE\u9009
+system.campaign.start=\u3010\u7ADE\u9009\u53D1\u8A00\u3011\u4E0A\u8B66\u73A9\u5BB6\u5F00\u59CB\u7ADE\u9009\u53D1\u8A00
+system.sheriff.voting.start=\u3010\u6295\u7968\u4E0A\u8B66\u3011\u975E\u7ADE\u9009\u73A9\u5BB6\u5F00\u59CB\u6295\u7968\u9009\u62E9\u8B66\u957F
+system.sheriff.transfer.start=\u3010\u8B66\u5FBD\u79FB\u4EA4\u3011\u8B66\u957F\u79BB\u573A\uFF0C\u53EF\u4EE5\u79FB\u4EA4\u8B66\u5FBD
+system.speak.order.decision={0} \u4F5C\u4E3A\u8B66\u957F\u51B3\u5B9A\u53D1\u8A00\u987A\u5E8F\uFF1A{1}
+# Sheriff Prompts
+prompt.sheriff.election.start=\u3010\u8B66\u957F\u7ADE\u9009\u3011\u767D\u5929\u9636\u6BB5\u5F00\u59CB\uFF0C\u73B0\u5728\u8FDB\u5165\u8B66\u957F\u7ADE\u9009\u73AF\u8282
+prompt.sheriff.register=\u767D\u5929\u9636\u6BB5\uFF0C\u4F60\u662F\u5426\u53C2\u4E0E\u7ADE\u9009\u8B66\u957F\uFF1F
+prompt.sheriff.vote=\u8BF7\u6295\u7968\u7ED9\u4F60\u5FC3\u76EE\u4E2D\u7684\u8B66\u957F
+prompt.speak.order=\u4F5C\u4E3A\u8B66\u957F\uFF0C\u8BF7\u51B3\u5B9A\u4ECA\u5929\u7684\u53D1\u8A00\u987A\u5E8F (normal=\u6B63\u5E8F/reversed=\u9006\u5E8F)
+prompt.speak.order.from.position={0}\uFF0C\u4F60\u63A5\u6536\u4E86\u8B66\u5FBD\u6210\u4E3A\u65B0\u8B66\u957F\uFF0C\u8BF7\u4ECE\u4F60\u7684\u4F4D\u7F6E\u51B3\u5B9A\u4ECA\u5929\u7684\u53D1\u8A00\u987A\u5E8F (normal=\u987A\u5E8F/reversed=\u9006\u5E8F)
+prompt.sheriff.transfer=\u4F60\u53EF\u4EE5\u79FB\u4EA4\u8B66\u5FBD\u7ED9\u53E6\u4E00\u540D\u73A9\u5BB6\uFF0C\u6216\u9009\u62E9 skip \u4E0D\u79FB\u4EA4
+prompt.sheriff.voting.header=\u3010\u8B66\u5FBD\u6295\u7968\u3011\u8BF7\u6295\u7968\u7ED9\u4F60\u5FC3\u76EE\u4E2D\u7684\u8B66\u957F\u5019\u9009\u4EBA\u3002\n\n\u5019\u9009\u4EBA\uFF1A\n
+prompt.sheriff.voting.footer=\n\u4F60\u5FC5\u987B\u6295\u7968\u7ED9\u5176\u4E2D\u4E00\u540D\u5019\u9009\u4EBA\u3002
+prompt.sheriff.speak.order={0}\uFF0C\u4F5C\u4E3A\u8B66\u957F\uFF0C\u4F60\u53EF\u4EE5\u51B3\u5B9A\u4ECA\u5929\u8BA8\u8BBA\u9636\u6BB5\u7684\u53D1\u8A00\u987A\u5E8F\u3002\u6B63\u5E8F\u8868\u793A\u4ECE\u4F60\u5F00\u59CB\u987A\u65F6\u9488\u53D1\u8A00\uFF0C\u9006\u5E8F\u8868\u793A\u4ECE\u4F60\u5F00\u59CB\u9006\u65F6\u9488\u53D1\u8A00\u3002
+prompt.sheriff.speak.order.from.position={0}\uFF0C\u4F60\u521A\u63A5\u6536\u4E86\u8B66\u5FBD\u6210\u4E3A\u65B0\u8B66\u957F\u3002\u8BF7\u4ECE\u4F60\u7684\u4F4D\u7F6E\u51B3\u5B9A\u4ECA\u5929\u7684\u53D1\u8A00\u987A\u5E8F\uFF1A\u987A\u5E8F\u8FD8\u662F\u9006\u5E8F\uFF1F
+prompt.sheriff.transfer.seer={0}\uFF0C\u4F60\u662F\u9884\u8A00\u5BB6\u8B66\u957F\uFF0C\u73B0\u5728\u8981\u79BB\u573A\u4E86\u3002\u4F60\u53EF\u4EE5\u9009\u62E9\u79FB\u4EA4\u8B66\u5FBD\u7ED9\u4F60\u9A8C\u51FA\u7684\u597D\u4EBA\uFF0C\u6216\u901A\u8FC7\u8B66\u5FBD\u4F20\u9012\u4F60\u7684\u9A8C\u4EBA\u4FE1\u606F\uFF08\u8B66\u5FBD\u6D41\uFF09\u3002
+\u5B58\u6D3B\u73A9\u5BB6\uFF1A
+prompt.sheriff.transfer.default={0}\uFF0C\u4F5C\u4E3A\u8B66\u957F\uFF0C\u4F60\u73B0\u5728\u8981\u79BB\u573A\u4E86\u3002\u4F60\u53EF\u4EE5\u9009\u62E9\u79FB\u4EA4\u8B66\u5FBD\u7ED9\u4F60\u4FE1\u4EFB\u7684\u4EBA\uFF0C\u6216\u9009\u62E9 skip \u4E0D\u79FB\u4EA4\u3002\n\n\u5B58\u6D3B\u73A9\u5BB6\uFF1A\n
+prompt.sheriff.transfer.footer=\n\u9009\u62E9\u4E00\u540D\u73A9\u5BB6\u79FB\u4EA4\u8B66\u5FBD\uFF0C\u6216\u9009\u62E9 skip\u3002
+# Sheriff Errors
+error.sheriff.registration=\u8B66\u957F\u4E0A\u8B66\u51B3\u5B9A\u9519\u8BEF: {0}
+error.sheriff.campaign=\u8B66\u957F\u7ADE\u9009\u53D1\u8A00\u9519\u8BEF: {0}
+error.sheriff.vote=\u8B66\u957F\u6295\u7968\u9519\u8BEF: {0}
+error.speak.order=\u53D1\u8A00\u987A\u5E8F\u51B3\u5B9A\u9519\u8BEF: {0}
+error.sheriff.transfer=\u8B66\u5FBD\u79FB\u4EA4\u9519\u8BEF: {0}
diff --git a/agentscope-examples/werewolf-hitl/src/main/resources/static/css/style.css b/agentscope-examples/werewolf-hitl/src/main/resources/static/css/style.css
index 098df2ed9..abec3db5a 100644
--- a/agentscope-examples/werewolf-hitl/src/main/resources/static/css/style.css
+++ b/agentscope-examples/werewolf-hitl/src/main/resources/static/css/style.css
@@ -138,8 +138,14 @@ body {
}
@keyframes badgeReveal {
- from { opacity: 0; transform: scale(0.8); }
- to { opacity: 1; transform: scale(1); }
+ from {
+ opacity: 0;
+ transform: scale(0.8);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
}
.my-role-icon {
@@ -157,11 +163,25 @@ body {
color: #fff;
}
-.my-role-badge .my-role-name.werewolf { color: #f87171; }
-.my-role-badge .my-role-name.seer { color: #c084fc; }
-.my-role-badge .my-role-name.witch { color: #4ade80; }
-.my-role-badge .my-role-name.hunter { color: #fbbf24; }
-.my-role-badge .my-role-name.villager { color: #60a5fa; }
+.my-role-badge .my-role-name.werewolf {
+ color: #f87171;
+}
+
+.my-role-badge .my-role-name.seer {
+ color: #c084fc;
+}
+
+.my-role-badge .my-role-name.witch {
+ color: #4ade80;
+}
+
+.my-role-badge .my-role-name.hunter {
+ color: #fbbf24;
+}
+
+.my-role-badge .my-role-name.villager {
+ color: #60a5fa;
+}
.my-role-badge .teammates-info {
font-size: 0.8rem;
@@ -207,6 +227,53 @@ body {
box-shadow: 0 0 15px rgba(34, 197, 94, 0.4);
}
+/* Raise hand icon for sheriff registration */
+.raise-hand-icon {
+ position: absolute;
+ bottom: -12px;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: 1.2rem;
+ animation: raiseHand 0.6s ease-out;
+ z-index: 10;
+}
+
+@keyframes raiseHand {
+ 0% {
+ transform: translateX(-50%) translateY(10px);
+ opacity: 0;
+ }
+ 100% {
+ transform: translateX(-50%) translateY(0);
+ opacity: 1;
+ }
+}
+
+/* Sheriff badge icon */
+.sheriff-badge {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ font-size: 1.4rem;
+ animation: sheriffBadgeAppear 0.5s ease-out;
+ z-index: 10;
+ filter: drop-shadow(0 2px 4px rgba(255, 215, 0, 0.6));
+}
+
+@keyframes sheriffBadgeAppear {
+ 0% {
+ transform: scale(0) rotate(-180deg);
+ opacity: 0;
+ }
+ 60% {
+ transform: scale(1.2) rotate(10deg);
+ }
+ 100% {
+ transform: scale(1) rotate(0deg);
+ opacity: 1;
+ }
+}
+
.player-name {
font-size: 0.9rem;
font-weight: bold;
@@ -222,11 +289,30 @@ body {
font-weight: 500;
}
-.player-role.villager { background: rgba(59, 130, 246, 0.3); color: #60a5fa; }
-.player-role.werewolf { background: rgba(239, 68, 68, 0.3); color: #f87171; }
-.player-role.seer { background: rgba(168, 85, 247, 0.3); color: #c084fc; }
-.player-role.witch { background: rgba(34, 197, 94, 0.3); color: #4ade80; }
-.player-role.hunter { background: rgba(251, 191, 36, 0.3); color: #fbbf24; }
+.player-role.villager {
+ background: rgba(59, 130, 246, 0.3);
+ color: #60a5fa;
+}
+
+.player-role.werewolf {
+ background: rgba(239, 68, 68, 0.3);
+ color: #f87171;
+}
+
+.player-role.seer {
+ background: rgba(168, 85, 247, 0.3);
+ color: #c084fc;
+}
+
+.player-role.witch {
+ background: rgba(34, 197, 94, 0.3);
+ color: #4ade80;
+}
+
+.player-role.hunter {
+ background: rgba(251, 191, 36, 0.3);
+ color: #fbbf24;
+}
/* Role hidden before game end */
.player-role.hidden {
@@ -326,8 +412,14 @@ body {
}
@keyframes modalSlideIn {
- from { opacity: 0; transform: scale(0.9) translateY(-20px); }
- to { opacity: 1; transform: scale(1) translateY(0); }
+ from {
+ opacity: 0;
+ transform: scale(0.9) translateY(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
}
.modal-title {
@@ -440,6 +532,13 @@ body {
text-align: center;
}
+.config-form-modal .config-input[readonly] {
+ background: rgba(255, 255, 255, 0.03);
+ border-color: rgba(255, 255, 255, 0.1);
+ color: #94a3b8;
+ cursor: not-allowed;
+}
+
.config-form-modal .config-input:focus {
outline: none;
border-color: #a855f7;
@@ -602,8 +701,14 @@ body {
}
@keyframes fadeIn {
- from { opacity: 0; transform: translateY(10px); }
- to { opacity: 1; transform: translateY(0); }
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
.log-entry.system {
@@ -643,6 +748,13 @@ body {
color: #f87171;
}
+/* Highlight for sheriff voting */
+.highlight-vote {
+ color: #ef4444;
+ font-weight: bold;
+ text-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
+}
+
.btn {
padding: 10px 20px;
border: none;
@@ -716,8 +828,12 @@ body {
}
@keyframes pulse {
- 0%, 100% { box-shadow: 0 0 10px rgba(34, 197, 94, 0.2); }
- 50% { box-shadow: 0 0 25px rgba(34, 197, 94, 0.4); }
+ 0%, 100% {
+ box-shadow: 0 0 10px rgba(34, 197, 94, 0.2);
+ }
+ 50% {
+ box-shadow: 0 0 25px rgba(34, 197, 94, 0.4);
+ }
}
#input-title {
@@ -771,13 +887,978 @@ body {
.input-text-area textarea {
width: 100%;
- padding: 10px;
+ min-height: 120px;
+ padding: 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
background: rgba(255, 255, 255, 0.05);
color: #e0e0e0;
+ font-size: 1rem;
+ line-height: 1.5;
+ resize: vertical;
+ font-family: inherit;
+}
+
+.input-text-area textarea:focus {
+ outline: none;
+ border-color: #22c55e;
+}
+
+/* ==================== Responsive ==================== */
+@media (max-width: 900px) {
+ .log-input-container {
+ flex-direction: column;
+ }
+
+ .input-card {
+ width: 100%;
+ }
+}
+
+@media (max-width: 800px) {
+ .players-row {
+ gap: 6px;
+ justify-content: flex-start;
+ padding: 0 10px;
+ }
+
+ .player-card {
+ padding: 6px 10px;
+ min-width: 50px;
+ }
+
+ .player-name {
+ font-size: 0.8rem;
+ }
+
+ .player-role {
+ font-size: 0.6rem;
+ padding: 2px 6px;
+ }
+
+ .title {
+ font-size: 1.8rem;
+ }
+
+ .log-card {
+ min-height: 350px;
+ }
+
+ .status-control-bar {
+ flex-direction: column;
+ gap: 15px;
+ }
+
+ .control-buttons {
+ width: 100%;
+ justify-content: center;
+ }
+
+ .btn {
+ padding: 10px 16px;
+ font-size: 0.9rem;
+ }
+}
+
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/* ==================== Base Styles ==================== */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
+ min-height: 100vh;
+ color: #e0e0e0;
+}
+
+.container {
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 20px;
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+/* ==================== Header ==================== */
+.header {
+ text-align: center;
+ padding: 15px 0 20px;
+}
+
+.header-controls {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 20px;
+ margin-top: 10px;
+}
+
+.title {
+ font-size: 2.5rem;
+ background: linear-gradient(90deg, #a855f7, #6366f1);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+ margin-bottom: 10px;
+}
+
+.language-switch {
+ display: flex;
+ gap: 5px;
+ background: rgba(255, 255, 255, 0.1);
+ padding: 3px;
+ border-radius: 20px;
+}
+
+.lang-btn {
+ padding: 5px 12px;
+ border: none;
+ border-radius: 15px;
+ background: transparent;
+ color: #94a3b8;
+ cursor: pointer;
font-size: 0.9rem;
+ transition: all 0.2s ease;
+}
+
+.lang-btn:hover {
+ color: #e0e0e0;
+}
+
+.lang-btn.active {
+ background: rgba(168, 85, 247, 0.5);
+ color: #fff;
+}
+
+.lang-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.phase-info {
+ font-size: 1.1rem;
+ color: #94a3b8;
+}
+
+#round-info {
+ padding: 5px 15px;
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 20px;
+}
+
+/* ==================== Players Bar (Horizontal) ==================== */
+.players-bar {
+ margin-bottom: 20px;
+ overflow-x: auto;
+ padding-bottom: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 15px;
+}
+
+.players-row {
+ display: flex;
+ justify-content: center;
+ gap: 10px;
+ flex-wrap: nowrap;
+ min-width: min-content;
+}
+
+/* My Role Badge - Compact display */
+.my-role-badge {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ background: linear-gradient(135deg, rgba(168, 85, 247, 0.2), rgba(99, 102, 241, 0.2));
+ border: 2px solid rgba(168, 85, 247, 0.5);
+ border-radius: 25px;
+ padding: 8px 16px;
+ backdrop-filter: blur(10px);
+ animation: badgeReveal 0.5s ease;
+ flex-shrink: 0;
+}
+
+@keyframes badgeReveal {
+ from {
+ opacity: 0;
+ transform: scale(0.8);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.my-role-icon {
+ font-size: 1.2rem;
+}
+
+.my-role-label {
+ font-size: 0.85rem;
+ color: #94a3b8;
+}
+
+.my-role-badge .my-role-name {
+ font-size: 1rem;
+ font-weight: bold;
+ color: #fff;
+}
+
+.my-role-badge .my-role-name.werewolf {
+ color: #f87171;
+}
+
+.my-role-badge .my-role-name.seer {
+ color: #c084fc;
+}
+
+.my-role-badge .my-role-name.witch {
+ color: #4ade80;
+}
+
+.my-role-badge .my-role-name.hunter {
+ color: #fbbf24;
+}
+
+.my-role-badge .my-role-name.villager {
+ color: #60a5fa;
+}
+
+.my-role-badge .teammates-info {
+ font-size: 0.8rem;
+ color: #f87171;
+ margin-left: 5px;
+}
+
+/* Player Card - Compact horizontal style */
+.player-card {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 10px;
+ padding: 8px 12px;
+ text-align: center;
+ position: relative;
+ transition: all 0.3s ease;
+ backdrop-filter: blur(10px);
+ min-width: 50px;
+ flex-shrink: 0;
+}
+
+.player-card:hover {
+ transform: translateY(-2px);
+ border-color: rgba(168, 85, 247, 0.5);
+ box-shadow: 0 5px 15px rgba(168, 85, 247, 0.2);
+}
+
+.player-card.dead {
+ opacity: 0.5;
+ filter: grayscale(0.8);
+}
+
+.player-card.dead::after {
+ content: '☠️';
+ position: absolute;
+ top: -8px;
+ right: -5px;
+ font-size: 0.8rem;
+}
+
+.player-card.speaking {
+ border-color: #22c55e;
+ box-shadow: 0 0 15px rgba(34, 197, 94, 0.4);
+}
+
+/* Raise hand icon for sheriff registration */
+.raise-hand-icon {
+ position: absolute;
+ bottom: -12px;
+ left: 50%;
+ transform: translateX(-50%);
+ font-size: 1.2rem;
+ animation: raiseHand 0.6s ease-out;
+ z-index: 10;
+}
+
+@keyframes raiseHand {
+ 0% {
+ transform: translateX(-50%) translateY(10px);
+ opacity: 0;
+ }
+ 100% {
+ transform: translateX(-50%) translateY(0);
+ opacity: 1;
+ }
+}
+
+/* Sheriff badge icon */
+.sheriff-badge {
+ position: absolute;
+ top: -8px;
+ right: -8px;
+ font-size: 1.4rem;
+ animation: sheriffBadgeAppear 0.5s ease-out;
+ z-index: 10;
+ filter: drop-shadow(0 2px 4px rgba(255, 215, 0, 0.6));
+}
+
+@keyframes sheriffBadgeAppear {
+ 0% {
+ transform: scale(0) rotate(-180deg);
+ opacity: 0;
+ }
+ 60% {
+ transform: scale(1.2) rotate(10deg);
+ }
+ 100% {
+ transform: scale(1) rotate(0deg);
+ opacity: 1;
+ }
+}
+
+.player-name {
+ font-size: 0.9rem;
+ font-weight: bold;
+ margin-bottom: 4px;
+ color: #fff;
+}
+
+.player-role {
+ font-size: 0.65rem;
+ padding: 2px 8px;
+ border-radius: 8px;
+ display: inline-block;
+ font-weight: 500;
+}
+
+.player-role.villager {
+ background: rgba(59, 130, 246, 0.3);
+ color: #60a5fa;
+}
+
+.player-role.werewolf {
+ background: rgba(239, 68, 68, 0.3);
+ color: #f87171;
+}
+
+.player-role.seer {
+ background: rgba(168, 85, 247, 0.3);
+ color: #c084fc;
+}
+
+.player-role.witch {
+ background: rgba(34, 197, 94, 0.3);
+ color: #4ade80;
+}
+
+.player-role.hunter {
+ background: rgba(251, 191, 36, 0.3);
+ color: #fbbf24;
+}
+
+/* Role hidden before game end */
+.player-role.hidden {
+ background: rgba(100, 116, 139, 0.3);
+ color: #94a3b8;
+}
+
+/* ==================== Main Content ==================== */
+.main-content {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ flex: 1;
+ max-width: 1000px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+/* Status and Control Bar (left-right layout) */
+.status-control-bar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 20px;
+}
+
+.control-area {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ align-items: flex-end;
+}
+
+.control-buttons {
+ display: flex;
+ gap: 10px;
+ flex-shrink: 0;
+ flex-wrap: wrap;
+}
+
+.btn-config {
+ background: rgba(168, 85, 247, 0.2);
+ color: #c084fc;
+ border: 1px solid rgba(168, 85, 247, 0.4);
+}
+
+.btn-config:hover:not(:disabled) {
+ background: rgba(168, 85, 247, 0.3);
+ border-color: rgba(168, 85, 247, 0.6);
+ transform: scale(1.02);
+}
+
+.btn-config.active {
+ background: rgba(168, 85, 247, 0.4);
+ border-color: rgba(168, 85, 247, 0.8);
+ box-shadow: 0 0 10px rgba(168, 85, 247, 0.3);
+}
+
+.btn-config:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Configuration Form in Modal */
+.config-form-modal {
+ display: flex;
+ flex-direction: column;
+ gap: 15px;
+ margin-bottom: 25px;
+ min-width: 300px;
+}
+
+/* ==================== Role Selection Modal ==================== */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ backdrop-filter: blur(5px);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1000;
+ animation: fadeIn 0.2s ease;
+}
+
+.role-selector-modal {
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
+ border: 2px solid rgba(168, 85, 247, 0.5);
+ border-radius: 20px;
+ padding: 30px 40px;
+ text-align: center;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+ animation: modalSlideIn 0.3s ease;
+}
+
+@keyframes modalSlideIn {
+ from {
+ opacity: 0;
+ transform: scale(0.9) translateY(-20px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+.modal-title {
+ font-size: 1.5rem;
+ color: #fff;
+ margin-bottom: 25px;
+ background: linear-gradient(90deg, #a855f7, #6366f1);
+ -webkit-background-clip: text;
+ -webkit-text-fill-color: transparent;
+ background-clip: text;
+}
+
+.role-options {
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+ flex-wrap: wrap;
+ margin-bottom: 25px;
+}
+
+.role-option-btn {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 5px;
+ padding: 15px 20px;
+ border: 2px solid rgba(255, 255, 255, 0.2);
+ border-radius: 15px;
+ background: rgba(255, 255, 255, 0.05);
+ color: #94a3b8;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-width: 80px;
+}
+
+.role-option-btn:hover {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(168, 85, 247, 0.5);
+ color: #e0e0e0;
+ transform: translateY(-3px);
+ box-shadow: 0 5px 20px rgba(168, 85, 247, 0.3);
+}
+
+.role-option-btn.selected {
+ background: rgba(168, 85, 247, 0.2);
+ border-color: #a855f7;
+ color: #c084fc;
+}
+
+.role-option-icon {
+ font-size: 2rem;
+}
+
+.role-option-name {
+ font-size: 0.85rem;
+ font-weight: 500;
+}
+
+.btn-cancel {
+ background: rgba(100, 116, 139, 0.3);
+ color: #94a3b8;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ padding: 8px 25px;
+ font-size: 0.9rem;
+}
+
+.btn-cancel:hover {
+ background: rgba(100, 116, 139, 0.5);
+ color: #e0e0e0;
+}
+
+/* Cards */
+.card {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 16px;
+ padding: 20px;
+ backdrop-filter: blur(10px);
+}
+
+/* Config Form (used in config modal) */
+.config-form-modal .config-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 15px;
+ padding: 10px 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.config-form-modal .config-row:last-of-type {
+ border-bottom: none;
+}
+
+.config-form-modal .config-row label {
+ color: #e0e0e0;
+ font-size: 1rem;
+ flex: 1;
+ text-align: left;
+}
+
+.config-form-modal .config-input {
+ width: 100px;
+ padding: 8px 12px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.05);
+ color: #e0e0e0;
+ font-size: 1rem;
+ text-align: center;
+}
+
+.config-form-modal .config-input[readonly] {
+ background: rgba(255, 255, 255, 0.03);
+ border-color: rgba(255, 255, 255, 0.1);
+ color: #94a3b8;
+ cursor: not-allowed;
+}
+
+.config-form-modal .config-input:focus {
+ outline: none;
+ border-color: #a855f7;
+ background: rgba(255, 255, 255, 0.08);
+ box-shadow: 0 0 10px rgba(168, 85, 247, 0.3);
+}
+
+.config-form-modal .config-total {
+ margin-top: 10px;
+ padding-top: 15px;
+ border-top: 2px solid rgba(168, 85, 247, 0.3);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-weight: bold;
+ font-size: 1.1rem;
+ color: #60a5fa;
+}
+
+.config-form-modal .config-total span:first-child {
+ color: #94a3b8;
+}
+
+.config-form-modal #config-total-count {
+ font-size: 1.3rem;
+ color: #60a5fa;
+}
+
+/* Configuration Error Message */
+.config-error {
+ margin-top: 15px;
+ padding: 12px;
+ border-radius: 8px;
+ font-size: 0.9rem;
+ line-height: 1.5;
+ display: none;
+}
+
+.config-error.error {
+ display: block;
+ background: rgba(239, 68, 68, 0.2);
+ border: 1px solid rgba(239, 68, 68, 0.5);
+ color: #f87171;
+}
+
+.config-error.success {
+ display: block;
+ background: rgba(34, 197, 94, 0.2);
+ border: 1px solid rgba(34, 197, 94, 0.5);
+ color: #4ade80;
+}
+
+.config-row {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 10px;
+}
+
+.config-row label {
+ color: #94a3b8;
+ font-size: 0.9rem;
+ flex: 1;
+}
+
+/* Status Card */
+.status-card {
+ display: flex;
+ align-items: center;
+ gap: 15px;
+ flex: 1;
+}
+
+.status-icon {
+ font-size: 2.5rem;
+}
+
+.status-text h3 {
+ font-size: 1.2rem;
+ margin-bottom: 5px;
+ color: #fff;
+}
+
+.status-text p {
+ font-size: 0.9rem;
+ color: #94a3b8;
+}
+
+/* Night/Day status */
+.status-card.night {
+ border-color: rgba(99, 102, 241, 0.5);
+ background: rgba(99, 102, 241, 0.1);
+}
+
+.status-card.day {
+ border-color: rgba(251, 191, 36, 0.5);
+ background: rgba(251, 191, 36, 0.1);
+}
+
+.status-card.end {
+ border-color: rgba(34, 197, 94, 0.5);
+ background: rgba(34, 197, 94, 0.1);
+}
+
+/* Log and Input Container */
+.log-input-container {
+ display: flex;
+ gap: 15px;
+ flex: 1;
+ min-height: 500px;
+}
+
+/* Log Card - Extended height */
+.log-card {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-height: 500px;
+ max-height: 600px;
+}
+
+.log-card h3 {
+ margin-bottom: 15px;
+ color: #fff;
+ flex-shrink: 0;
+}
+
+.log-content {
+ flex: 1;
+ overflow-y: scroll;
+ padding-right: 10px;
+ max-height: 520px;
+}
+
+.log-content::-webkit-scrollbar {
+ width: 8px;
+}
+
+.log-content::-webkit-scrollbar-track {
+ background: rgba(255, 255, 255, 0.1);
+ border-radius: 4px;
+}
+
+.log-content::-webkit-scrollbar-thumb {
+ background: rgba(168, 85, 247, 0.5);
+ border-radius: 4px;
+}
+
+.log-content::-webkit-scrollbar-thumb:hover {
+ background: rgba(168, 85, 247, 0.7);
+}
+
+.log-entry {
+ padding: 10px 14px;
+ margin-bottom: 10px;
+ border-radius: 8px;
+ font-size: 0.95rem;
+ line-height: 1.5;
+ animation: fadeIn 0.3s ease;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.log-entry.system {
+ background: rgba(100, 116, 139, 0.2);
+ color: #94a3b8;
+ border-left: 3px solid #64748b;
+}
+
+.log-entry.speak {
+ background: rgba(59, 130, 246, 0.1);
+ border-left: 3px solid #3b82f6;
+}
+
+.log-entry.speak .speaker {
+ color: #60a5fa;
+ font-weight: bold;
+}
+
+.log-entry.vote {
+ background: rgba(168, 85, 247, 0.1);
+ border-left: 3px solid #a855f7;
+}
+
+.log-entry.action {
+ background: rgba(34, 197, 94, 0.1);
+ border-left: 3px solid #22c55e;
+}
+
+.log-entry.eliminate {
+ background: rgba(239, 68, 68, 0.1);
+ border-left: 3px solid #ef4444;
+}
+
+.log-entry.error {
+ background: rgba(239, 68, 68, 0.2);
+ border-left: 3px solid #ef4444;
+ color: #f87171;
+}
+
+/* Highlight for sheriff voting */
+.highlight-vote {
+ color: #ef4444;
+ font-weight: bold;
+ text-shadow: 0 0 8px rgba(239, 68, 68, 0.6);
+}
+
+.btn {
+ padding: 10px 20px;
+ border: none;
+ border-radius: 30px;
+ font-size: 1.1rem;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.btn-primary {
+ background: linear-gradient(90deg, #a855f7, #6366f1);
+ color: white;
+}
+
+.btn-primary:hover:not(:disabled) {
+ transform: scale(1.05);
+ box-shadow: 0 5px 20px rgba(168, 85, 247, 0.4);
+}
+
+.btn-primary:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-secondary {
+ background: rgba(100, 116, 139, 0.3);
+ color: #e0e0e0;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+.btn-secondary:hover:not(:disabled) {
+ background: rgba(100, 116, 139, 0.5);
+ transform: scale(1.02);
+}
+
+.btn-secondary:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+
+/* Human player highlight */
+.player-card.human {
+ border: 2px solid #22c55e;
+ box-shadow: 0 0 15px rgba(34, 197, 94, 0.3);
+}
+
+.player-card.human::before {
+ content: '👤';
+ position: absolute;
+ top: -10px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #22c55e;
+ padding: 2px 6px;
+ border-radius: 10px;
+ font-size: 0.7rem;
+}
+
+/* ==================== Input Card ==================== */
+.input-card {
+ border: 2px solid rgba(34, 197, 94, 0.5);
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.1), rgba(22, 163, 74, 0.1));
+ animation: pulse 2s infinite;
+ width: 320px;
+ flex-shrink: 0;
+ align-self: flex-start;
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ box-shadow: 0 0 10px rgba(34, 197, 94, 0.2);
+ }
+ 50% {
+ box-shadow: 0 0 25px rgba(34, 197, 94, 0.4);
+ }
+}
+
+#input-title {
+ color: #4ade80;
+ font-size: 1.2rem;
+ margin-bottom: 10px;
+}
+
+.input-prompt {
+ font-size: 0.95rem;
+ color: #e0e0e0;
+ margin-bottom: 15px;
+}
+
+.input-options {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ justify-content: center;
+ margin-bottom: 10px;
+}
+
+.input-option-btn {
+ padding: 8px 16px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 15px;
+ background: rgba(255, 255, 255, 0.05);
+ color: #e0e0e0;
+ cursor: pointer;
+ font-size: 0.9rem;
+ transition: all 0.2s ease;
+}
+
+.input-option-btn:hover {
+ background: rgba(34, 197, 94, 0.2);
+ border-color: #22c55e;
+ transform: scale(1.05);
+}
+
+.input-option-btn.selected {
+ background: rgba(34, 197, 94, 0.3);
+ border-color: #22c55e;
+ color: #4ade80;
+}
+
+.input-text-area {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.input-text-area textarea {
+ width: 100%;
+ min-height: 120px;
+ padding: 12px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 10px;
+ background: rgba(255, 255, 255, 0.05);
+ color: #e0e0e0;
+ font-size: 1rem;
+ line-height: 1.5;
resize: vertical;
+ font-family: inherit;
}
.input-text-area textarea:focus {
diff --git a/agentscope-examples/werewolf-hitl/src/main/resources/static/index.html b/agentscope-examples/werewolf-hitl/src/main/resources/static/index.html
index 14115f930..6181e2c58 100644
--- a/agentscope-examples/werewolf-hitl/src/main/resources/static/index.html
+++ b/agentscope-examples/werewolf-hitl/src/main/resources/static/index.html
@@ -21,171 +21,234 @@
狼人杀 - 实时对战
+
-