|
import streamlit as st |
|
from streamlit.components.v1 import html |
|
import random |
|
import json |
|
import math |
|
|
|
|
|
if 'game_state' not in st.session_state: |
|
st.session_state.game_state = { |
|
'hex_grid': [[{'type': 'empty', 'emoji': ''} for _ in range(15)] for _ in range(20)], |
|
'buildings': [], |
|
'players': {}, |
|
'train': {'x': 0, 'y': 5 * 40, 'dir': 1}, |
|
'monster_mode': False, |
|
'current_monster': 'Godzilla', |
|
'monster_x': 400, |
|
'monster_y': 300 |
|
} |
|
|
|
|
|
random_names = [ |
|
"SkyWalker", "ForestRanger", "CityBuilder", "MonsterTamer", "RailMaster", |
|
"PlantWhisperer", "UrbanWizard", "NatureSage", "GridLord", "EcoWarrior" |
|
] |
|
|
|
|
|
story_beginnings = [ |
|
("๐ ", "In a bustling city nestled between towering mountains,"), |
|
("๐", "A lone figure darted through the streets as the sun began to set,"), |
|
("๐ง ", "Thoughts raced through their mind, planning the next move,"), |
|
("๐ฒ", "Shock rippled through the crowd as the ground trembled,"), |
|
("๐ฌ", "'We need to act now!' someone shouted over the chaos,"), |
|
("๐", "A closer look revealed hidden secrets in the cityโs core,"), |
|
("๐ฌ", "A train screeched into the station, unloading its mysteries,"), |
|
("๐๏ธ", "Lush greenery framed the skyline, a stark contrast to the urban sprawl,"), |
|
("โฉ", "Events unfolded rapidly, leaving no time to breathe,"), |
|
("๐๏ธ", "The horizon glowed with an eerie, captivating light,"), |
|
("๐", "Whispers of wind carried tales of forgotten lands,"), |
|
("๐ฃ๏ธ", "A juicy rumor spread like wildfire among the citizens,"), |
|
("๐คธ", "With a simple leap, the adventure began anew,"), |
|
("๐งฉ", "The essentials of survival lay scattered across the grid,"), |
|
("๐ถ", "A slow walk revealed the cityโs heartbeat,"), |
|
("๐", "A journey unfolded, waves of change crashing in,"), |
|
("๐ฏ", "Focus sharpened as the goal came into view,"), |
|
("๐ค", "Neurotic thoughts plagued the plannerโs restless mind,") |
|
] |
|
|
|
|
|
nature_labels = [ |
|
"Whispering Winds", "Sassy Sunlight", "Grumpy Gravel", "Chatty Clouds", |
|
"Pensive Pines", "Witty Wetlands", "Sneaky Streams", "Bouncy Breezes", |
|
"Daring Dewdrops", "Cheeky Crags" |
|
] |
|
|
|
|
|
plants = ["๐ฑ", "๐ฒ", "๐ณ", "๐ด", "๐ต", "๐พ", "๐ฟ", "๐", "๐", "๐"] |
|
buildings = ["๐ ", "๐ก", "๐ข", "๐ฃ", "๐ค", "๐ฅ", "๐ฆ", "๐จ", "๐ฉ", "๐ช"] |
|
creatures = ["๐พ", "๐ฑ", "๐ถ", "๐ญ", "๐ฐ", "๐ฆ", "๐ป", "๐ท", "๐ฎ", "๐ธ"] |
|
|
|
|
|
p5js_code = """ |
|
let hexSize = 40; |
|
let hexGrid = []; |
|
let buildings = []; |
|
let train = { x: 0, y: 0, dir: 1 }; |
|
let monsterMode = false; |
|
let currentMonster = 'Godzilla'; |
|
let monsterX, monsterY; |
|
let angle = PI / 6; |
|
let debugText = "Loading..."; |
|
let playerId = null; |
|
const sqrt3 = Math.sqrt(3); |
|
|
|
function setup() { |
|
createCanvas(800, 600); |
|
hexGrid = JSON.parse(document.getElementById('game_state').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; |
|
debugText = `Player ${playerId} joined. Click to build!`; |
|
// Set train track on row 5 |
|
for (let i = 0; i < hexGrid.length; i++) { |
|
hexGrid[i][5].type = 'track'; |
|
} |
|
} |
|
|
|
function draw() { |
|
background(220); |
|
drawHexGrid(); |
|
drawBuildings(); |
|
drawTrain(); |
|
if (monsterMode) drawMonster(); |
|
fill(0); |
|
textSize(12); |
|
text(debugText, 10, 20); |
|
} |
|
|
|
function drawHexGrid() { |
|
for (let i = 0; i < hexGrid.length; i++) { |
|
for (let j = 0; j < hexGrid[i].length; j++) { |
|
let x = i * hexSize * 1.5; |
|
let y = j * hexSize * sqrt3 + (i % 2 === 1 ? hexSize * sqrt3 / 2 : 0); |
|
let z = j * hexSize * sin(angle); |
|
fill(hexGrid[i][j].type === 'track' ? 100 : 150, 200, 150); |
|
stroke(0); |
|
beginShape(); |
|
for (let k = 0; k < 6; k++) { |
|
let angleRad = PI / 3 * k; |
|
vertex(x + hexSize * cos(angleRad), y + hexSize * sin(angleRad) - z); |
|
} |
|
endShape(CLOSE); |
|
if (hexGrid[i][j].emoji) { |
|
textSize(20); |
|
text(hexGrid[i][j].emoji, x - 10, y + 5 - z); |
|
} |
|
} |
|
} |
|
} |
|
|
|
function drawBuildings() { |
|
buildings.forEach(b => { |
|
let x = b.x * hexSize * 1.5; |
|
let y = b.y * hexSize * sqrt3 + (b.x % 2 === 1 ? hexSize * sqrt3 / 2 : 0); |
|
let z = b.y * hexSize * sin(angle); |
|
fill(b.color); |
|
noStroke(); |
|
beginShape(); |
|
if (b.type === 'Residential') { |
|
vertex(x + 10, y + 30 - z); vertex(x + 20, y + 10 - z); vertex(x + 30, y + 30 - z); |
|
vertex(x + 25, y + 30 - z); vertex(x + 25, y + 35 - z); vertex(x + 15, y + 35 - z); |
|
vertex(x + 15, y + 30 - z); vertex(x + 17, y + 25 - z); vertex(x + 23, y + 25 - z); |
|
} else if (b.type === 'Commercial') { |
|
vertex(x + 10, y + 35 - z); vertex(x + 15, y + 15 - z); vertex(x + 25, y + 15 - z); |
|
vertex(x + 30, y + 35 - z); vertex(x + 27, y + 35 - z); vertex(x + 27, y + 20 - z); |
|
vertex(x + 13, y + 20 - z); vertex(x + 13, y + 35 - z); |
|
} else if (b.type === 'Industrial') { |
|
vertex(x + 5, y + 35 - z); vertex(x + 15, y + 20 - z); vertex(x + 25, y + 20 - z); |
|
vertex(x + 35, y + 35 - z); vertex(x + 30, y + 35 - z); vertex(x + 30, y + 15 - z); |
|
vertex(x + 33, y + 15 - z); vertex(x + 33, y + 35 - z); |
|
} else if (b.type === 'School') { |
|
vertex(x + 5, y + 35 - z); vertex(x + 10, y + 20 - z); vertex(x + 30, y + 20 - z); |
|
vertex(x + 35, y + 35 - z); vertex(x + 25, y + 35 - z); vertex(x + 25, y + 10 - z); |
|
vertex(x + 27, y + 10 - z); vertex(x + 27, y + 35 - z); |
|
} else if (b.type === 'PowerPlant') { |
|
vertex(x + 5, y + 35 - z); vertex(x + 15, y + 15 - z); vertex(x + 25, y + 15 - z); |
|
vertex(x + 35, y + 35 - z); vertex(x + 30, y + 35 - z); vertex(x + 30, y + 25 - z); |
|
vertex(x + 20, y + 25 - z); vertex(x + 20, y + 35 - z); |
|
} |
|
endShape(CLOSE); |
|
}); |
|
} |
|
|
|
function drawTrain() { |
|
let tx = train.x; |
|
let ty = train.y; |
|
let tz = train.y * sin(angle); |
|
fill(150, 50, 50); |
|
noStroke(); |
|
beginShape(); |
|
vertex(tx + 10, ty + 10 - tz); vertex(tx + 30, ty + 10 - tz); vertex(tx + 35, ty + 20 - tz); |
|
vertex(tx + 30, ty + 30 - tz); vertex(tx + 10, ty + 30 - tz); vertex(tx + 5, ty + 20 - tz); |
|
vertex(tx + 15, ty + 20 - tz); vertex(tx + 15, ty + 15 - tz); vertex(tx + 25, ty + 15 - tz); |
|
vertex(tx + 25, ty + 20 - tz); |
|
endShape(CLOSE); |
|
train.x += train.dir * 2; |
|
if (train.x > width || train.x < 0) train.dir *= -1; |
|
} |
|
|
|
function drawMonster() { |
|
let mz = monsterY * sin(angle); |
|
fill(255, 0, 0); |
|
noStroke(); |
|
beginShape(); |
|
if (currentMonster === 'Godzilla') { |
|
vertex(monsterX, monsterY + 40 - mz); vertex(monsterX + 10, monsterY + 20 - mz); |
|
vertex(monsterX + 20, monsterY - mz); vertex(monsterX + 30, monsterY + 20 - mz); |
|
vertex(monsterX + 40, monsterY + 40 - mz); vertex(monsterX + 35, monsterY + 50 - mz); |
|
vertex(monsterX + 25, monsterY + 60 - mz); vertex(monsterX + 15, monsterY + 50 - mz); |
|
vertex(monsterX + 20, monsterY + 40 - mz); vertex(monsterX + 25, monsterY + 30 - mz); |
|
} else if (currentMonster === 'GiantRobot') { |
|
vertex(monsterX, monsterY + 40 - mz); vertex(monsterX + 10, monsterY + 20 - mz); |
|
vertex(monsterX + 15, monsterY - mz); vertex(monsterX + 25, monsterY - mz); |
|
vertex(monsterX + 30, monsterY + 20 - mz); vertex(monsterX + 40, monsterY + 40 - mz); |
|
vertex(monsterX + 35, monsterY + 50 - mz); vertex(monsterX + 20, monsterY + 60 - mz); |
|
vertex(monsterX + 5, monsterY + 50 - mz); vertex(monsterX + 15, monsterY + 30 - mz); |
|
} |
|
endShape(CLOSE); |
|
monsterX += random(-5, 5); |
|
monsterY += random(-5, 5); |
|
monsterX = constrain(monsterX, 0, width); |
|
monsterY = constrain(monsterY, 0, height); |
|
} |
|
|
|
function mousePressed() { |
|
let i = floor(mouseX / (hexSize * 1.5)); |
|
let j = floor((mouseY - (i % 2 === 1 ? hexSize * sqrt3 / 2 : 0)) / (hexSize * sqrt3)); |
|
if (i >= 0 && i < hexGrid.length && j >= 0 && j < hexGrid[0].length) { |
|
if (hexGrid[i][j].type === 'empty' && !monsterMode) { |
|
let selectedEmoji = document.getElementById('selected_emoji').innerHTML; |
|
if (selectedEmoji.startsWith('๐ ') || selectedEmoji.startsWith('๐ก') || selectedEmoji.startsWith('๐ข') || |
|
selectedEmoji.startsWith('๐ฃ') || selectedEmoji.startsWith('๐ค') || selectedEmoji.startsWith('๐ฅ') || |
|
selectedEmoji.startsWith('๐ฆ') || selectedEmoji.startsWith('๐จ') || selectedEmoji.startsWith('๐ฉ') || |
|
selectedEmoji.startsWith('๐ช')) { |
|
let types = ['Residential', 'Commercial', 'Industrial', 'School', 'PowerPlant']; |
|
let colors = [[0, 200, 0], [0, 0, 200], [200, 200, 0], [200, 0, 200], [100, 100, 100]]; |
|
let idx = floor(random(5)); |
|
buildings.push({ x: i, y: j, type: types[idx], color: colors[idx], player: playerId }); |
|
hexGrid[i][j].type = 'building'; |
|
} else { |
|
hexGrid[i][j].type = 'placed'; |
|
hexGrid[i][j].emoji = selectedEmoji; |
|
} |
|
debugText = `Player ${playerId} placed ${selectedEmoji} at (${i}, ${j})`; |
|
updateState(); |
|
} |
|
} |
|
} |
|
|
|
function keyPressed() { |
|
if (key === 'm' || key === 'M') { |
|
monsterMode = !monsterMode; |
|
debugText = `Monster Mode: ${monsterMode}`; |
|
updateState(); |
|
} |
|
if (key === 'g' || key === 'G') { |
|
currentMonster = 'Godzilla'; |
|
debugText = "Monster set to Godzilla"; |
|
updateState(); |
|
} |
|
if (key === 'r' || key === 'R') { |
|
currentMonster = 'GiantRobot'; |
|
debugText = "Monster set to Giant Robot"; |
|
updateState(); |
|
} |
|
if (key === 't' || key === 'T') { |
|
train.dir *= -1; |
|
debugText = "Train direction reversed"; |
|
updateState(); |
|
} |
|
} |
|
|
|
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, |
|
player_id: playerId |
|
}) |
|
}); |
|
} |
|
""" |
|
|
|
|
|
if 'player_id' not in st.session_state: |
|
st.session_state.player_id = random.choice(random_names) |
|
player_id = st.sidebar.selectbox("Choose Player", random_names, index=random_names.index(st.session_state.player_id)) |
|
st.session_state.player_id = player_id |
|
|
|
|
|
story_html = f""" |
|
<div style='background: #f9f9f9; padding: 10px; border-radius: 5px; margin-bottom: 10px;'> |
|
<h3>๐ Story Plot</h3> |
|
<p>{random.choice(story_beginnings)[1]} {random.choice(nature_labels)} stirred the scene.</p> |
|
</div> |
|
""" |
|
st.sidebar.markdown(story_html, unsafe_allow_html=True) |
|
|
|
|
|
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): |
|
col = cols[i % 5] |
|
if col.button(emoji, key=f"emoji_{i}"): |
|
st.session_state.selected_emoji = emoji |
|
selected_emoji = emoji |
|
|
|
|
|
if player_id not in st.session_state.game_state['players']: |
|
st.session_state.game_state['players'][player_id] = 0 |
|
for b in st.session_state.game_state['buildings']: |
|
if b.get('player') == player_id: |
|
st.session_state.game_state['players'][player_id] += 1 |
|
for i in range(len(st.session_state.game_state['hex_grid'])): |
|
for j in range(len(st.session_state.game_state['hex_grid'][i])): |
|
if st.session_state.game_state['hex_grid'][i][j]['type'] == 'placed': |
|
st.session_state.game_state['players'][player_id] += 1 |
|
score_html = "<div style='background: #f9f9f9; padding: 10px; border-radius: 5px;'><h3>๐ Scores</h3><ul>" |
|
for p, s in st.session_state.game_state['players'].items(): |
|
score_html += f"<li>{p}: {s}</li>" |
|
score_html += "</ul></div>" |
|
st.sidebar.markdown(score_html, unsafe_allow_html=True) |
|
|
|
|
|
game_html = f""" |
|
<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.min.js"></script> |
|
<style> |
|
body {{ font-family: Arial, sans-serif; margin: 0; padding: 0; background: #f0f0f0; }} |
|
#controls {{ padding: 10px; background: #e0e0e0; }} |
|
button {{ padding: 8px 16px; margin: 5px; background: #4CAF50; color: white; border: none; border-radius: 5px; cursor: pointer; }} |
|
button:hover {{ background: #45a049; }} |
|
select {{ padding: 8px; margin: 5px; border-radius: 5px; }} |
|
</style> |
|
</head> |
|
<body> |
|
<div id="controls"> |
|
<button onclick="toggleMonster()">Toggle Monster Mode (M)</button> |
|
<select onchange="setMonster(this.value)"> |
|
<option value="Godzilla">Godzilla (G)</option> |
|
<option value="GiantRobot">Giant Robot (R)</option> |
|
</select> |
|
<button onclick="reverseTrain()">Reverse Train (T)</button> |
|
</div> |
|
<div id="sketch-holder"></div> |
|
<div id="game_state" 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> |
|
|
|
<script> |
|
{p5js_code} |
|
|
|
function toggleMonster() {{ window.toggleMonsterMode(); }} |
|
function setMonster(monster) {{ window.setMonster(monster); }} |
|
function reverseTrain() {{ window.reverseTrain(); }} |
|
</script> |
|
</body> |
|
</html> |
|
""" |
|
|
|
|
|
st.title("HexCity 2000 Multiplayer Adventure") |
|
st.write(f"Player: {player_id}. Click to place {selected_emoji}. Use M/G/R/T keys.") |
|
html(game_html, height=650) |
|
|
|
|
|
def update_state(data): |
|
st.session_state.game_state['hex_grid'] = data['hex_grid'] |
|
st.session_state.game_state['buildings'] = data['buildings'] |
|
st.session_state.game_state['train'] = data['train'] |
|
st.session_state.game_state['monster_mode'] = data['monster_mode'] |
|
st.session_state.game_state['current_monster'] = data['current_monster'] |
|
st.session_state.game_state['monster_x'] = data['monster_x'] |
|
st.session_state.game_state['monster_y'] = data['monster_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.rerun() |
|
|
|
|
|
if st.session_state.get('last_update'): |
|
update_state(st.session_state['last_update']) |