Spaces:
Running
Running
Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,306 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import streamlit as st
|
2 |
+
import random
|
3 |
+
import json
|
4 |
+
import math
|
5 |
+
|
6 |
+
# Initialize game state
|
7 |
+
if 'game_state' not in st.session_state:
|
8 |
+
st.session_state.game_state = {
|
9 |
+
'hex_grid': [[{'type': 'empty', 'emoji': '', 'alive': False} for _ in range(12)] for _ in range(16)],
|
10 |
+
'buildings': [],
|
11 |
+
'players': {},
|
12 |
+
'train': {'x': 0, 'y': 5, 'dir': 1},
|
13 |
+
'monster_mode': False,
|
14 |
+
'current_monster': 'Godzilla',
|
15 |
+
'monster_x': 8,
|
16 |
+
'monster_y': 6,
|
17 |
+
'story': [],
|
18 |
+
'drawn_cards': 0
|
19 |
+
}
|
20 |
+
|
21 |
+
# Constants
|
22 |
+
HEX_SIZE = 40
|
23 |
+
PLAYER_NAMES = ["SkyWalker", "ForestRanger", "CityBuilder", "MonsterTamer", "RailMaster"]
|
24 |
+
PLANTS = ["π±", "π²", "π³", "π΄", "π΅"]
|
25 |
+
BUILDINGS = ["π ", "π‘", "π’", "π₯", "π¦"]
|
26 |
+
CREATURES = ["πΎ", "π±", "πΆ", "π", "π°"]
|
27 |
+
SUIT_PROPERTIES = {"Hearts": "emotional", "Diamonds": "wealthy", "Clubs": "conflict", "Spades": "mysterious"}
|
28 |
+
SENTENCE_TEMPLATES = {
|
29 |
+
"Building": "A {property} {word} rose in the city.",
|
30 |
+
"Monster": "A {property} {word} emerged to challenge all!",
|
31 |
+
"Train": "The {property} train sped through with {word}."
|
32 |
+
}
|
33 |
+
|
34 |
+
# Game of Life rules: Conway-inspired for buildings
|
35 |
+
def apply_game_of_life(grid):
|
36 |
+
new_grid = [[cell.copy() for cell in row] for row in grid]
|
37 |
+
for i in range(len(grid)):
|
38 |
+
for j in range(len(grid[0])):
|
39 |
+
neighbors = count_neighbors(grid, i, j)
|
40 |
+
if grid[i][j]['type'] in ['building', 'placed']:
|
41 |
+
grid[i][j]['alive'] = True
|
42 |
+
if neighbors < 2 or neighbors > 3: # Underpopulation or overpopulation
|
43 |
+
new_grid[i][j]['alive'] = False
|
44 |
+
new_grid[i][j]['type'] = 'empty'
|
45 |
+
new_grid[i][j]['emoji'] = ''
|
46 |
+
elif neighbors in [2, 3]:
|
47 |
+
new_grid[i][j]['alive'] = True
|
48 |
+
elif neighbors == 3 and grid[i][j]['type'] == 'empty': # Reproduction
|
49 |
+
new_grid[i][j]['type'] = 'placed'
|
50 |
+
new_grid[i][j]['emoji'] = random.choice(PLANTS)
|
51 |
+
new_grid[i][j]['alive'] = True
|
52 |
+
return new_grid
|
53 |
+
|
54 |
+
def count_neighbors(grid, x, y):
|
55 |
+
count = 0
|
56 |
+
for di in [-1, 0, 1]:
|
57 |
+
for dj in [-1, 0, 1]:
|
58 |
+
if di == 0 and dj == 0:
|
59 |
+
continue
|
60 |
+
ni, nj = x + di, y + dj
|
61 |
+
if 0 <= ni < len(grid) and 0 <= nj < len(grid[0]) and grid[ni][nj]['alive']:
|
62 |
+
count += 1
|
63 |
+
return count
|
64 |
+
|
65 |
+
# Card deck for storytelling
|
66 |
+
def create_deck():
|
67 |
+
suits = ["Hearts", "Diamonds", "Clubs", "Spades"]
|
68 |
+
ranks = list(range(1, 14))
|
69 |
+
deck = [(suit, rank) for suit in suits for rank in ranks]
|
70 |
+
random.shuffle(deck)
|
71 |
+
return deck
|
72 |
+
|
73 |
+
if 'deck' not in st.session_state:
|
74 |
+
st.session_state.deck = create_deck()
|
75 |
+
|
76 |
+
# p5.js rendering
|
77 |
+
p5js_code = """
|
78 |
+
const HEX_SIZE = 40;
|
79 |
+
const SQRT_3 = Math.sqrt(3);
|
80 |
+
let hexGrid = [];
|
81 |
+
let buildings = [];
|
82 |
+
let train = {};
|
83 |
+
let monsterMode = false;
|
84 |
+
let currentMonster = '';
|
85 |
+
let monsterX, monsterY;
|
86 |
+
let playerId;
|
87 |
+
let story = [];
|
88 |
+
|
89 |
+
function setup() {
|
90 |
+
createCanvas(640, 480);
|
91 |
+
updateFromState();
|
92 |
+
}
|
93 |
+
|
94 |
+
function updateFromState() {
|
95 |
+
hexGrid = JSON.parse(document.getElementById('hex_grid').innerHTML);
|
96 |
+
buildings = JSON.parse(document.getElementById('buildings').innerHTML);
|
97 |
+
train = JSON.parse(document.getElementById('train').innerHTML);
|
98 |
+
monsterMode = JSON.parse(document.getElementById('monster_mode').innerHTML);
|
99 |
+
currentMonster = document.getElementById('current_monster').innerHTML;
|
100 |
+
monsterX = parseInt(document.getElementById('monster_x').innerHTML);
|
101 |
+
monsterY = parseInt(document.getElementById('monster_y').innerHTML);
|
102 |
+
playerId = document.getElementById('player_id').innerHTML;
|
103 |
+
story = JSON.parse(document.getElementById('story').innerHTML);
|
104 |
+
}
|
105 |
+
|
106 |
+
function draw() {
|
107 |
+
background(220);
|
108 |
+
drawHexGrid();
|
109 |
+
drawBuildings();
|
110 |
+
drawTrain();
|
111 |
+
if (monsterMode) drawMonster();
|
112 |
+
drawStory();
|
113 |
+
train.x += train.dir * 0.05;
|
114 |
+
if (train.x >= hexGrid.length || train.x < 0) train.dir *= -1;
|
115 |
+
}
|
116 |
+
|
117 |
+
function drawHexGrid() {
|
118 |
+
for (let i = 0; i < hexGrid.length; i++) {
|
119 |
+
for (let j = 0; j < hexGrid[i].length; j++) {
|
120 |
+
let x = i * HEX_SIZE * 1.5;
|
121 |
+
let y = j * HEX_SIZE * SQRT_3 + (i % 2 ? HEX_SIZE * SQRT_3 / 2 : 0);
|
122 |
+
fill(hexGrid[i][j].type === 'track' ? '#808080' : hexGrid[i][j].alive ? '#90EE90' : '#D3D3D3');
|
123 |
+
stroke(0);
|
124 |
+
drawHex(x, y);
|
125 |
+
if (hexGrid[i][j].emoji) {
|
126 |
+
textSize(20);
|
127 |
+
text(hexGrid[i][j].emoji, x - 10, y + 5);
|
128 |
+
}
|
129 |
+
}
|
130 |
+
}
|
131 |
+
}
|
132 |
+
|
133 |
+
function drawHex(x, y) {
|
134 |
+
beginShape();
|
135 |
+
for (let a = 0; a < 6; a++) {
|
136 |
+
let angle = TWO_PI / 6 * a;
|
137 |
+
vertex(x + HEX_SIZE * cos(angle), y + HEX_SIZE * sin(angle));
|
138 |
+
}
|
139 |
+
endShape(CLOSE);
|
140 |
+
}
|
141 |
+
|
142 |
+
function drawBuildings() {
|
143 |
+
buildings.forEach(b => {
|
144 |
+
let x = b.x * HEX_SIZE * 1.5;
|
145 |
+
let y = b.y * HEX_SIZE * SQRT_3 + (b.x % 2 ? HEX_SIZE * SQRT_3 / 2 : 0);
|
146 |
+
fill(b.color[0], b.color[1], b.color[2]);
|
147 |
+
noStroke();
|
148 |
+
ellipse(x, y, HEX_SIZE * 0.8);
|
149 |
+
textSize(20);
|
150 |
+
text(b.emoji, x - 10, y + 5);
|
151 |
+
});
|
152 |
+
}
|
153 |
+
|
154 |
+
function drawTrain() {
|
155 |
+
let x = train.x * HEX_SIZE * 1.5;
|
156 |
+
let y = train.y * HEX_SIZE * SQRT_3;
|
157 |
+
fill(150, 50, 50);
|
158 |
+
rect(x - 15, y - 10, 30, 20);
|
159 |
+
text('π', x - 10, y + 5);
|
160 |
+
}
|
161 |
+
|
162 |
+
function drawMonster() {
|
163 |
+
let x = monsterX * HEX_SIZE * 1.5;
|
164 |
+
let y = monsterY * HEX_SIZE * SQRT_3 + (monsterX % 2 ? HEX_SIZE * SQRT_3 / 2 : 0);
|
165 |
+
fill(255, 0, 0);
|
166 |
+
ellipse(x, y, HEX_SIZE * 1.2);
|
167 |
+
text(currentMonster === 'Godzilla' ? 'π¦' : 'π€', x - 10, y + 5);
|
168 |
+
}
|
169 |
+
|
170 |
+
function drawStory() {
|
171 |
+
fill(0);
|
172 |
+
textSize(14);
|
173 |
+
for (let i = 0; i < Math.min(story.length, 5); i++) {
|
174 |
+
text(story[story.length - 1 - i], 10, 20 + i * 20);
|
175 |
+
}
|
176 |
+
}
|
177 |
+
|
178 |
+
function mousePressed() {
|
179 |
+
let i = Math.floor(mouseX / (HEX_SIZE * 1.5));
|
180 |
+
let j = Math.floor((mouseY - (i % 2 ? HEX_SIZE * SQRT_3 / 2 : 0)) / (HEX_SIZE * SQRT_3));
|
181 |
+
if (i >= 0 && i < hexGrid.length && j >= 0 && j < hexGrid[0].length && hexGrid[i][j].type === 'empty') {
|
182 |
+
let emoji = document.getElementById('selected_emoji').innerHTML;
|
183 |
+
if (!monsterMode) {
|
184 |
+
if (emoji.startsWith('π ') || emoji.startsWith('π‘') || emoji.startsWith('π’') ||
|
185 |
+
emoji.startsWith('π₯') || emoji.startsWith('π¦')) {
|
186 |
+
buildings.push({
|
187 |
+
x: i, y: j, emoji: emoji,
|
188 |
+
color: [random(100, 255), random(100, 255), random(100, 255)],
|
189 |
+
player: playerId
|
190 |
+
});
|
191 |
+
hexGrid[i][j].type = 'building';
|
192 |
+
hexGrid[i][j].alive = true;
|
193 |
+
addStory('Building', emoji);
|
194 |
+
} else {
|
195 |
+
hexGrid[i][j].type = 'placed';
|
196 |
+
hexGrid[i][j].emoji = emoji;
|
197 |
+
hexGrid[i][j].alive = true;
|
198 |
+
}
|
199 |
+
updateState();
|
200 |
+
}
|
201 |
+
}
|
202 |
+
}
|
203 |
+
|
204 |
+
function keyPressed() {
|
205 |
+
if (key === 'm' || key === 'M') {
|
206 |
+
monsterMode = !monsterMode;
|
207 |
+
if (monsterMode) addStory('Monster', currentMonster);
|
208 |
+
}
|
209 |
+
if (key === 'g' || key === 'G') currentMonster = 'Godzilla';
|
210 |
+
if (key === 'r' || key === 'R') currentMonster = 'GiantRobot';
|
211 |
+
if (key === 't' || key === 'T') {
|
212 |
+
train.dir *= -1;
|
213 |
+
addStory('Train', 'purpose');
|
214 |
+
}
|
215 |
+
updateState();
|
216 |
+
}
|
217 |
+
|
218 |
+
function addStory(type, word) {
|
219 |
+
let card = st.session_state.deck[st.session_state.drawn_cards % 52];
|
220 |
+
let suit = card[0];
|
221 |
+
let sentence = SENTENCE_TEMPLATES[type].format(
|
222 |
+
property: SUIT_PROPERTIES[suit],
|
223 |
+
word: word
|
224 |
+
);
|
225 |
+
story.push(sentence);
|
226 |
+
st.session_state.drawn_cards += 1;
|
227 |
+
}
|
228 |
+
|
229 |
+
function updateState() {
|
230 |
+
fetch('/update_state', {
|
231 |
+
method: 'POST',
|
232 |
+
headers: { 'Content-Type': 'application/json' },
|
233 |
+
body: JSON.stringify({
|
234 |
+
hex_grid: hexGrid,
|
235 |
+
buildings: buildings,
|
236 |
+
train: train,
|
237 |
+
monster_mode: monsterMode,
|
238 |
+
current_monster: currentMonster,
|
239 |
+
monster_x: monsterX,
|
240 |
+
monster_y: monsterY,
|
241 |
+
story: story,
|
242 |
+
player_id: playerId
|
243 |
+
})
|
244 |
+
});
|
245 |
+
}
|
246 |
+
"""
|
247 |
+
|
248 |
+
# UI Components
|
249 |
+
if 'player_id' not in st.session_state:
|
250 |
+
st.session_state.player_id = random.choice(PLAYER_NAMES)
|
251 |
+
|
252 |
+
player_id = st.sidebar.selectbox("Choose Player", PLAYER_NAMES, index=PLAYER_NAMES.index(st.session_state.player_id))
|
253 |
+
st.session_state.player_id = player_id
|
254 |
+
|
255 |
+
st.sidebar.subheader("π¨ Palette")
|
256 |
+
cols = st.sidebar.columns(5)
|
257 |
+
selected_emoji = st.session_state.get('selected_emoji', PLANTS[0])
|
258 |
+
for i, emoji in enumerate(PLANTS + BUILDINGS + CREATURES):
|
259 |
+
if cols[i % 5].button(emoji, key=f"emoji_{i}"):
|
260 |
+
st.session_state.selected_emoji = emoji
|
261 |
+
|
262 |
+
st.sidebar.subheader("π Scores")
|
263 |
+
for p, s in st.session_state.game_state['players'].items():
|
264 |
+
st.sidebar.write(f"{p}: {s}")
|
265 |
+
|
266 |
+
# Game HTML
|
267 |
+
game_html = f"""
|
268 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.2/p5.min.js"></script>
|
269 |
+
<div id="hex_grid" style="display:none">{json.dumps(st.session_state.game_state['hex_grid'])}</div>
|
270 |
+
<div id="buildings" style="display:none">{json.dumps(st.session_state.game_state['buildings'])}</div>
|
271 |
+
<div id="train" style="display:none">{json.dumps(st.session_state.game_state['train'])}</div>
|
272 |
+
<div id="monster_mode" style="display:none">{json.dumps(st.session_state.game_state['monster_mode'])}</div>
|
273 |
+
<div id="current_monster" style="display:none">{st.session_state.game_state['current_monster']}</div>
|
274 |
+
<div id="monster_x" style="display:none">{st.session_state.game_state['monster_x']}</div>
|
275 |
+
<div id="monster_y" style="display:none">{st.session_state.game_state['monster_y']}</div>
|
276 |
+
<div id="player_id" style="display:none">{player_id}</div>
|
277 |
+
<div id="selected_emoji" style="display:none">{selected_emoji}</div>
|
278 |
+
<div id="story" style="display:none">{json.dumps(st.session_state.game_state['story'])}</div>
|
279 |
+
<script>{p5js_code.replace('SENTENCE_TEMPLATES[type].format', '"A " + SUIT_PROPERTIES[suit] + " event with " + word')}</script>
|
280 |
+
"""
|
281 |
+
|
282 |
+
# Main layout
|
283 |
+
st.title("HexCitySaga: Build, Battle, and Tell Your Tale")
|
284 |
+
st.write(f"Player: {player_id}. Click to place {selected_emoji}. Keys: M (monster), G/R (monster type), T (train)")
|
285 |
+
html(game_html, height=500)
|
286 |
+
|
287 |
+
# State update handler
|
288 |
+
def update_state(data):
|
289 |
+
st.session_state.game_state.update({
|
290 |
+
'hex_grid': apply_game_of_life(data['hex_grid']),
|
291 |
+
'buildings': data['buildings'],
|
292 |
+
'train': data['train'],
|
293 |
+
'monster_mode': data['monster_mode'],
|
294 |
+
'current_monster': data['current_monster'],
|
295 |
+
'monster_x': data['monster_x'],
|
296 |
+
'monster_y': data['monster_y'],
|
297 |
+
'story': data['story']
|
298 |
+
})
|
299 |
+
if data['player_id'] not in st.session_state.game_state['players']:
|
300 |
+
st.session_state.game_state['players'][data['player_id']] = 0
|
301 |
+
st.session_state.game_state['players'][data['player_id']] += 1
|
302 |
+
st.rerun()
|
303 |
+
|
304 |
+
# Initialize tracks
|
305 |
+
for i in range(len(st.session_state.game_state['hex_grid'])):
|
306 |
+
st.session_state.game_state['hex_grid'][i][5]['type'] = 'track'
|