HexCitySaga / app.py
awacke1's picture
Update app.py
b36f898 verified
raw
history blame
10.6 kB
import streamlit as st
import random
import json
import math
# Set page config for a wider, immersive layout
st.set_page_config(page_title="HexCitySaga", 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)],
'buildings': [],
'players': {},
'train': {'x': 0, 'y': 5, 'dir': 1},
'monster_mode': False,
'current_monster': 'Godzilla',
'monster_x': 8,
'monster_y': 6,
'story': [],
'drawn_cards': 0
}
# Constants
HEX_SIZE = 40
PLAYER_NAMES = ["SkyWalker", "ForestRanger", "CityBuilder", "MonsterTamer", "RailMaster"]
PLANTS = ["🌱", "🌲", "🌳", "🌴", "🌡"]
BUILDINGS = ["🏠", "🏑", "🏒", "πŸ₯", "🏦"]
CREATURES = ["🐾", "🐱", "🐢", "🐭", "🐰"]
SUIT_PROPERTIES = {"Hearts": "emotional", "Diamonds": "wealthy", "Clubs": "conflict", "Spades": "mysterious"}
SENTENCE_TEMPLATES = {
"Building": "A {property} {word} rose in the city.",
"Monster": "A {property} {word} emerged to challenge all!",
"Train": "The {property} train sped through with {word}."
}
# Game of Life rules
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 ['building', 'placed']:
grid[i][j]['alive'] = True
if neighbors < 2 or neighbors > 3: # Underpopulation or overpopulation
new_grid[i][j]['alive'] = False
new_grid[i][j]['type'] = 'empty'
new_grid[i][j]['emoji'] = ''
elif neighbors in [2, 3]:
new_grid[i][j]['alive'] = True
elif neighbors == 3 and grid[i][j]['type'] == 'empty': # Reproduction
new_grid[i][j]['type'] = 'placed'
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 for storytelling
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 buildings = [];
let train = {};
let monsterMode = false;
let currentMonster = '';
let monsterX, monsterY;
let playerId;
let story = [];
function setup() {
createCanvas(640, 480);
updateFromState();
}
function updateFromState() {
hexGrid = JSON.parse(document.getElementById('hex_grid').innerHTML);
buildings = JSON.parse(document.getElementById('buildings').innerHTML);
train = JSON.parse(document.getElementById('train').innerHTML);
monsterMode = JSON.parse(document.getElementById('monster_mode').innerHTML);
currentMonster = document.getElementById('current_monster').innerHTML;
monsterX = parseInt(document.getElementById('monster_x').innerHTML);
monsterY = parseInt(document.getElementById('monster_y').innerHTML);
playerId = document.getElementById('player_id').innerHTML;
story = JSON.parse(document.getElementById('story').innerHTML);
}
function draw() {
background(220);
drawHexGrid();
drawBuildings();
drawTrain();
if (monsterMode) drawMonster();
drawStory();
train.x += train.dir * 0.05;
if (train.x >= hexGrid.length || train.x < 0) train.dir *= -1;
}
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 === 'track' ? '#808080' : hexGrid[i][j].alive ? '#90EE90' : '#D3D3D3');
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 drawBuildings() {
buildings.forEach(b => {
let x = b.x * HEX_SIZE * 1.5;
let y = b.y * HEX_SIZE * SQRT_3 + (b.x % 2 ? HEX_SIZE * SQRT_3 / 2 : 0);
fill(b.color[0], b.color[1], b.color[2]);
noStroke();
ellipse(x, y, HEX_SIZE * 0.8);
textSize(20);
text(b.emoji, x - 10, y + 5);
});
}
function drawTrain() {
let x = train.x * HEX_SIZE * 1.5;
let y = train.y * HEX_SIZE * SQRT_3;
fill(150, 50, 50);
rect(x - 15, y - 10, 30, 20);
text('πŸš‚', x - 10, y + 5);
}
function drawMonster() {
let x = monsterX * HEX_SIZE * 1.5;
let y = monsterY * HEX_SIZE * SQRT_3 + (monsterX % 2 ? HEX_SIZE * SQRT_3 / 2 : 0);
fill(255, 0, 0);
ellipse(x, y, HEX_SIZE * 1.2);
text(currentMonster === 'Godzilla' ? 'πŸ¦–' : 'πŸ€–', 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 && hexGrid[i][j].type === 'empty') {
let emoji = document.getElementById('selected_emoji').innerHTML;
if (!monsterMode) {
if (['🏠', '🏑', '🏒', 'πŸ₯', '🏦'].includes(emoji)) {
buildings.push({
x: i, y: j, emoji: emoji,
color: [random(100, 255), random(100, 255), random(100, 255)],
player: playerId
});
hexGrid[i][j].type = 'building';
hexGrid[i][j].alive = true;
addStory('Building', emoji);
} else {
hexGrid[i][j].type = 'placed';
hexGrid[i][j].emoji = emoji;
hexGrid[i][j].alive = true;
}
updateState();
}
}
}
function keyPressed() {
if (key === 'm' || key === 'M') {
monsterMode = !monsterMode;
if (monsterMode) addStory('Monster', currentMonster);
}
if (key === 'g' || key === 'G') currentMonster = 'Godzilla';
if (key === 'r' || key === 'R') currentMonster = 'GiantRobot';
if (key === 't' || key === 'T') {
train.dir *= -1;
addStory('Train', 'purpose');
}
updateState();
}
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 with ${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,
buildings: buildings,
train: train,
monster_mode: monsterMode,
current_monster: currentMonster,
monster_x: monsterX,
monster_y: monsterY,
story: story,
player_id: playerId
})
});
}
"""
# UI Components
if 'player_id' not in st.session_state:
st.session_state.player_id = random.choice(PLAYER_NAMES)
player_id = st.sidebar.selectbox("Choose Player", PLAYER_NAMES, index=PLAYER_NAMES.index(st.session_state.player_id))
st.session_state.player_id = player_id
st.sidebar.subheader("🎨 Palette")
cols = st.sidebar.columns(5)
selected_emoji = st.session_state.get('selected_emoji', PLANTS[0])
for i, emoji in enumerate(PLANTS + BUILDINGS + CREATURES):
if cols[i % 5].button(emoji, key=f"emoji_{i}"):
st.session_state.selected_emoji = emoji
st.sidebar.subheader("πŸ† Scores")
for p, s in st.session_state.game_state['players'].items():
st.sidebar.write(f"{p}: {s}")
# Game HTML with deck included
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="buildings" style="display:none">{json.dumps(st.session_state.game_state['buildings'])}</div>
<div id="train" style="display:none">{json.dumps(st.session_state.game_state['train'])}</div>
<div id="monster_mode" style="display:none">{json.dumps(st.session_state.game_state['monster_mode'])}</div>
<div id="current_monster" style="display:none">{st.session_state.game_state['current_monster']}</div>
<div id="monster_x" style="display:none">{st.session_state.game_state['monster_x']}</div>
<div id="monster_y" style="display:none">{st.session_state.game_state['monster_y']}</div>
<div id="player_id" style="display:none">{player_id}</div>
<div id="selected_emoji" style="display:none">{selected_emoji}</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: Build, Battle, and Tell Your Tale")
st.write(f"Player: {player_id}. Click to place {selected_emoji}. Keys: M (monster), G/R (monster type), T (train)")
st.components.v1.html(game_html, height=500)
# State update handler
def update_state(data):
st.session_state.game_state.update({
'hex_grid': apply_game_of_life(data['hex_grid']),
'buildings': data['buildings'],
'train': data['train'],
'monster_mode': data['monster_mode'],
'current_monster': data['current_monster'],
'monster_x': data['monster_x'],
'monster_y': data['monster_y'],
'story': data['story']
})
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.rerun()
# Initialize tracks
for i in range(len(st.session_state.game_state['hex_grid'])):
st.session_state.game_state['hex_grid'][i][5]['type'] = 'track'