ryomo commited on
Commit
ea5a5fd
·
1 Parent(s): abe8a34

feat: implement execute_turn functionality

Browse files
Files changed (2) hide show
  1. app.py +79 -2
  2. src/unpredictable_lord/game_state.py +207 -0
app.py CHANGED
@@ -15,6 +15,7 @@ from unpredictable_lord.game_state import (
15
  ADVICE_DESCRIPTIONS,
16
  PERSONALITY_DESCRIPTIONS,
17
  create_session,
 
18
  get_available_advice,
19
  get_session,
20
  )
@@ -108,6 +109,50 @@ def list_available_advice() -> dict:
108
  }
109
 
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  # Gradio UI
112
  with gr.Blocks(title="Unpredictable Lord") as demo:
113
  gr.Markdown("# Unpredictable Lord\nLord Advisor AI Simulation")
@@ -194,12 +239,15 @@ Add the following to your MCP settings configuration:
194
  | `init_game` | Initialize a new game session. Returns a session_id and available advice options. |
195
  | `get_game_state` | Get the current game state for a session. |
196
  | `list_available_advice` | Get all available advice options for execute_turn. |
 
197
 
198
  ### Usage Flow
199
 
200
  1. Call `init_game(personality)` to start a new game session
201
- 2. Use the returned `session_id` for all subsequent game operations
202
- 3. Game state is managed server-side
 
 
203
  """
204
  )
205
 
@@ -238,6 +286,35 @@ Add the following to your MCP settings configuration:
238
  fn=list_available_advice, inputs=[], outputs=advice_output
239
  )
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
  if __name__ == "__main__":
243
  demo.launch(mcp_server=True)
 
15
  ADVICE_DESCRIPTIONS,
16
  PERSONALITY_DESCRIPTIONS,
17
  create_session,
18
+ execute_turn_logic,
19
  get_available_advice,
20
  get_session,
21
  )
 
109
  }
110
 
111
 
112
+ def execute_turn(session_id: str, advice: str) -> dict:
113
+ """
114
+ Execute a turn with the given advice.
115
+
116
+ This MCP tool is called by the Lord AI after interpreting the user's suggestion.
117
+ The lord will decide whether to follow the advice based on personality and trust.
118
+
119
+ Args:
120
+ session_id: The session ID returned from init_game.
121
+ advice: The advice type to execute. Must be one of:
122
+ - "increase_tax": Raise taxes (Treasury ↑, Satisfaction ↓)
123
+ - "decrease_tax": Lower taxes (Treasury ↓, Satisfaction ↑)
124
+ - "expand_territory": Military expansion (Territory ↑, Treasury ↓, risky)
125
+ - "improve_diplomacy": Diplomatic efforts (Royal Trust ↑, Treasury ↓)
126
+ - "public_festival": Hold festival (Satisfaction ↑, Treasury ↓)
127
+ - "build_infrastructure": Build infrastructure (Population ↑, Treasury ↓)
128
+ - "do_nothing": Maintain current state
129
+
130
+ Returns:
131
+ dict: Result containing:
132
+ - adopted: Whether the lord followed the advice
133
+ - action_taken: The actual action the lord took
134
+ - parameter_changes: Changes to game parameters
135
+ - game_over: Whether the game has ended
136
+ - new_state: Updated game state
137
+ """
138
+ state = get_session(session_id)
139
+
140
+ if state is None:
141
+ return {
142
+ "error": "Session not found",
143
+ "message": f"No game session found with ID: {session_id}. Please call init_game first.",
144
+ }
145
+
146
+ result = execute_turn_logic(state, advice)
147
+
148
+ logger.info(
149
+ f"Turn executed for session {session_id}: advice={advice}, "
150
+ f"adopted={result.get('adopted')}, action={result.get('action_taken')}"
151
+ )
152
+
153
+ return result
154
+
155
+
156
  # Gradio UI
157
  with gr.Blocks(title="Unpredictable Lord") as demo:
158
  gr.Markdown("# Unpredictable Lord\nLord Advisor AI Simulation")
 
239
  | `init_game` | Initialize a new game session. Returns a session_id and available advice options. |
240
  | `get_game_state` | Get the current game state for a session. |
241
  | `list_available_advice` | Get all available advice options for execute_turn. |
242
+ | `execute_turn` | Execute a turn with the given advice. Returns whether advice was adopted and results. |
243
 
244
  ### Usage Flow
245
 
246
  1. Call `init_game(personality)` to start a new game session
247
+ 2. User gives free-form advice to the Lord AI
248
+ 3. Lord AI interprets advice and calls `execute_turn(session_id, advice)`
249
+ 4. Lord AI explains the result to the user (adopted/rejected, action taken, effects)
250
+ 5. Repeat from step 2 until game over
251
  """
252
  )
253
 
 
286
  fn=list_available_advice, inputs=[], outputs=advice_output
287
  )
288
 
289
+ gr.Markdown("### Test: Execute Turn")
290
+ with gr.Row():
291
+ exec_session_id = gr.Textbox(
292
+ label="Session ID",
293
+ placeholder="Enter session_id",
294
+ )
295
+ exec_advice = gr.Dropdown(
296
+ choices=[
297
+ "increase_tax",
298
+ "decrease_tax",
299
+ "expand_territory",
300
+ "improve_diplomacy",
301
+ "public_festival",
302
+ "build_infrastructure",
303
+ "do_nothing",
304
+ ],
305
+ value="do_nothing",
306
+ label="Advice",
307
+ )
308
+ exec_btn = gr.Button("Execute Turn")
309
+
310
+ exec_output = gr.JSON(label="Turn Result")
311
+
312
+ exec_btn.click(
313
+ fn=execute_turn,
314
+ inputs=[exec_session_id, exec_advice],
315
+ outputs=exec_output,
316
+ )
317
+
318
 
319
  if __name__ == "__main__":
320
  demo.launch(mcp_server=True)
src/unpredictable_lord/game_state.py CHANGED
@@ -5,6 +5,7 @@ This module defines the game state data structure and provides
5
  functions for managing game sessions.
6
  """
7
 
 
8
  from dataclasses import asdict, dataclass, field
9
  from typing import Literal
10
 
@@ -157,6 +158,212 @@ class GameState:
157
 
158
  return False, None
159
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
  # Global session storage
162
  game_sessions: dict[str, GameState] = {}
 
5
  functions for managing game sessions.
6
  """
7
 
8
+ import random
9
  from dataclasses import asdict, dataclass, field
10
  from typing import Literal
11
 
 
158
 
159
  return False, None
160
 
161
+ def clamp(self, value: int, min_val: int = 0, max_val: int = 100) -> int:
162
+ """Clamp a value to the valid range."""
163
+ return max(min_val, min(max_val, value))
164
+
165
+ def apply_changes(self, changes: dict[str, int]) -> dict[str, int]:
166
+ """
167
+ Apply parameter changes and return the actual changes made.
168
+
169
+ Args:
170
+ changes: Dictionary of parameter changes (can be positive or negative).
171
+
172
+ Returns:
173
+ Dictionary of actual changes applied (after clamping).
174
+ """
175
+ actual_changes = {}
176
+ for param, delta in changes.items():
177
+ if hasattr(self, param):
178
+ old_value = getattr(self, param)
179
+ new_value = self.clamp(old_value + delta)
180
+ setattr(self, param, new_value)
181
+ actual_changes[param] = new_value - old_value
182
+ return actual_changes
183
+
184
+
185
+ # Personality-based acceptance rates and biases
186
+ PERSONALITY_TRAITS = {
187
+ "cautious": {
188
+ "base_acceptance": 0.4, # Low base acceptance
189
+ "preferred": ["do_nothing", "improve_diplomacy"],
190
+ "disliked": ["expand_territory", "decrease_tax"],
191
+ "trust_weight": 0.6, # How much advisor_trust affects acceptance
192
+ },
193
+ "idealist": {
194
+ "base_acceptance": 0.5,
195
+ "preferred": ["build_infrastructure", "public_festival", "decrease_tax"],
196
+ "disliked": ["increase_tax", "do_nothing"],
197
+ "trust_weight": 0.4, # Less influenced by trust, more by ideals
198
+ },
199
+ "populist": {
200
+ "base_acceptance": 0.6, # Higher base acceptance
201
+ "preferred": ["decrease_tax", "public_festival"],
202
+ "disliked": ["increase_tax", "expand_territory"],
203
+ "trust_weight": 0.5,
204
+ },
205
+ }
206
+
207
+ # Effects of each action when adopted
208
+ ACTION_EFFECTS: dict[str, dict[str, int]] = {
209
+ "increase_tax": {
210
+ "treasury": 15,
211
+ "satisfaction": -10,
212
+ },
213
+ "decrease_tax": {
214
+ "treasury": -10,
215
+ "satisfaction": 15,
216
+ },
217
+ "expand_territory": {
218
+ "territory": 10,
219
+ "treasury": -15,
220
+ "population": 5,
221
+ # royal_trust is handled specially (can go up or down)
222
+ },
223
+ "improve_diplomacy": {
224
+ "royal_trust": 12,
225
+ "treasury": -8,
226
+ },
227
+ "public_festival": {
228
+ "satisfaction": 15,
229
+ "treasury": -12,
230
+ },
231
+ "build_infrastructure": {
232
+ "population": 10,
233
+ "treasury": -10,
234
+ "satisfaction": 5,
235
+ },
236
+ "do_nothing": {},
237
+ }
238
+
239
+ # Alternative actions when lord rejects advice
240
+ REJECTION_ALTERNATIVES = {
241
+ "increase_tax": ["do_nothing", "improve_diplomacy"],
242
+ "decrease_tax": ["do_nothing", "increase_tax"],
243
+ "expand_territory": ["do_nothing", "improve_diplomacy"],
244
+ "improve_diplomacy": ["do_nothing", "public_festival"],
245
+ "public_festival": ["do_nothing", "improve_diplomacy"],
246
+ "build_infrastructure": ["do_nothing", "improve_diplomacy"],
247
+ "do_nothing": ["do_nothing"], # Always accept do_nothing
248
+ }
249
+
250
+
251
+ def execute_turn_logic(state: "GameState", advice: str) -> dict:
252
+ """
253
+ Execute a turn based on the given advice.
254
+
255
+ This function determines whether the lord accepts the advice based on
256
+ personality and trust, applies the effects, and returns the result.
257
+
258
+ Args:
259
+ state: The current game state.
260
+ advice: The advice type chosen by the Lord AI.
261
+
262
+ Returns:
263
+ dict: Result containing adoption status, action taken, effects, and messages.
264
+ """
265
+ if advice not in ADVICE_DESCRIPTIONS:
266
+ return {
267
+ "error": "Invalid advice",
268
+ "message": f"Unknown advice type: {advice}. Use list_available_advice to see options.",
269
+ }
270
+
271
+ if state.game_over:
272
+ return {
273
+ "error": "Game over",
274
+ "message": f"This game has ended with result: {state.result}",
275
+ "game_state": state.to_dict(),
276
+ }
277
+
278
+ personality = state.lord_personality
279
+ traits = PERSONALITY_TRAITS[personality]
280
+
281
+ # Calculate acceptance probability
282
+ base_prob = traits["base_acceptance"]
283
+ trust_bonus = (state.advisor_trust / 100) * traits["trust_weight"]
284
+
285
+ if advice in traits["preferred"]:
286
+ preference_bonus = 0.2
287
+ elif advice in traits["disliked"]:
288
+ preference_bonus = -0.25
289
+ else:
290
+ preference_bonus = 0
291
+
292
+ acceptance_prob = min(0.95, max(0.1, base_prob + trust_bonus + preference_bonus))
293
+
294
+ # Determine if advice is adopted
295
+ adopted = random.random() < acceptance_prob
296
+
297
+ if adopted:
298
+ action_taken = advice
299
+ adoption_message = f"The lord has decided to follow your advice: {ADVICE_DESCRIPTIONS[advice]['name']}."
300
+ else:
301
+ # Lord rejects and takes alternative action
302
+ alternatives = REJECTION_ALTERNATIVES.get(advice, ["do_nothing"])
303
+ action_taken = random.choice(alternatives)
304
+ if action_taken == advice:
305
+ action_taken = "do_nothing"
306
+ adoption_message = (
307
+ f"The lord has rejected your advice ({ADVICE_DESCRIPTIONS[advice]['name']}) "
308
+ f"and instead chose: {ADVICE_DESCRIPTIONS[action_taken]['name']}."
309
+ )
310
+
311
+ # Apply effects of the action taken
312
+ effects = ACTION_EFFECTS.get(action_taken, {}).copy()
313
+
314
+ # Special handling for expand_territory (risky)
315
+ if action_taken == "expand_territory":
316
+ if random.random() < 0.6: # 60% success
317
+ effects["royal_trust"] = 8
318
+ outcome_message = "The military campaign was successful!"
319
+ else:
320
+ effects["royal_trust"] = -10
321
+ effects["territory"] = 0 # Failed expansion
322
+ effects["satisfaction"] = -5
323
+ outcome_message = "The military campaign failed, damaging our reputation."
324
+ else:
325
+ outcome_message = None
326
+
327
+ # Apply changes to state
328
+ actual_changes = state.apply_changes(effects)
329
+
330
+ # Adjust advisor trust based on whether advice was followed
331
+ if adopted:
332
+ trust_change = random.randint(2, 5)
333
+ else:
334
+ trust_change = random.randint(-5, -1)
335
+ state.advisor_trust = state.clamp(state.advisor_trust + trust_change)
336
+ actual_changes["advisor_trust"] = trust_change
337
+
338
+ # Advance turn
339
+ state.turn += 1
340
+
341
+ # Check for game over
342
+ game_over, result = state.check_game_over()
343
+ if game_over:
344
+ state.game_over = True
345
+ state.result = result
346
+
347
+ # Build result message
348
+ changes_str = ", ".join(
349
+ f"{k}: {'+' if v > 0 else ''}{v}" for k, v in actual_changes.items() if v != 0
350
+ )
351
+
352
+ return {
353
+ "adopted": adopted,
354
+ "advice_given": advice,
355
+ "action_taken": action_taken,
356
+ "action_name": ADVICE_DESCRIPTIONS[action_taken]["name"],
357
+ "adoption_message": adoption_message,
358
+ "outcome_message": outcome_message,
359
+ "parameter_changes": actual_changes,
360
+ "changes_summary": changes_str or "No changes",
361
+ "game_over": state.game_over,
362
+ "game_result": state.result,
363
+ "new_state": state.to_dict(),
364
+ "status_summary": state.get_status_summary(),
365
+ }
366
+
367
 
368
  # Global session storage
369
  game_sessions: dict[str, GameState] = {}