File size: 15,766 Bytes
389bf98
 
 
 
 
 
 
ea5a5fd
389bf98
 
 
 
 
 
 
 
 
 
 
abe8a34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389bf98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea5a5fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353b2d2
 
 
 
 
ea5a5fd
 
b48bb47
ea5a5fd
b48bb47
ea5a5fd
 
 
b48bb47
ea5a5fd
 
b48bb47
ea5a5fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b48bb47
 
 
 
ea5a5fd
353b2d2
 
 
 
 
 
 
ea5a5fd
353b2d2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea5a5fd
 
 
 
 
b48bb47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea5a5fd
b48bb47
ea5a5fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b48bb47
 
 
 
 
 
 
 
 
 
 
 
 
 
ea5a5fd
 
 
 
b48bb47
ea5a5fd
b48bb47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea5a5fd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389bf98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
"""
Game state management for Unpredictable Lord.

This module defines the game state data structure and provides
functions for managing game sessions.
"""

import random
from dataclasses import asdict, dataclass, field
from typing import Literal

LordPersonality = Literal["cautious", "idealist", "populist"]

PERSONALITY_DESCRIPTIONS = {
    "cautious": "A risk-averse lord who prioritizes stability and financial security above all else.",
    "idealist": "An emotional idealist who pursues noble goals but reacts harshly to opposition.",
    "populist": "A popularity-focused lord who prioritizes public approval over long-term planning.",
}

# Available advice options that the Lord AI can choose from
AdviceType = Literal[
    "increase_tax",
    "decrease_tax",
    "expand_territory",
    "improve_diplomacy",
    "public_festival",
    "build_infrastructure",
    "do_nothing",
]

ADVICE_DESCRIPTIONS: dict[str, dict[str, str]] = {
    "increase_tax": {
        "name": "Increase Tax",
        "description": "Raise taxes to increase treasury, but decrease public satisfaction.",
        "effects": "Treasury ↑, Satisfaction ↓",
    },
    "decrease_tax": {
        "name": "Decrease Tax",
        "description": "Lower taxes to improve public satisfaction, but decrease treasury.",
        "effects": "Treasury ↓, Satisfaction ↑",
    },
    "expand_territory": {
        "name": "Expand Territory",
        "description": "Launch a military campaign to expand territory. Risky but rewarding.",
        "effects": "Territory ↑, Treasury ↓, Royal Trust Β±, Risk involved",
    },
    "improve_diplomacy": {
        "name": "Improve Diplomacy",
        "description": "Strengthen diplomatic relations with the kingdom.",
        "effects": "Royal Trust ↑, Treasury ↓",
    },
    "public_festival": {
        "name": "Hold Public Festival",
        "description": "Organize a festival to boost public morale.",
        "effects": "Satisfaction ↑, Treasury ↓",
    },
    "build_infrastructure": {
        "name": "Build Infrastructure",
        "description": "Invest in infrastructure to support population growth.",
        "effects": "Population ↑, Treasury ↓, Long-term Treasury ↑",
    },
    "do_nothing": {
        "name": "Do Nothing",
        "description": "Maintain the current state without any action.",
        "effects": "No change (turn passes)",
    },
}


def get_available_advice() -> dict[str, dict[str, str]]:
    """
    Get all available advice options with their descriptions.

    Returns:
        dict: Dictionary of advice types and their descriptions.
    """
    return ADVICE_DESCRIPTIONS


@dataclass
class GameState:
    """
    Represents the current state of a game session.

    All numeric parameters range from 0 to 100.
    Initial values are set to 50 (balanced state).
    """

    # Core parameters (0-100, higher is better)
    territory: int = 50  # Territory size - affects income and population cap
    population: int = 50  # Population - base for tax revenue
    treasury: int = 50  # Finances - required for policy execution
    satisfaction: int = 50  # Public satisfaction - low values risk rebellion
    royal_trust: int = 50  # Trust from the kingdom - affects diplomacy
    advisor_trust: int = 50  # Lord's trust in advisor - affects advice acceptance

    # Game progress
    turn: int = 1
    lord_personality: LordPersonality = "cautious"

    # Game status
    game_over: bool = False
    result: str | None = None  # "victory_territory", "victory_trust", etc.

    # Event tracking
    current_event: str | None = None
    event_history: list[str] = field(default_factory=list)

    def to_dict(self) -> dict:
        """Convert game state to dictionary for JSON serialization."""
        return asdict(self)

    def get_status_summary(self) -> str:
        """Generate a human-readable status summary."""
        status_lines = [
            f"Turn: {self.turn}",
            f"Lord Personality: {self.lord_personality}",
            "",
            "=== Parameters ===",
            f"Territory: {self.territory}/100",
            f"Population: {self.population}/100",
            f"Treasury: {self.treasury}/100",
            f"Public Satisfaction: {self.satisfaction}/100",
            f"Royal Trust: {self.royal_trust}/100",
            f"Advisor Trust: {self.advisor_trust}/100",
        ]

        if self.current_event:
            status_lines.extend(["", f"Current Event: {self.current_event}"])

        if self.game_over:
            status_lines.extend(["", f"GAME OVER: {self.result}"])

        return "\n".join(status_lines)

    def check_game_over(self) -> tuple[bool, str | None]:
        """
        Check if the game has ended.

        Returns:
            tuple: (is_game_over, result_type)
        """
        # Defeat conditions
        if self.satisfaction <= 0:
            return True, "defeat_rebellion"
        if self.royal_trust <= 0:
            return True, "defeat_exile"
        if self.treasury <= 0 and self.satisfaction <= 20:
            return True, "defeat_bankruptcy"

        # Victory conditions
        if self.territory >= 100:
            return True, "victory_territory"
        if self.advisor_trust >= 100 and self.turn >= 10:
            return True, "victory_trust"
        if self.treasury >= 100 and self.royal_trust >= 80:
            return True, "victory_wealth"

        return False, None

    def clamp(self, value: int, min_val: int = 0, max_val: int = 100) -> int:
        """Clamp a value to the valid range."""
        return max(min_val, min(max_val, value))

    def apply_changes(self, changes: dict[str, int]) -> dict[str, int]:
        """
        Apply parameter changes and return the actual changes made.

        Args:
            changes: Dictionary of parameter changes (can be positive or negative).

        Returns:
            Dictionary of actual changes applied (after clamping).
        """
        actual_changes = {}
        for param, delta in changes.items():
            if hasattr(self, param):
                old_value = getattr(self, param)
                new_value = self.clamp(old_value + delta)
                setattr(self, param, new_value)
                actual_changes[param] = new_value - old_value
        return actual_changes


# Personality-based acceptance rates and biases
PERSONALITY_TRAITS = {
    "cautious": {
        "base_acceptance": 0.4,  # Low base acceptance
        "preferred": ["do_nothing", "improve_diplomacy"],
        "disliked": ["expand_territory", "decrease_tax"],
        "trust_weight": 0.6,  # How much advisor_trust affects acceptance
    },
    "idealist": {
        "base_acceptance": 0.5,
        "preferred": ["build_infrastructure", "public_festival", "decrease_tax"],
        "disliked": ["increase_tax", "do_nothing"],
        "trust_weight": 0.4,  # Less influenced by trust, more by ideals
    },
    "populist": {
        "base_acceptance": 0.6,  # Higher base acceptance
        "preferred": ["decrease_tax", "public_festival"],
        "disliked": ["increase_tax", "expand_territory"],
        "trust_weight": 0.5,
    },
}

# Effects of each action when adopted
ACTION_EFFECTS: dict[str, dict[str, int]] = {
    "increase_tax": {
        "treasury": 15,
        "satisfaction": -10,
    },
    "decrease_tax": {
        "treasury": -10,
        "satisfaction": 15,
    },
    "expand_territory": {
        "territory": 10,
        "treasury": -15,
        "population": 5,
        # royal_trust is handled specially (can go up or down)
    },
    "improve_diplomacy": {
        "royal_trust": 12,
        "treasury": -8,
    },
    "public_festival": {
        "satisfaction": 15,
        "treasury": -12,
    },
    "build_infrastructure": {
        "population": 10,
        "treasury": -10,
        "satisfaction": 5,
    },
    "do_nothing": {},
}

# All possible actions (derived from ACTION_EFFECTS keys)
ALL_ACTIONS: list[str] = list(ACTION_EFFECTS.keys())

# Probability that lord chooses a preferred action when rejecting advice
PREFERRED_ACTION_PROBABILITY = 0.7


def _calculate_acceptance(state: "GameState", advice: str) -> tuple[bool, str, str]:
    """
    Determine whether the lord accepts the advice.

    Args:
        state: The current game state.
        advice: The advice type.

    Returns:
        tuple: (adopted, action_taken, adoption_message)
    """
    personality = state.lord_personality
    traits = PERSONALITY_TRAITS[personality]

    # Calculate acceptance probability
    base_prob = traits["base_acceptance"]
    trust_bonus = (state.advisor_trust / 100) * traits["trust_weight"]

    if advice in traits["preferred"]:
        preference_bonus = 0.2
    elif advice in traits["disliked"]:
        preference_bonus = -0.25
    else:
        preference_bonus = 0

    acceptance_prob = min(0.95, max(0.1, base_prob + trust_bonus + preference_bonus))

    # Determine if advice is adopted
    adopted = random.random() < acceptance_prob

    if adopted:
        action_taken = advice
        adoption_message = (
            f"The lord has decided to follow your advice: "
            f"{ADVICE_DESCRIPTIONS[advice]['name']}."
        )
    else:
        # Lord rejects and takes alternative action based on personality
        # Exclude: the original advice and disliked actions
        excluded_actions = {advice} | set(traits["disliked"])
        available_actions = [a for a in ALL_ACTIONS if a not in excluded_actions]

        # If no actions available (edge case), fall back to do_nothing
        if not available_actions:
            action_taken = "do_nothing"
        else:
            # High probability: choose from preferred actions
            # Low probability: choose from other available actions
            preferred_available = [
                a for a in traits["preferred"] if a not in excluded_actions
            ]

            if preferred_available and random.random() < PREFERRED_ACTION_PROBABILITY:
                action_taken = random.choice(preferred_available)
            else:
                # Choose from non-preferred available actions
                non_preferred = [
                    a for a in available_actions if a not in traits["preferred"]
                ]
                if non_preferred:
                    action_taken = random.choice(non_preferred)
                elif preferred_available:
                    # All available actions are preferred, pick from preferred
                    action_taken = random.choice(preferred_available)
                else:
                    action_taken = "do_nothing"

        adoption_message = (
            f"The lord has rejected your advice ({ADVICE_DESCRIPTIONS[advice]['name']}) "
            f"and instead chose: {ADVICE_DESCRIPTIONS[action_taken]['name']}."
        )

    return adopted, action_taken, adoption_message


def _apply_action_effects(
    state: "GameState", action_taken: str
) -> tuple[dict[str, int], str | None]:
    """
    Apply the effects of the action taken by the lord.

    Args:
        state: The current game state.
        action_taken: The action the lord decided to take.

    Returns:
        tuple: (actual_changes, outcome_message)
    """
    effects = ACTION_EFFECTS.get(action_taken, {}).copy()
    outcome_message = None

    # Special handling for expand_territory (risky)
    if action_taken == "expand_territory":
        if random.random() < 0.6:  # 60% success
            effects["royal_trust"] = 8
            outcome_message = "The military campaign was successful!"
        else:
            effects["royal_trust"] = -10
            effects["territory"] = 0  # Failed expansion
            effects["satisfaction"] = -5
            outcome_message = "The military campaign failed, damaging our reputation."

    # Apply changes to state
    actual_changes = state.apply_changes(effects)

    return actual_changes, outcome_message


def _update_advisor_trust(state: "GameState", adopted: bool) -> int:
    """
    Update the advisor trust based on whether advice was followed.

    Args:
        state: The current game state.
        adopted: Whether the advice was adopted.

    Returns:
        int: The change in advisor trust.
    """
    if adopted:
        trust_change = random.randint(2, 5)
    else:
        trust_change = random.randint(-5, -1)

    state.advisor_trust = state.clamp(state.advisor_trust + trust_change)
    return trust_change


def execute_turn_logic(state: "GameState", advice: str) -> dict:
    """
    Execute a turn based on the given advice.

    This function determines whether the lord accepts the advice based on
    personality and trust, applies the effects, and returns the result.

    Args:
        state: The current game state.
        advice: The advice type chosen by the Lord AI.

    Returns:
        dict: Result containing adoption status, action taken, effects, and messages.
    """
    # Validate input
    if advice not in ADVICE_DESCRIPTIONS:
        return {
            "error": "Invalid advice",
            "message": f"Unknown advice type: {advice}. Use list_available_advice to see options.",
        }

    if state.game_over:
        return {
            "error": "Game over",
            "message": f"This game has ended with result: {state.result}",
            "game_state": state.to_dict(),
        }

    # Step 1: Determine if advice is accepted
    adopted, action_taken, adoption_message = _calculate_acceptance(state, advice)

    # Step 2: Apply effects of the action
    actual_changes, outcome_message = _apply_action_effects(state, action_taken)

    # Step 3: Update advisor trust
    trust_change = _update_advisor_trust(state, adopted)
    actual_changes["advisor_trust"] = trust_change

    # Advance turn
    state.turn += 1

    # Check for game over
    game_over, result = state.check_game_over()
    if game_over:
        state.game_over = True
        state.result = result

    # Build result message
    changes_str = ", ".join(
        f"{k}: {'+' if v > 0 else ''}{v}" for k, v in actual_changes.items() if v != 0
    )

    return {
        "adopted": adopted,
        "advice_given": advice,
        "action_taken": action_taken,
        "action_name": ADVICE_DESCRIPTIONS[action_taken]["name"],
        "adoption_message": adoption_message,
        "outcome_message": outcome_message,
        "parameter_changes": actual_changes,
        "changes_summary": changes_str or "No changes",
        "game_over": state.game_over,
        "game_result": state.result,
        "new_state": state.to_dict(),
        "status_summary": state.get_status_summary(),
    }


# Global session storage
game_sessions: dict[str, GameState] = {}


def create_session(
    session_id: str, personality: LordPersonality = "cautious"
) -> GameState:
    """
    Create a new game session with the given ID.

    Args:
        session_id: Unique session identifier.
        personality: The lord's personality type.

    Returns:
        The newly created GameState.
    """
    state = GameState(lord_personality=personality)
    game_sessions[session_id] = state
    return state


def get_session(session_id: str) -> GameState | None:
    """
    Retrieve a game session by ID.

    Args:
        session_id: The session ID to look up.

    Returns:
        The GameState if found, None otherwise.
    """
    return game_sessions.get(session_id)


def delete_session(session_id: str) -> bool:
    """
    Delete a game session.

    Args:
        session_id: The session ID to delete.

    Returns:
        True if deleted, False if not found.
    """
    if session_id in game_sessions:
        del game_sessions[session_id]
        return True
    return False