awacke1 commited on
Commit
9fd7787
Β·
verified Β·
1 Parent(s): f2da46a

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +306 -0
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'