Spaces:
Running
Running
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' | |