Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import random | |
| import json | |
| import math | |
| # Set page config | |
| st.set_page_config(page_title="HexCitySaga: Dungeon Crawl", layout="wide") | |
| # Initialize game state | |
| if 'game_state' not in st.session_state: | |
| st.session_state.game_state = { | |
| 'hex_grid': [[{'type': 'empty', 'emoji': '', 'alive': False} for _ in range(12)] for _ in range(16)], | |
| 'heroes': {}, # {player_id: {x, y, hp, atk, def, xp, gear}} | |
| 'monsters': [], # [{x, y, type, hp, atk}] | |
| 'loot': [], # [{x, y, type}] | |
| 'exit': {'x': 15, 'y': 11}, # Goal to escape | |
| 'overlord': {'x': 14, 'y': 10, 'hp': 50, 'atk': 5, 'alive': True}, | |
| 'players': {}, | |
| 'story': [], | |
| 'drawn_cards': 0, | |
| 'turn': 0 | |
| } | |
| # Constants | |
| HEX_SIZE = 40 | |
| PLAYER_NAMES = ["SkyWalker", "ForestRanger", "CityBuilder", "MonsterTamer", "RailMaster"] | |
| PLANTS = ["π±", "π²", "π³", "π΄", "π΅"] | |
| LOOT = ["βοΈ", "π‘οΈ", "π°", "π"] | |
| TRAPS = ["β‘", "π³οΈ"] | |
| SUIT_PROPERTIES = {"Hearts": "heroic", "Diamonds": "treacherous", "Clubs": "fierce", "Spades": "enigmatic"} | |
| SENTENCE_TEMPLATES = { | |
| "Hero": "A {property} hero {word} ventured forth.", | |
| "Monster": "A {property} {word} ambushed from the shadows!", | |
| "Loot": "The party found a {property} {word} in the depths.", | |
| "Trap": "A {property} {word} sprung upon them!", | |
| "Puzzle": "An {property} puzzle blocked with {word}." | |
| } | |
| # Game of Life for dungeon decay | |
| def apply_game_of_life(grid): | |
| new_grid = [[cell.copy() for cell in row] for row in grid] | |
| for i in range(len(grid)): | |
| for j in range(len(grid[0])): | |
| neighbors = count_neighbors(grid, i, j) | |
| if grid[i][j]['type'] in ['plant', 'trap'] and grid[i][j]['alive']: | |
| if neighbors < 2 or neighbors > 3: | |
| new_grid[i][j]['alive'] = False | |
| new_grid[i][j]['type'] = 'empty' | |
| new_grid[i][j]['emoji'] = '' | |
| elif grid[i][j]['type'] == 'empty' and neighbors == 3: | |
| new_grid[i][j]['type'] = 'plant' | |
| new_grid[i][j]['emoji'] = random.choice(PLANTS) | |
| new_grid[i][j]['alive'] = True | |
| return new_grid | |
| def count_neighbors(grid, x, y): | |
| count = 0 | |
| for di in [-1, 0, 1]: | |
| for dj in [-1, 0, 1]: | |
| if di == 0 and dj == 0: | |
| continue | |
| ni, nj = x + di, y + dj | |
| if 0 <= ni < len(grid) and 0 <= nj < len(grid[0]) and grid[ni][nj]['alive']: | |
| count += 1 | |
| return count | |
| # Card deck | |
| def create_deck(): | |
| suits = ["Hearts", "Diamonds", "Clubs", "Spades"] | |
| ranks = list(range(1, 14)) | |
| deck = [(suit, rank) for suit in suits for rank in ranks] | |
| random.shuffle(deck) | |
| return deck | |
| if 'deck' not in st.session_state: | |
| st.session_state.deck = create_deck() | |
| # p5.js rendering | |
| p5js_code = """ | |
| const HEX_SIZE = 40; | |
| const SQRT_3 = Math.sqrt(3); | |
| let hexGrid = []; | |
| let heroes = {}; | |
| let monsters = []; | |
| let loot = []; | |
| let exit = {}; | |
| let overlord = {}; | |
| let playerId; | |
| let story = []; | |
| function setup() { | |
| createCanvas(640, 480); | |
| updateFromState(); | |
| } | |
| function updateFromState() { | |
| hexGrid = JSON.parse(document.getElementById('hex_grid').innerHTML); | |
| heroes = JSON.parse(document.getElementById('heroes').innerHTML); | |
| monsters = JSON.parse(document.getElementById('monsters').innerHTML); | |
| loot = JSON.parse(document.getElementById('loot').innerHTML); | |
| exit = JSON.parse(document.getElementById('exit').innerHTML); | |
| overlord = JSON.parse(document.getElementById('overlord').innerHTML); | |
| playerId = document.getElementById('player_id').innerHTML; | |
| story = JSON.parse(document.getElementById('story').innerHTML); | |
| } | |
| function draw() { | |
| background(169, 169, 169); // Dungeon gray | |
| drawHexGrid(); | |
| drawLoot(); | |
| drawHeroes(); | |
| drawMonsters(); | |
| drawExit(); | |
| drawOverlord(); | |
| drawStory(); | |
| } | |
| function drawHexGrid() { | |
| for (let i = 0; i < hexGrid.length; i++) { | |
| for (let j = 0; j < hexGrid[i].length; j++) { | |
| let x = i * HEX_SIZE * 1.5; | |
| let y = j * HEX_SIZE * SQRT_3 + (i % 2 ? HEX_SIZE * SQRT_3 / 2 : 0); | |
| fill(hexGrid[i][j].type === 'wall' ? '#555' : hexGrid[i][j].alive ? '#90EE90' : '#A9A9A9'); | |
| stroke(0); | |
| drawHex(x, y); | |
| if (hexGrid[i][j].emoji) { | |
| textSize(20); | |
| text(hexGrid[i][j].emoji, x - 10, y + 5); | |
| } | |
| } | |
| } | |
| } | |
| function drawHex(x, y) { | |
| beginShape(); | |
| for (let a = 0; a < 6; a++) { | |
| let angle = TWO_PI / 6 * a; | |
| vertex(x + HEX_SIZE * cos(angle), y + HEX_SIZE * sin(angle)); | |
| } | |
| endShape(CLOSE); | |
| } | |
| function drawHeroes() { | |
| Object.keys(heroes).forEach(h => { | |
| let hero = heroes[h]; | |
| let x = hero.x * HEX_SIZE * 1.5; | |
| let y = hero.y * HEX_SIZE * SQRT_3 + (hero.x % 2 ? HEX_SIZE * SQRT_3 / 2 : 0); | |
| fill(0, 255, 0); | |
| ellipse(x, y, HEX_SIZE * 0.8); | |
| textSize(14); | |
| text(h.slice(0, 2), x - 10, y + 5); | |
| }); | |
| } | |
| function drawMonsters() { | |
| monsters.forEach(m => { | |
| let x = m.x * HEX_SIZE * 1.5; | |
| let y = m.y * HEX_SIZE * SQRT_3 + (m.x % 2 ? HEX_SIZE * SQRT_3 / 2 : 0); | |
| fill(255, 0, 0); | |
| ellipse(x, y, HEX_SIZE * 0.8); | |
| textSize(20); | |
| text(m.type === 'Godzilla' ? 'π¦' : 'π€', x - 10, y + 5); | |
| }); | |
| } | |
| function drawLoot() { | |
| loot.forEach(l => { | |
| let x = l.x * HEX_SIZE * 1.5; | |
| let y = l.y * HEX_SIZE * SQRT_3 + (l.x % 2 ? HEX_SIZE * SQRT_3 / 2 : 0); | |
| fill(255, 215, 0); | |
| ellipse(x, y, HEX_SIZE * 0.6); | |
| textSize(20); | |
| text(l.type, x - 10, y + 5); | |
| }); | |
| } | |
| function drawExit() { | |
| let x = exit.x * HEX_SIZE * 1.5; | |
| let y = exit.y * HEX_SIZE * SQRT_3 + (exit.x % 2 ? HEX_SIZE * SQRT_3 / 2 : 0); | |
| fill(0, 191, 255); | |
| ellipse(x, y, HEX_SIZE * 0.8); | |
| textSize(20); | |
| text('πͺ', x - 10, y + 5); | |
| } | |
| function drawOverlord() { | |
| if (overlord.alive) { | |
| let x = overlord.x * HEX_SIZE * 1.5; | |
| let y = overlord.y * HEX_SIZE * SQRT_3 + (overlord.x % 2 ? HEX_SIZE * SQRT_3 / 2 : 0); | |
| fill(139, 0, 139); | |
| ellipse(x, y, HEX_SIZE * 1.2); | |
| textSize(20); | |
| text('π', x - 10, y + 5); | |
| } | |
| } | |
| function drawStory() { | |
| fill(0); | |
| textSize(14); | |
| for (let i = 0; i < Math.min(story.length, 5); i++) { | |
| text(story[story.length - 1 - i], 10, 20 + i * 20); | |
| } | |
| } | |
| function mousePressed() { | |
| let i = Math.floor(mouseX / (HEX_SIZE * 1.5)); | |
| let j = Math.floor((mouseY - (i % 2 ? HEX_SIZE * SQRT_3 / 2 : 0)) / (HEX_SIZE * SQRT_3)); | |
| if (i >= 0 && i < hexGrid.length && j >= 0 && j < hexGrid[0].length) { | |
| let hero = heroes[playerId]; | |
| if (hexGrid[i][j].type === 'empty' && Math.abs(hero.x - i) <= 1 && Math.abs(hero.y - j) <= 1) { | |
| moveHero(i, j); | |
| } | |
| } | |
| } | |
| function keyPressed() { | |
| let hero = heroes[playerId]; | |
| if (key === 'w' || key === 'W') moveHero(hero.x, hero.y - 1); | |
| if (key === 's' || key === 'S') moveHero(hero.x, hero.y + 1); | |
| if (key === 'a' || key === 'A') moveHero(hero.x - 1, hero.y); | |
| if (key === 'd' || key === 'D') moveHero(hero.x + 1, hero.y); | |
| if (key === 'q' || key === 'Q') moveHero(hero.x - 1, hero.y - 1); | |
| if (key === 'e' || key === 'E') moveHero(hero.x + 1, hero.y - 1); | |
| } | |
| function moveHero(newX, newY) { | |
| if (newX < 0 || newX >= hexGrid.length || newY < 0 || newY >= hexGrid[0].length) return; | |
| let hero = heroes[playerId]; | |
| let cell = hexGrid[newX][newY]; | |
| if (cell.type === 'empty' || cell.type === 'plant') { | |
| hero.x = newX; | |
| hero.y = newY; | |
| handleInteractions(); | |
| updateState(); | |
| } | |
| } | |
| function handleInteractions() { | |
| let hero = heroes[playerId]; | |
| // Loot | |
| let lootIdx = loot.findIndex(l => l.x === hero.x && l.y === hero.y); | |
| if (lootIdx !== -1) { | |
| let item = loot[lootIdx].type; | |
| if (item === 'βοΈ') hero.atk += 2; | |
| if (item === 'π‘οΈ') hero.def += 2; | |
| if (item === 'π°') hero.xp += 10; | |
| if (item === 'π') hero.gear.push('π'); | |
| loot.splice(lootIdx, 1); | |
| addStory('Loot', item); | |
| } | |
| // Trap | |
| if (hexGrid[hero.x][hero.y].type === 'trap') { | |
| hero.hp -= random(1, 5); | |
| addStory('Trap', hexGrid[hero.x][hero.y].emoji); | |
| } | |
| // Monster combat | |
| let monsterIdx = monsters.findIndex(m => m.x === hero.x && m.y === hero.y); | |
| if (monsterIdx !== -1) { | |
| let monster = monsters[monsterIdx]; | |
| let damage = Math.max(0, hero.atk - 1); | |
| monster.hp -= damage; | |
| hero.hp -= Math.max(0, monster.atk - hero.def); | |
| if (monster.hp <= 0) { | |
| monsters.splice(monsterIdx, 1); | |
| hero.xp += 10; | |
| addStory('Monster', monster.type + ' slain'); | |
| } | |
| } | |
| // Overlord combat | |
| if (overlord.alive && hero.x === overlord.x && hero.y === overlord.y) { | |
| let damage = Math.max(0, hero.atk - 2); | |
| overlord.hp -= damage; | |
| hero.hp -= Math.max(0, overlord.atk - hero.def); | |
| if (overlord.hp <= 0) { | |
| overlord.alive = false; | |
| hero.xp += 50; | |
| addStory('Monster', 'Overlord defeated'); | |
| } | |
| } | |
| // Exit | |
| if (hero.x === exit.x && hero.y === exit.y && hero.gear.includes('π')) { | |
| alert('Victory! You escaped the Hex Dungeon!'); | |
| resetGame(); | |
| } | |
| // Level up | |
| if (hero.xp >= hero.level * 20) { | |
| hero.level += 1; | |
| hero.hp += 5; | |
| hero.atk += 1; | |
| hero.def += 1; | |
| addStory('Hero', 'leveled up'); | |
| } | |
| } | |
| function addStory(type, word) { | |
| let cardIndex = st.session_state.drawn_cards % 52; | |
| let card = JSON.parse(document.getElementById('deck').innerHTML)[cardIndex]; | |
| let suit = card[0]; | |
| let sentence = `A ${SUIT_PROPERTIES[suit]} event: ${type.toLowerCase()} ${word}`; | |
| story.push(sentence); | |
| st.session_state.drawn_cards += 1; | |
| } | |
| function updateState() { | |
| fetch('/update_state', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| hex_grid: hexGrid, | |
| heroes: heroes, | |
| monsters: monsters, | |
| loot: loot, | |
| exit: exit, | |
| overlord: overlord, | |
| story: story, | |
| player_id: playerId | |
| }) | |
| }); | |
| } | |
| """ | |
| # UI Components | |
| if 'player_id' not in st.session_state: | |
| st.session_state.player_id = random.choice(PLAYER_NAMES) | |
| st.session_state.game_state['heroes'][st.session_state.player_id] = { | |
| 'x': 0, 'y': 0, 'hp': 20, 'atk': 3, 'def': 1, 'xp': 0, 'level': 1, 'gear': [] | |
| } | |
| player_id = st.sidebar.selectbox("Choose Hero", PLAYER_NAMES, index=PLAYER_NAMES.index(st.session_state.player_id)) | |
| st.session_state.player_id = player_id | |
| st.sidebar.subheader("π‘οΈ Hero Stats") | |
| hero = st.session_state.game_state['heroes'].get(player_id, {}) | |
| st.sidebar.write(f"HP: {hero.get('hp', 20)} | ATK: {hero.get('atk', 3)} | DEF: {hero.get('def', 1)}") | |
| st.sidebar.write(f"Level: {hero.get('level', 1)} | XP: {hero.get('xp', 0)}") | |
| st.sidebar.write(f"Gear: {', '.join(hero.get('gear', [])) or 'None'}") | |
| st.sidebar.subheader("π Scores") | |
| for p, s in st.session_state.game_state['players'].items(): | |
| st.sidebar.write(f"{p}: {s}") | |
| # Game HTML | |
| game_html = f""" | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.min.js"></script> | |
| <div id="hex_grid" style="display:none">{json.dumps(st.session_state.game_state['hex_grid'])}</div> | |
| <div id="heroes" style="display:none">{json.dumps(st.session_state.game_state['heroes'])}</div> | |
| <div id="monsters" style="display:none">{json.dumps(st.session_state.game_state['monsters'])}</div> | |
| <div id="loot" style="display:none">{json.dumps(st.session_state.game_state['loot'])}</div> | |
| <div id="exit" style="display:none">{json.dumps(st.session_state.game_state['exit'])}</div> | |
| <div id="overlord" style="display:none">{json.dumps(st.session_state.game_state['overlord'])}</div> | |
| <div id="player_id" style="display:none">{player_id}</div> | |
| <div id="story" style="display:none">{json.dumps(st.session_state.game_state['story'])}</div> | |
| <div id="deck" style="display:none">{json.dumps(st.session_state.deck)}</div> | |
| <script>{p5js_code}</script> | |
| """ | |
| # Main layout | |
| st.title("HexCitySaga: Dungeon Crawl") | |
| st.write(f"Hero: {player_id}. Use WASD/QE to move. Goal: Escape (πͺ) with π or slay the Overlord (π).") | |
| st.components.v1.html(game_html, height=500) | |
| # State update handler with Overlord AI | |
| def update_state(data): | |
| st.session_state.game_state.update({ | |
| 'hex_grid': apply_game_of_life(data['hex_grid']), | |
| 'heroes': data['heroes'], | |
| 'monsters': data['monsters'], | |
| 'loot': data['loot'], | |
| 'exit': data['exit'], | |
| 'overlord': data['overlord'], | |
| 'story': data['story'] | |
| }) | |
| # Random events | |
| if random.random() < 0.1: | |
| spawn = {'x': random.randint(0, 15), 'y': random.randint(0, 11)} | |
| if random.random() < 0.5: | |
| st.session_state.game_state['monsters'].append({'x': spawn['x'], 'y': spawn['y'], 'type': random.choice(['Godzilla', 'GiantRobot']), 'hp': 10, 'atk': 2}) | |
| st.session_state.game_state['story'].append(f"A fierce ambush: Monster at ({spawn['x']}, {spawn['y']})!") | |
| else: | |
| st.session_state.game_state['loot'].append({'x': spawn['x'], 'y': spawn['y'], 'type': random.choice(LOOT)}) | |
| st.session_state.game_state['story'].append(f"A treacherous find: Loot at ({spawn['x']}, {spawn['y']})!") | |
| # Traps | |
| if random.random() < 0.05: | |
| tx, ty = random.randint(0, 15), random.randint(0, 11) | |
| if st.session_state.game_state['hex_grid'][tx][ty]['type'] == 'empty': | |
| st.session_state.game_state['hex_grid'][tx][ty] = {'type': 'trap', 'emoji': random.choice(TRAPS), 'alive': True} | |
| # Overlord move | |
| if st.session_state.game_state['overlord']['alive']: | |
| closest_hero = min(st.session_state.game_state['heroes'].items(), key=lambda h: abs(h[1]['x'] - st.session_state.game_state['overlord']['x']) + abs(h[1]['y'] - st.session_state.game_state['overlord']['y'])) | |
| dx = 1 if closest_hero[1]['x'] > st.session_state.game_state['overlord']['x'] else -1 if closest_hero[1]['x'] < st.session_state.game_state['overlord']['x'] else 0 | |
| dy = 1 if closest_hero[1]['y'] > st.session_state.game_state['overlord']['y'] else -1 if closest_hero[1]['y'] < st.session_state.game_state['overlord']['y'] else 0 | |
| new_x, new_y = st.session_state.game_state['overlord']['x'] + dx, st.session_state.game_state['overlord']['y'] + dy | |
| if 0 <= new_x < 16 and 0 <= new_y < 12 and st.session_state.game_state['hex_grid'][new_x][new_y]['type'] == 'empty': | |
| st.session_state.game_state['overlord']['x'], st.session_state.game_state['overlord']['y'] = new_x, new_y | |
| if data['player_id'] not in st.session_state.game_state['players']: | |
| st.session_state.game_state['players'][data['player_id']] = 0 | |
| st.session_state.game_state['players'][data['player_id']] += 1 | |
| st.session_state.game_state['turn'] += 1 | |
| st.rerun() | |
| # Initialize dungeon | |
| for i in range(16): | |
| for j in range(12): | |
| if random.random() < 0.1: | |
| st.session_state.game_state['hex_grid'][i][j] = {'type': 'wall', 'emoji': 'π§±', 'alive': False} | |
| def reset_game(): | |
| st.session_state.game_state = { | |
| 'hex_grid': [[{'type': 'empty', 'emoji': '', 'alive': False} for _ in range(12)] for _ in range(16)], | |
| 'heroes': {player_id: {'x': 0, 'y': 0, 'hp': 20, 'atk': 3, 'def': 1, 'xp': 0, 'level': 1, 'gear': []}}, | |
| 'monsters': [], | |
| 'loot': [], | |
| 'exit': {'x': 15, 'y': 11}, | |
| 'overlord': {'x': 14, 'y': 10, 'hp': 50, 'atk': 5, 'alive': True}, | |
| 'players': {}, | |
| 'story': [], | |
| 'drawn_cards': 0, | |
| 'turn': 0 | |
| } | |
| for i in range(16): | |
| for j in range(12): | |
| if random.random() < 0.1: | |
| st.session_state.game_state['hex_grid'][i][j] = {'type': 'wall', 'emoji': 'π§±', 'alive': False} | |
| if st.sidebar.button("Reset Game"): | |
| reset_game() |