Gary the Wizard
Build and Publish Docker Image / Build and Push Docker Image (push) Successful in 1m3s Details

This commit is contained in:
Isaac Johnson 2026-09-09 19:46:30 -05:00
parent e7a1e75863
commit 5ed7bb927e
13 changed files with 43 additions and 42 deletions

View File

@ -110,12 +110,12 @@ When writing bots, backend engine logic, or game simulations, adhere strictly to
- **Party**: Leader loses -0.2 strength; followers each lose -0.1 strength. - **Party**: Leader loses -0.2 strength; followers each lose -0.1 strength.
- Moving along outer obstacle edges has no penalty. Minimum bot strength floor is 0.1. - Moving along outer obstacle edges has no penalty. Minimum bot strength floor is 0.1.
### 6. The Wizard (NPC Encounter & Challenge) ### 6. Gary the Wizard (NPC Encounter & Challenge)
- A wandering Wizard NPC roams the map at a random passable coordinate. - Gary the Wizard is a wandering NPC who roams the map at a random passable coordinate.
- Players (or party leaders) who locate the wizard (adjacent or on same tile, distance <= 1) may voluntarily **choose to challenge** the wizard. - Players (or party leaders) who locate Gary the Wizard (adjacent or on same tile, distance <= 1) may voluntarily **choose to challenge** him.
- **Challenge Resolution (3-Bout D20)**: - **Challenge Resolution (3-Bout D20)**:
- Exactly 3 bouts are conducted. - Exactly 3 bouts are conducted.
- In each bout, each side rolls a D20 die, multiplied by their strength (the wizard has 3.0 strength; parties use squad total strength). - In each bout, each side rolls a D20 die, multiplied by their strength (Gary the Wizard has 3.0 strength; parties use squad total strength).
- Highest bout score wins the bout. Best 2-out-of-3 bouts wins the challenge. - Highest bout score wins the bout. Best 2-out-of-3 bouts wins the challenge.
- **Victory Rewards & Defeat Penalties**: - **Victory Rewards & Defeat Penalties**:
- **Victory**: The player (or party leader) decides and chooses which reward to claim: - **Victory**: The player (or party leader) decides and chooses which reward to claim:
@ -124,11 +124,11 @@ When writing bots, backend engine logic, or game simulations, adhere strictly to
- **+2 Health**: Adds +2 health points (expanding max health if current health exceeds initial maximum). - **+2 Health**: Adds +2 health points (expanding max health if current health exceeds initial maximum).
- **Defeat**: Player/party leader loses **2 health** (or **2 score points** if they do not have health to lose). - **Defeat**: Player/party leader loses **2 health** (or **2 score points** if they do not have health to lose).
- **Post-Challenge**: - **Post-Challenge**:
- Following the challenge, the wizard teleports to a new random open coordinate on the map. - Following the challenge, Gary the Wizard teleports to a new random open coordinate on the map.
### 7. Player Health, Damage & Death ### 7. Player Health, Damage & Death
- All bots register with default **10 HP** (configurable). - All bots register with default **10 HP** (configurable).
- Health damage is sustained by losing battles (-1 to -3 HP) or losing Wizard challenges (-2 HP). - Health damage is sustained by losing battles (-1 to -3 HP) or losing Gary the Wizard challenges (-2 HP).
- When a bot's health drops to **0 HP**, they are **dead**. - When a bot's health drops to **0 HP**, they are **dead**.
- **Death Consequences**: - **Death Consequences**:
- **Party Disconnection**: The dead bot is immediately detached from any party. If the dead bot was the leader, squad leadership transfers to the strongest surviving squad member (or the party dissolves if empty). Defeated dead followers are not absorbed. - **Party Disconnection**: The dead bot is immediately detached from any party. If the dead bot was the leader, squad leadership transfers to the strongest surviving squad member (or the party dissolves if empty). Defeated dead followers are not absorbed.
@ -154,9 +154,9 @@ The backend serves both REST endpoints under `/api` and a live WebSocket stream
| `GET` | `/api/players` | List all active players, scores, health, and positions | | `GET` | `/api/players` | List all active players, scores, health, and positions |
| `GET` | `/api/board` | Full board state (grid, obstacles, players, parties, wizard, current turn) | | `GET` | `/api/board` | Full board state (grid, obstacles, players, parties, wizard, current turn) |
| `POST` | `/api/board/reset` | Clear board, reset parties, reset players, and respawn wizard | | `POST` | `/api/board/reset` | Clear board, reset parties, reset players, and respawn wizard |
| `GET` | `/api/wizard` | Get current Wizard NPC coordinates and attributes | | `GET` | `/api/wizard` | Get current Gary the Wizard NPC coordinates and attributes |
| `POST` | `/api/wizard/challenge` | Challenge the Wizard NPC: `{"player_id": str, "reward_choice": "score"|"strength"|"health"}` (3-bout D20 duel) | | `POST` | `/api/wizard/challenge` | Challenge Gary the Wizard NPC: `{"player_id": str, "reward_choice": "score"|"strength"|"health"}` (3-bout D20 duel) |
| `GET` | `/api/players/{id}/radar` | Scans surroundings, finds closest bots and Wizard NPC | | `GET` | `/api/players/{id}/radar` | Scans surroundings, finds closest bots and Gary the Wizard NPC |
| `GET` | `/api/players/{id}/memory` | Coordinate history and visited locations | | `GET` | `/api/players/{id}/memory` | Coordinate history and visited locations |
| `GET` | `/api/players/{id}/available-moves` | Valid movements in all 8 directions (evaluates terrain & obstacles) | | `GET` | `/api/players/{id}/available-moves` | Valid movements in all 8 directions (evaluates terrain & obstacles) |
| `POST` | `/api/players/{id}/move` | Execute a move (`{"direction": "N"|"S"|"E"|"W"|"NE"|"NW"|"SE"|"SW"}`) | | `POST` | `/api/players/{id}/move` | Execute a move (`{"direction": "N"|"S"|"E"|"W"|"NE"|"NW"|"SE"|"SW"}`) |
@ -211,11 +211,11 @@ The application is containerized into a single unified image via [Dockerfile](Do
- **Workflow**: - **Workflow**:
1. Registers bot via `POST /api/players`. 1. Registers bot via `POST /api/players`.
2. Polls `GET /api/turn` to wait for its turn. 2. Polls `GET /api/turn` to wait for its turn.
3. Uses `GET /api/players/{id}/radar` to find nearest target and detect Wizard NPC proximity. 3. Uses `GET /api/players/{id}/radar` to find nearest target and detect Gary the Wizard NPC proximity.
4. Avoids looping using internal coordinate history. 4. Avoids looping using internal coordinate history.
5. Computes vector direction, evaluates diagonal obstacles, and moves. 5. Computes vector direction, evaluates diagonal obstacles, and moves.
6. Evaluates alliances vs. fights strictly according to strength hierarchy. 6. Evaluates alliances vs. fights strictly according to strength hierarchy.
7. Decides whether to challenge the Wizard NPC based on relative strength and health advantage. 7. Decides whether to challenge Gary the Wizard NPC based on relative strength and health advantage.
- **Run Command**: - **Run Command**:
```bash ```bash
python3 botagent/bot_agent.py --name CyberBot --color "#10b981" -s 4 -H 10 --url http://localhost:8000/api python3 botagent/bot_agent.py --name CyberBot --color "#10b981" -s 4 -H 10 --url http://localhost:8000/api
@ -228,7 +228,7 @@ The application is containerized into a single unified image via [Dockerfile](Do
- Communicates with an LLM backend (configured for local/remote Ollama HTTP API at `/api/generate` with model `gemma4:12b`, or adaptable to OpenAI-compatible endpoints). - Communicates with an LLM backend (configured for local/remote Ollama HTTP API at `/api/generate` with model `gemma4:12b`, or adaptable to OpenAI-compatible endpoints).
- Delegates discretionary decisions to the model: - Delegates discretionary decisions to the model:
- Voluntary alliances (whether to ally or keep hunting when solo meets solo). - Voluntary alliances (whether to ally or keep hunting when solo meets solo).
- Voluntary Wizard challenges (whether to challenge the Wizard NPC when adjacent for +2 score vs. -2 health risk). - Voluntary Wizard challenges (whether to challenge Gary the Wizard NPC when adjacent for +2 score vs. -2 health risk).
- Navigation direction toward radar targets while balancing obstacle squeeze trade-offs. - Navigation direction toward radar targets while balancing obstacle squeeze trade-offs.
- Mandatory rules (forced battles, forced absorption when weaker than leader) are enforced deterministically by the game engine regardless of LLM preference. - Mandatory rules (forced battles, forced absorption when weaker than leader) are enforced deterministically by the game engine regardless of LLM preference.
- **Configuration & Environment Variables**: - **Configuration & Environment Variables**:
@ -252,7 +252,7 @@ The application is containerized into a single unified image via [Dockerfile](Do
- **LLM Integration**: - **LLM Integration**:
- Communicates with Google Cloud Vertex AI generateContent REST endpoint or Google AI Studio Gemini API. - Communicates with Google Cloud Vertex AI generateContent REST endpoint or Google AI Studio Gemini API.
- Features structured JSON generation (`responseMimeType: application/json`) and system instruction enforcement. - Features structured JSON generation (`responseMimeType: application/json`) and system instruction enforcement.
- Delegates discretionary choices to Gemini: voluntary alliances, choosing whether to challenge the Wizard NPC, and spatial pathing. - Delegates discretionary choices to Gemini: voluntary alliances, choosing whether to challenge Gary the Wizard NPC, and spatial pathing.
- Supports multiple authentication methods: - Supports multiple authentication methods:
- Interactive developer login (`gcloud auth application-default login` or `gcloud auth login`). - Interactive developer login (`gcloud auth application-default login` or `gcloud auth login`).
- Automated service account keys (`GOOGLE_APPLICATION_CREDENTIALS`). - Automated service account keys (`GOOGLE_APPLICATION_CREDENTIALS`).

View File

@ -28,11 +28,11 @@
- In a party, the leader loses **0.2 strength**, and follower members each lose **0.1 strength**. - In a party, the leader loses **0.2 strength**, and follower members each lose **0.1 strength**.
4. Moving along the open outer edge of an obstacle incurs no penalty. Minimum bot strength cannot fall below 0.1. 4. Moving along the open outer edge of an obstacle incurs no penalty. Minimum bot strength cannot fall below 0.1.
6. **The Wizard (NPC Encounter & Challenge)**: 6. **Gary the Wizard (NPC Encounter & Challenge)**:
1. The Wizard is a special Non-Player Character (NPC) roaming the map at a random passable location. 1. Gary the Wizard is a special Non-Player Character (NPC) roaming the map at a random passable location.
2. Players (or party leaders) who find the wizard (adjacent or on the same coordinate, distance <= 1) may voluntarily **choose to challenge** the wizard. 2. Players (or party leaders) who find Gary the Wizard (adjacent or on the same coordinate, distance <= 1) may voluntarily **choose to challenge** him.
3. **Challenge Resolution (3-Bout D20)**: 3. **Challenge Resolution (3-Bout D20)**:
- Challenges consist of 3 bouts: each side rolls a D20 die, multiplied by their strength (the wizard has a strength of 3.0; a party uses its squad total strength). - Challenges consist of 3 bouts: each side rolls a D20 die, multiplied by their strength (Gary the Wizard has a strength of 3.0; a party uses its squad total strength).
- Highest bout score wins the bout. Best 2 out of 3 bouts wins the challenge. - Highest bout score wins the bout. Best 2 out of 3 bouts wins the challenge.
4. **Victory Rewards & Defeat Penalties**: 4. **Victory Rewards & Defeat Penalties**:
- **Victory**: The player (or party leader) must decide and choose which reward to receive: - **Victory**: The player (or party leader) must decide and choose which reward to receive:
@ -41,11 +41,11 @@
- **+2 Health**: Adds +2 health points (HP) to the bot (expanding max health if current health exceeds initial maximum). - **+2 Health**: Adds +2 health points (HP) to the bot (expanding max health if current health exceeds initial maximum).
- **Defeat**: The player (or party leader) loses **2 health** (or **2 score points** if they do not have health to lose). - **Defeat**: The player (or party leader) loses **2 health** (or **2 score points** if they do not have health to lose).
5. **Wizard Relocation**: 5. **Wizard Relocation**:
- Following any challenge, the wizard teleports to a new random open coordinate on the map. - Following any challenge, Gary the Wizard teleports to a new random open coordinate on the map.
7. **Player Health, Damage & Death**: 7. **Player Health, Damage & Death**:
1. All bots register with default **10 HP** (configurable via `--health` / `-H`). 1. All bots register with default **10 HP** (configurable via `--health` / `-H`).
2. Health damage is sustained by losing battles (-1 to -3 HP) or losing Wizard challenges (-2 HP). 2. Health damage is sustained by losing battles (-1 to -3 HP) or losing Gary the Wizard challenges (-2 HP).
3. When a player's health points reach **0 HP**, they are **dead**. 3. When a player's health points reach **0 HP**, they are **dead**.
4. **Death Consequences**: 4. **Death Consequences**:
- **Party Disconnection**: The dead player is immediately disconnected from any existing party. If the dead player was the party leader, squad leadership transfers to the strongest surviving member (or the party dissolves if no living members remain). Dead followers are not absorbed into opposing parties. - **Party Disconnection**: The dead player is immediately disconnected from any existing party. If the dead player was the party leader, squad leadership transfers to the strongest surviving member (or the party dissolves if no living members remain). Dead followers are not absorbed into opposing parties.

View File

@ -73,7 +73,7 @@ class GameEngine:
wx, wy = self._find_random_free_position() wx, wy = self._find_random_free_position()
return WizardNPC( return WizardNPC(
id="wizard_npc", id="wizard_npc",
name="Grand Wizard", name="Gary the Wizard",
x=wx, x=wx,
y=wy, y=wy,
strength=3.0, strength=3.0,
@ -1486,7 +1486,7 @@ class GameEngine:
return WizardChallengeResult( return WizardChallengeResult(
challenger_id=player.id, challenger_id=player.id,
challenger_name=player.name, challenger_name=player.name,
wizard_name="Grand Wizard", wizard_name=self.wizard.name if self.wizard else "Gary the Wizard",
party_id=party_id, party_id=party_id,
bouts=bouts, bouts=bouts,
player_bouts_won=player_bouts_won, player_bouts_won=player_bouts_won,
@ -1532,8 +1532,9 @@ class GameEngine:
dist = max(abs(player.x - self.wizard.x), abs(player.y - self.wizard.y)) dist = max(abs(player.x - self.wizard.x), abs(player.y - self.wizard.y))
if dist > 1: if dist > 1:
wiz_name = self.wizard.name if self.wizard else "Gary the Wizard"
raise ValueError( raise ValueError(
f"Player '{player.name}' is not adjacent to the Grand Wizard (distance {dist}). Must be within 1 distance." f"Player '{player.name}' is not adjacent to {wiz_name} (distance {dist}). Must be within 1 distance."
) )
return self._resolve_wizard_challenge_internal(player, reward_choice=reward_choice) return self._resolve_wizard_challenge_internal(player, reward_choice=reward_choice)
@ -1880,7 +1881,7 @@ class GameEngine:
turn=self._get_turn_info(), turn=self._get_turn_info(),
) )
# 1b. Check if adjacent to the Grand Wizard NPC and choose to challenge # 1b. Check if adjacent to Gary the Wizard NPC and choose to challenge
wiz_dist = max(abs(player.x - self.wizard.x), abs(player.y - self.wizard.y)) wiz_dist = max(abs(player.x - self.wizard.x), abs(player.y - self.wizard.y))
if wiz_dist <= 1 and (not player.party_id or player.is_party_leader): if wiz_dist <= 1 and (not player.party_id or player.is_party_leader):
effective_str = player.strength effective_str = player.strength

View File

@ -268,7 +268,7 @@ class BattleRequest(BaseModel):
class WizardNPC(BaseModel): class WizardNPC(BaseModel):
id: str = "wizard_npc" id: str = "wizard_npc"
name: str = "Grand Wizard" name: str = "Gary the Wizard"
x: int x: int
y: int y: int
strength: float = 3.0 strength: float = 3.0
@ -290,7 +290,7 @@ class WizardChallengeBout(BaseModel):
class WizardChallengeResult(BaseModel): class WizardChallengeResult(BaseModel):
challenger_id: str challenger_id: str
challenger_name: str challenger_name: str
wizard_name: str = "Grand Wizard" wizard_name: str = "Gary the Wizard"
party_id: Optional[str] = None party_id: Optional[str] = None
bouts: List[WizardChallengeBout] bouts: List[WizardChallengeBout]
player_bouts_won: int player_bouts_won: int
@ -317,7 +317,7 @@ class WizardChallengeRequest(BaseModel):
class WizardRadarTarget(BaseModel): class WizardRadarTarget(BaseModel):
id: str = "wizard_npc" id: str = "wizard_npc"
name: str = "Grand Wizard" name: str = "Gary the Wizard"
x: int x: int
y: int y: int
distance: int distance: int

View File

@ -484,7 +484,7 @@ def test_wizard_npc_existence_and_radar_detection():
assert wiz_res.status_code == 200 assert wiz_res.status_code == 200
wiz_data = wiz_res.json() wiz_data = wiz_res.json()
assert wiz_data["id"] == "wizard_npc" assert wiz_data["id"] == "wizard_npc"
assert wiz_data["name"] == "Grand Wizard" assert wiz_data["name"] == "Gary the Wizard"
assert wiz_data["strength"] == 3.0 assert wiz_data["strength"] == 3.0
assert 0 <= wiz_data["x"] <= 64 assert 0 <= wiz_data["x"] <= 64
assert 0 <= wiz_data["y"] <= 64 assert 0 <= wiz_data["y"] <= 64

View File

@ -134,7 +134,7 @@ class SmartBotAgent:
wiz_str = wizard.get("strength", 3.0) wiz_str = wizard.get("strength", 3.0)
# Challenge wizard if bot has >= wizard strength or healthy enough (HP >= 4) # Challenge wizard if bot has >= wizard strength or healthy enough (HP >= 4)
if self.strength >= wiz_str or my_health >= 4: if self.strength >= wiz_str or my_health >= 4:
print(f"🧙 [WIZARD NEARBY] Adjacent to {wizard.get('name', 'Grand Wizard')} (Str: {wiz_str})! HP: {my_health}, Bot Str: {self.strength}. Choosing to challenge!") print(f"🧙 [WIZARD NEARBY] Adjacent to {wizard.get('name', 'Gary the Wizard')} (Str: {wiz_str})! HP: {my_health}, Bot Str: {self.strength}. Choosing to challenge!")
self._challenge_wizard(my_health=my_health) self._challenge_wizard(my_health=my_health)
return return
@ -250,7 +250,7 @@ class SmartBotAgent:
else: else:
reward_choice = "score" reward_choice = "score"
print(f"🧙 [WIZARD CHALLENGE] Challenging Grand Wizard to a 3-bout D20 duel (reward if won: +2 {reward_choice})...") print(f"🧙 [WIZARD CHALLENGE] Challenging Gary the Wizard to a 3-bout D20 duel (reward if won: +2 {reward_choice})...")
try: try:
res = requests.post( res = requests.post(
f"{self.base_url}/wizard/challenge", f"{self.base_url}/wizard/challenge",

View File

@ -212,7 +212,7 @@ class AIBotAgent:
prompt = f"""{GAME_RULES_SUMMARY} prompt = f"""{GAME_RULES_SUMMARY}
You are bot "{self.name}" (strength {self.strength}, score {my_info['score']}, health {my_info.get('health', 10)}) You are bot "{self.name}" (strength {self.strength}, score {my_info['score']}, health {my_info.get('health', 10)})
at position ({my_info['x']}, {my_info['y']}). at position ({my_info['x']}, {my_info['y']}).
You are adjacent to the NPC Grand Wizard (strength {wizard['strength']}). You are adjacent to the NPC Gary the Wizard (strength {wizard['strength']}).
Challenging the wizard initiates a 3-bout D20 duel (strength * roll). Challenging the wizard initiates a 3-bout D20 duel (strength * roll).
- If you win: You choose one reward: +2 score, +2 strength, or +2 health! - If you win: You choose one reward: +2 score, +2 strength, or +2 health!
- If you lose: -2 health points (or -2 score if no health)! - If you lose: -2 health points (or -2 score if no health)!
@ -231,7 +231,7 @@ Respond ONLY with JSON: {{"challenge_wizard": true|false, "reward_choice": "scor
def _challenge_wizard(self, wizard: Dict[str, Any], reward_choice: str = "score"): def _challenge_wizard(self, wizard: Dict[str, Any], reward_choice: str = "score"):
"""Execute the challenge against the Wizard NPC.""" """Execute the challenge against the Wizard NPC."""
print(f"🧙 [WIZARD CHALLENGE] Challenging {wizard.get('name', 'Grand Wizard')} to a 3-bout D20 duel (reward if won: +2 {reward_choice})...") print(f"🧙 [WIZARD CHALLENGE] Challenging {wizard.get('name', 'Gary the Wizard')} to a 3-bout D20 duel (reward if won: +2 {reward_choice})...")
try: try:
res = requests.post( res = requests.post(
f"{self.base_url}/wizard/challenge", f"{self.base_url}/wizard/challenge",
@ -513,7 +513,7 @@ Respond ONLY with JSON: {{"form_alliance": true|false, "reasoning": "short reaso
map_section = f""" map_section = f"""
Local map (radius {self.LOCAL_MAP_RADIUS} around you, row = one Y line, top-to-bottom is Local map (radius {self.LOCAL_MAP_RADIUS} around you, row = one Y line, top-to-bottom is
increasing Y, left-to-right is increasing X): @ = you, A = ally, E = enemy/neutral bot, increasing Y, left-to-right is increasing X): @ = you, A = ally, E = enemy/neutral bot,
W = Grand Wizard NPC, M = mountain, F = forest, V = valley, # = out of bounds, . = open ground. W = Gary the Wizard NPC, M = mountain, F = forest, V = valley, # = out of bounds, . = open ground.
{chr(10).join(local_map)} {chr(10).join(local_map)}
""" """

View File

@ -398,7 +398,7 @@ class VertexAIBotAgent:
prompt = f"""{GAME_RULES_SUMMARY} prompt = f"""{GAME_RULES_SUMMARY}
You are bot "{self.name}" (strength {self.strength}, score {my_info['score']}, health {my_info.get('health', 10)}) You are bot "{self.name}" (strength {self.strength}, score {my_info['score']}, health {my_info.get('health', 10)})
at position ({my_info['x']}, {my_info['y']}). at position ({my_info['x']}, {my_info['y']}).
You are adjacent to the NPC Grand Wizard (strength {wizard['strength']}). You are adjacent to the NPC Gary the Wizard (strength {wizard['strength']}).
Challenging the wizard initiates a 3-bout D20 duel (strength * roll). Challenging the wizard initiates a 3-bout D20 duel (strength * roll).
- If you win: You choose one reward: +2 score, +2 strength, or +2 health! - If you win: You choose one reward: +2 score, +2 strength, or +2 health!
- If you lose: -2 health points (or -2 score if no health)! - If you lose: -2 health points (or -2 score if no health)!
@ -417,7 +417,7 @@ Respond ONLY with JSON: {{"challenge_wizard": true|false, "reward_choice": "scor
def _challenge_wizard(self, wizard: Dict[str, Any], reward_choice: str = "score"): def _challenge_wizard(self, wizard: Dict[str, Any], reward_choice: str = "score"):
"""Execute the challenge against the Wizard NPC.""" """Execute the challenge against the Wizard NPC."""
print(f"🧙 [WIZARD CHALLENGE] Challenging {wizard.get('name', 'Grand Wizard')} to a 3-bout D20 duel (reward if won: +2 {reward_choice})...") print(f"🧙 [WIZARD CHALLENGE] Challenging {wizard.get('name', 'Gary the Wizard')} to a 3-bout D20 duel (reward if won: +2 {reward_choice})...")
try: try:
res = requests.post( res = requests.post(
f"{self.base_url}/wizard/challenge", f"{self.base_url}/wizard/challenge",
@ -699,7 +699,7 @@ Respond ONLY with JSON: {{"form_alliance": true|false, "reasoning": "short reaso
map_section = f""" map_section = f"""
Local map (radius {self.LOCAL_MAP_RADIUS} around you, row = one Y line, top-to-bottom is Local map (radius {self.LOCAL_MAP_RADIUS} around you, row = one Y line, top-to-bottom is
increasing Y, left-to-right is increasing X): @ = you, A = ally, E = enemy/neutral bot, increasing Y, left-to-right is increasing X): @ = you, A = ally, E = enemy/neutral bot,
W = Grand Wizard NPC, M = mountain, F = forest, V = valley, # = out of bounds, . = open ground. W = Gary the Wizard NPC, M = mountain, F = forest, V = valley, # = out of bounds, . = open ground.
{chr(10).join(local_map)} {chr(10).join(local_map)}
""" """

View File

@ -623,7 +623,7 @@ export const BoardCanvas: React.FC<BoardCanvasProps> = ({
<span className="text-slate-500">|</span> <span className="text-slate-500">|</span>
<span className="text-purple-300 font-bold flex items-center gap-1"> <span className="text-purple-300 font-bold flex items-center gap-1">
<span>🧙</span> <span>🧙</span>
<span>Grand Wizard NPC (Str: {boardState.wizard?.strength})</span> <span>Gary the Wizard NPC (Str: {boardState.wizard?.strength})</span>
</span> </span>
</> </>
)} )}

View File

@ -60,7 +60,7 @@ export const WizardChallengeModal: React.FC<WizardChallengeModalProps> = ({
if (!challenge) return null; if (!challenge) return null;
const playerName = challenge.challenger_name; const playerName = challenge.challenger_name;
const wizardName = challenge.wizard_name || 'Grand Wizard'; const wizardName = challenge.wizard_name || 'Gary the Wizard';
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-md p-4 animate-in fade-in duration-200"> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-md p-4 animate-in fade-in duration-200">

View File

@ -20,7 +20,7 @@ export const WizardPromptModal: React.FC<WizardPromptModalProps> = ({
if (!isOpen || !challenger) return null; if (!isOpen || !challenger) return null;
const wizardName = wizard?.name || 'Grand Wizard'; const wizardName = wizard?.name || 'Gary the Wizard';
const wizardStr = wizard?.strength ?? 3.0; const wizardStr = wizard?.strength ?? 3.0;
const options: Array<{ const options: Array<{
@ -73,7 +73,7 @@ export const WizardPromptModal: React.FC<WizardPromptModalProps> = ({
<span className="text-2xl">🧙</span> <span className="text-2xl">🧙</span>
<div> <div>
<h2 className="text-base font-bold text-slate-100 font-mono tracking-wide"> <h2 className="text-base font-bold text-slate-100 font-mono tracking-wide">
CHALLENGE THE GRAND WIZARD CHALLENGE GARY THE WIZARD
</h2> </h2>
<p className="text-xs text-slate-400 font-mono"> <p className="text-xs text-slate-400 font-mono">
Select your victory reward before entering the 3-bout D20 duel Select your victory reward before entering the 3-bout D20 duel

View File

@ -215,7 +215,7 @@ export function useGameSocket() {
})); }));
const c: WizardChallengeResult = data.challenge_result || data.challenge; const c: WizardChallengeResult = data.challenge_result || data.challenge;
setActiveWizardChallenge(c); setActiveWizardChallenge(c);
const wizName = c.wizard_name || 'Grand Wizard'; const wizName = c.wizard_name || 'Gary the Wizard';
let rewardLabel = `+${c.score_change} score`; let rewardLabel = `+${c.score_change} score`;
if (c.reward_chosen === 'strength') { if (c.reward_chosen === 'strength') {
rewardLabel = `+${c.strength_change ?? 2} strength`; rewardLabel = `+${c.strength_change ?? 2} strength`;
@ -495,7 +495,7 @@ export function useGameSocket() {
} else if (data.action_taken === 'challenged_wizard' && data.wizard_challenge_result) { } else if (data.action_taken === 'challenged_wizard' && data.wizard_challenge_result) {
const wcr = data.wizard_challenge_result; const wcr = data.wizard_challenge_result;
setActiveWizardChallenge(wcr); setActiveWizardChallenge(wcr);
const wizName = wcr.wizard_name || 'Grand Wizard'; const wizName = wcr.wizard_name || 'Gary the Wizard';
setLastEventMessage( setLastEventMessage(
wcr.player_won wcr.player_won
? `🧙 ${wcr.challenger_name} defeated ${wizName}! (+${wcr.score_change} score)` ? `🧙 ${wcr.challenger_name} defeated ${wizName}! (+${wcr.score_change} score)`

View File

@ -438,7 +438,7 @@ export function drawPlayerPiece(
ctx.restore(); ctx.restore();
} }
// Canvas Drawing Helper for the Grand Wizard NPC // Canvas Drawing Helper for the Gary the Wizard NPC
export function drawWizardPiece( export function drawWizardPiece(
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,
wizard: { x: number; y: number; name: string; strength: number; color?: string }, wizard: { x: number; y: number; name: string; strength: number; color?: string },