Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
import streamlit as st
|
2 |
-
import streamlit.components.v1 as components
|
3 |
import os
|
4 |
import random
|
5 |
import time
|
@@ -10,241 +9,212 @@ from pathlib import Path
|
|
10 |
import base64
|
11 |
from io import BytesIO
|
12 |
|
13 |
-
#
|
14 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
st.session_state.game_state = {
|
16 |
'players': {},
|
17 |
'chat_messages': [],
|
18 |
'tile_map': [],
|
19 |
-
'
|
20 |
}
|
21 |
-
|
22 |
-
if 'player_name' not in st.session_state:
|
23 |
st.session_state.player_name = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
st.session_state.start_time = None
|
30 |
-
|
31 |
-
if 'play_duration' not in st.session_state:
|
32 |
-
st.session_state.play_duration = 0
|
33 |
-
|
34 |
-
# Utility functions
|
35 |
-
def load_tiles_and_text():
|
36 |
-
"""Load tile images and their corresponding markdown text from the root directory"""
|
37 |
-
tiles = {}
|
38 |
-
for file in os.listdir('.'):
|
39 |
-
if file.endswith(('.png', '.jpg', '.jpeg')):
|
40 |
-
tile_name = os.path.splitext(file)[0]
|
41 |
-
# Load image
|
42 |
-
img = Image.open(file)
|
43 |
-
# Convert image to base64 for HTML embedding
|
44 |
-
buffered = BytesIO()
|
45 |
-
img.save(buffered, format="PNG")
|
46 |
-
img_str = base64.b64encode(buffered.getvalue()).decode()
|
47 |
-
|
48 |
-
# Check for corresponding markdown file
|
49 |
-
md_file = f"{tile_name}.md"
|
50 |
-
overlay_text = ""
|
51 |
-
if os.path.exists(md_file):
|
52 |
-
with open(md_file, 'r') as f:
|
53 |
-
overlay_text = f.read().strip()
|
54 |
-
|
55 |
-
tiles[tile_name] = {
|
56 |
-
'image': img_str,
|
57 |
-
'text': overlay_text
|
58 |
-
}
|
59 |
-
return tiles
|
60 |
-
|
61 |
-
def generate_map(width=20, height=15):
|
62 |
-
"""Generate a random tile map"""
|
63 |
-
tile_types = ['grass', 'water', 'rock']
|
64 |
-
return [[random.choice(tile_types) for _ in range(width)] for _ in range(height)]
|
65 |
-
|
66 |
-
def create_tile_html(tile_data, size=60):
|
67 |
-
"""Create HTML for a single tile with text overlay"""
|
68 |
-
return f"""
|
69 |
-
<div style="position: relative; width: {size}px; height: {size}px; display: inline-block;">
|
70 |
-
<img src="data:image/png;base64,{tile_data['image']}"
|
71 |
-
style="width: {size}px; height: {size}px; object-fit: cover;">
|
72 |
-
<div style="position: absolute; top: 0; left: 0; right: 0; bottom: 0;
|
73 |
-
background-color: rgba(255, 255, 255, 0.7);
|
74 |
-
display: flex; align-items: center; justify-content: center;
|
75 |
-
text-align: center; font-size: 10px; padding: 2px;
|
76 |
-
opacity: 0.8; pointer-events: none;">
|
77 |
-
{tile_data['text']}
|
78 |
-
</div>
|
79 |
-
</div>
|
80 |
-
"""
|
81 |
-
|
82 |
-
def create_game_board_html(tile_map, tiles, players):
|
83 |
-
"""Create HTML for the entire game board"""
|
84 |
-
board_html = """
|
85 |
-
<style>
|
86 |
-
.game-board { line-height: 0; }
|
87 |
-
.player-marker {
|
88 |
-
position: absolute;
|
89 |
-
width: 10px;
|
90 |
-
height: 10px;
|
91 |
-
background-color: red;
|
92 |
-
border-radius: 50%;
|
93 |
-
z-index: 2;
|
94 |
-
}
|
95 |
-
</style>
|
96 |
-
<div class="game-board">
|
97 |
-
"""
|
98 |
|
99 |
-
|
100 |
-
for x, tile_type in enumerate(row):
|
101 |
-
if tile_type in tiles:
|
102 |
-
board_html += create_tile_html(tiles[tile_type])
|
103 |
-
board_html += "<br>"
|
104 |
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
113 |
|
114 |
-
|
115 |
-
|
116 |
-
|
117 |
-
def
|
118 |
-
"""
|
119 |
-
|
120 |
-
|
121 |
-
'
|
122 |
-
|
123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
}
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
return f"{int(hours):02d}:{int(minutes):02d}:{int(seconds):02d}"
|
152 |
-
|
153 |
-
def check_auto_refresh():
|
154 |
-
"""Check if it's time to refresh the page"""
|
155 |
-
current_time = time.time()
|
156 |
-
if current_time - st.session_state.last_refresh >= 5:
|
157 |
-
st.session_state.last_refresh = current_time
|
158 |
-
st.rerun()
|
159 |
-
|
160 |
-
def update_play_duration():
|
161 |
-
"""Update the player's total play duration"""
|
162 |
-
if st.session_state.start_time is not None:
|
163 |
-
current_time = time.time()
|
164 |
-
st.session_state.play_duration = int(current_time - st.session_state.start_time)
|
165 |
-
|
166 |
-
# Main game UI
|
167 |
-
def main():
|
168 |
-
st.title("Multiplayer Tile Game")
|
169 |
-
|
170 |
-
if st.session_state.player_name is None:
|
171 |
-
with st.form("join_game"):
|
172 |
-
player_name = st.text_input("Enter your name:")
|
173 |
-
submitted = st.form_submit_button("Join Game")
|
174 |
-
if submitted and player_name:
|
175 |
-
st.session_state.player_name = player_name
|
176 |
-
st.session_state.start_time = time.time()
|
177 |
-
st.session_state.play_duration = 0
|
178 |
-
st.session_state.game_state['players'][player_name] = {
|
179 |
-
'position': {'x': 0, 'y': 0},
|
180 |
-
'last_active': time.time(),
|
181 |
-
'score': 0
|
182 |
-
}
|
183 |
-
st.rerun()
|
184 |
-
|
185 |
-
else:
|
186 |
-
update_play_duration()
|
187 |
-
st.sidebar.metric("Play Time", format_duration(st.session_state.play_duration))
|
188 |
|
189 |
-
|
|
|
190 |
|
191 |
-
|
192 |
-
|
193 |
-
|
194 |
-
st.session_state.game_state['tile_map'] = generate_map()
|
195 |
-
|
196 |
-
tiles = load_tiles_and_text()
|
197 |
-
if tiles:
|
198 |
-
# Generate and display the game board
|
199 |
-
board_html = create_game_board_html(
|
200 |
-
st.session_state.game_state['tile_map'],
|
201 |
-
tiles,
|
202 |
-
st.session_state.game_state['players']
|
203 |
-
)
|
204 |
-
components.html(board_html, height=600)
|
205 |
-
|
206 |
-
# Movement controls
|
207 |
-
if st.session_state.player_name:
|
208 |
-
player = st.session_state.game_state['players'][st.session_state.player_name]
|
209 |
-
|
210 |
-
cols = st.columns(4)
|
211 |
-
if cols[0].button("←"):
|
212 |
-
player['position']['x'] = max(0, player['position']['x'] - 1)
|
213 |
-
if cols[1].button("↑"):
|
214 |
-
player['position']['y'] = max(0, player['position']['y'] - 1)
|
215 |
-
if cols[2].button("↓"):
|
216 |
-
player['position']['y'] = min(14, player['position']['y'] + 1)
|
217 |
-
if cols[3].button("→"):
|
218 |
-
player['position']['x'] = min(19, player['position']['x'] + 1)
|
219 |
-
|
220 |
-
st.write(f"Position: ({player['position']['x']}, {player['position']['y']})")
|
221 |
-
else:
|
222 |
-
st.warning("No image files found. Please add .png files and optional .md files with matching names.")
|
223 |
|
224 |
-
|
225 |
-
st.subheader("Chat Room")
|
226 |
-
chat_container = st.container()
|
227 |
-
with chat_container:
|
228 |
-
for message in st.session_state.game_state['chat_messages'][-50:]:
|
229 |
-
st.text(f"[{message['timestamp']}] {message['player']}: {message['message']}")
|
230 |
-
|
231 |
-
with st.form("chat_form", clear_on_submit=True):
|
232 |
-
message = st.text_input("Message:")
|
233 |
-
if st.form_submit_button("Send") and message:
|
234 |
-
add_chat_message(st.session_state.player_name, message)
|
235 |
-
st.rerun()
|
236 |
|
237 |
-
|
238 |
-
|
239 |
-
|
240 |
-
|
241 |
-
|
242 |
-
|
243 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
244 |
st.rerun()
|
245 |
|
246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
247 |
|
248 |
if __name__ == "__main__":
|
249 |
-
load_game_state()
|
250 |
main()
|
|
|
1 |
import streamlit as st
|
|
|
2 |
import os
|
3 |
import random
|
4 |
import time
|
|
|
9 |
import base64
|
10 |
from io import BytesIO
|
11 |
|
12 |
+
# Fantasy name generator
|
13 |
+
def generate_fantasy_name():
|
14 |
+
prefixes = ['Aer', 'Bal', 'Cal', 'Dor', 'El', 'Fae', 'Gor', 'Hel', 'Il', 'Jor',
|
15 |
+
'Kal', 'Lyr', 'Mel', 'Nym', 'Oro', 'Pyr', 'Qar', 'Ryn', 'Syl', 'Tyr']
|
16 |
+
suffixes = ['ian', 'or', 'ion', 'us', 'ix', 'ar', 'en', 'yr', 'el', 'an',
|
17 |
+
'is', 'ax', 'on', 'ir', 'ex', 'az', 'er', 'eth', 'ys', 'ix']
|
18 |
+
return random.choice(prefixes) + random.choice(suffixes)
|
19 |
+
|
20 |
+
# Initialize session state
|
21 |
+
if 'initialized' not in st.session_state:
|
22 |
+
st.session_state.initialized = False
|
23 |
st.session_state.game_state = {
|
24 |
'players': {},
|
25 |
'chat_messages': [],
|
26 |
'tile_map': [],
|
27 |
+
'last_sync': time.time()
|
28 |
}
|
|
|
|
|
29 |
st.session_state.player_name = None
|
30 |
+
st.session_state.character_stats = None
|
31 |
+
|
32 |
+
# Character Stats Generation
|
33 |
+
def roll_stats():
|
34 |
+
return {
|
35 |
+
'STR': sum(sorted([random.randint(1, 6) for _ in range(4)])[1:]),
|
36 |
+
'DEX': sum(sorted([random.randint(1, 6) for _ in range(4)])[1:]),
|
37 |
+
'CON': sum(sorted([random.randint(1, 6) for _ in range(4)])[1:]),
|
38 |
+
'INT': sum(sorted([random.randint(1, 6) for _ in range(4)])[1:]),
|
39 |
+
'WIS': sum(sorted([random.randint(1, 6) for _ in range(4)])[1:]),
|
40 |
+
'CHA': sum(sorted([random.randint(1, 6) for _ in range(4)])[1:]),
|
41 |
+
'HP': random.randint(1, 20) * 2 + random.randint(1, 20),
|
42 |
+
'MAX_HP': 40,
|
43 |
+
'score': 0,
|
44 |
+
'created_at': time.time()
|
45 |
+
}
|
46 |
|
47 |
+
def save_character_sheet(player_name, stats):
|
48 |
+
"""Save character sheet as markdown"""
|
49 |
+
filepath = f"characters/{player_name}.md"
|
50 |
+
os.makedirs('characters', exist_ok=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
51 |
|
52 |
+
markdown = f"""# {player_name}'s Character Sheet
|
|
|
|
|
|
|
|
|
53 |
|
54 |
+
## Stats
|
55 |
+
- **STR**: {stats['STR']}
|
56 |
+
- **DEX**: {stats['DEX']}
|
57 |
+
- **CON**: {stats['CON']}
|
58 |
+
- **INT**: {stats['INT']}
|
59 |
+
- **WIS**: {stats['WIS']}
|
60 |
+
- **CHA**: {stats['CHA']}
|
61 |
+
|
62 |
+
## Health
|
63 |
+
HP: {stats['HP']}/{stats['MAX_HP']}
|
64 |
+
|
65 |
+
## Score
|
66 |
+
Current Score: {stats['score']}
|
67 |
+
|
68 |
+
## Session Info
|
69 |
+
Created: {datetime.fromtimestamp(stats['created_at']).strftime('%Y-%m-%d %H:%M:%S')}
|
70 |
+
Last Updated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
71 |
+
"""
|
72 |
|
73 |
+
with open(filepath, 'w') as f:
|
74 |
+
f.write(markdown)
|
75 |
+
|
76 |
+
def load_character_sheet(player_name):
|
77 |
+
"""Load character sheet if it exists"""
|
78 |
+
filepath = f"characters/{player_name}.md"
|
79 |
+
if os.path.exists(filepath):
|
80 |
+
with open(filepath, 'r') as f:
|
81 |
+
return f.read()
|
82 |
+
return None
|
83 |
+
|
84 |
+
def create_game_js():
|
85 |
+
"""Create JavaScript for real-time game updates"""
|
86 |
+
return """
|
87 |
+
<script>
|
88 |
+
const gameState = {
|
89 |
+
playerName: null,
|
90 |
+
position: { x: 0, y: 0 },
|
91 |
+
velocity: { x: 0, y: 0 },
|
92 |
+
lastUpdate: Date.now(),
|
93 |
+
needsSync: false
|
94 |
+
};
|
95 |
+
|
96 |
+
function updateClock() {
|
97 |
+
const now = new Date();
|
98 |
+
document.getElementById('game-clock').textContent =
|
99 |
+
now.toLocaleTimeString();
|
100 |
}
|
101 |
+
|
102 |
+
function handleMovement(e) {
|
103 |
+
const speed = 5;
|
104 |
+
switch(e.key.toLowerCase()) {
|
105 |
+
case 'w': case 'arrowup':
|
106 |
+
gameState.velocity.y = -speed;
|
107 |
+
break;
|
108 |
+
case 's': case 'arrowdown':
|
109 |
+
gameState.velocity.y = speed;
|
110 |
+
break;
|
111 |
+
case 'a': case 'arrowleft':
|
112 |
+
gameState.velocity.x = -speed;
|
113 |
+
break;
|
114 |
+
case 'd': case 'arrowright':
|
115 |
+
gameState.velocity.x = speed;
|
116 |
+
break;
|
117 |
+
case 'x':
|
118 |
+
gameState.velocity = { x: 0, y: 0 };
|
119 |
+
break;
|
120 |
+
}
|
121 |
+
gameState.needsSync = true;
|
122 |
+
}
|
123 |
+
|
124 |
+
function updatePosition() {
|
125 |
+
const now = Date.now();
|
126 |
+
const delta = (now - gameState.lastUpdate) / 1000;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
127 |
|
128 |
+
gameState.position.x += gameState.velocity.x * delta;
|
129 |
+
gameState.position.y += gameState.velocity.y * delta;
|
130 |
|
131 |
+
// Boundary checks
|
132 |
+
gameState.position.x = Math.max(0, Math.min(gameState.position.x, 19));
|
133 |
+
gameState.position.y = Math.max(0, Math.min(gameState.position.y, 14));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
134 |
|
135 |
+
gameState.lastUpdate = now;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
136 |
|
137 |
+
// Update player marker position
|
138 |
+
const marker = document.querySelector('.player-marker');
|
139 |
+
if (marker) {
|
140 |
+
marker.style.left = `${gameState.position.x * 60 + 25}px`;
|
141 |
+
marker.style.top = `${gameState.position.y * 60 + 25}px`;
|
142 |
+
}
|
143 |
+
}
|
144 |
+
|
145 |
+
function syncWithServer() {
|
146 |
+
if (gameState.needsSync) {
|
147 |
+
// Send position to Streamlit
|
148 |
+
window.parent.postMessage({
|
149 |
+
type: 'streamlit:sync',
|
150 |
+
position: gameState.position
|
151 |
+
}, '*');
|
152 |
+
gameState.needsSync = false;
|
153 |
+
}
|
154 |
+
}
|
155 |
+
|
156 |
+
// Initialize game loop
|
157 |
+
setInterval(updatePosition, 16); // ~60 FPS
|
158 |
+
setInterval(syncWithServer, 5000); // Sync every 5 seconds
|
159 |
+
setInterval(updateClock, 1000); // Update clock every second
|
160 |
+
|
161 |
+
// Set up event listeners
|
162 |
+
document.addEventListener('keydown', handleMovement);
|
163 |
+
</script>
|
164 |
+
"""
|
165 |
+
|
166 |
+
def main():
|
167 |
+
# Sidebar for player info and controls
|
168 |
+
st.sidebar.title("Player Info")
|
169 |
+
|
170 |
+
# Player name handling
|
171 |
+
if st.session_state.player_name is None:
|
172 |
+
default_name = generate_fantasy_name()
|
173 |
+
player_name = st.sidebar.text_input("Enter your name or use generated name:",
|
174 |
+
value=default_name)
|
175 |
+
if st.sidebar.button("Start Playing"):
|
176 |
+
st.session_state.player_name = player_name
|
177 |
+
if st.session_state.character_stats is None:
|
178 |
+
st.session_state.character_stats = roll_stats()
|
179 |
+
save_character_sheet(player_name, st.session_state.character_stats)
|
180 |
+
st.rerun()
|
181 |
+
else:
|
182 |
+
# Show current name and allow changes
|
183 |
+
new_name = st.sidebar.text_input("Your name:",
|
184 |
+
value=st.session_state.player_name)
|
185 |
+
if new_name != st.session_state.player_name:
|
186 |
+
old_name = st.session_state.player_name
|
187 |
+
st.session_state.player_name = new_name
|
188 |
+
# Rename character sheet
|
189 |
+
os.rename(f"characters/{old_name}.md",
|
190 |
+
f"characters/{new_name}.md")
|
191 |
st.rerun()
|
192 |
|
193 |
+
# Display character sheet
|
194 |
+
character_sheet = load_character_sheet(st.session_state.player_name)
|
195 |
+
if character_sheet:
|
196 |
+
st.sidebar.markdown(character_sheet)
|
197 |
+
|
198 |
+
# Movement controls with emojis
|
199 |
+
st.sidebar.markdown("### Movement Controls")
|
200 |
+
move_cols = st.sidebar.columns(3)
|
201 |
+
move_cols[1].button("⬆️", key="up")
|
202 |
+
cols = st.sidebar.columns(3)
|
203 |
+
cols[0].button("⬅️", key="left")
|
204 |
+
cols[1].button("⬇️", key="down")
|
205 |
+
cols[2].button("➡️", key="right")
|
206 |
+
st.sidebar.button("🛑", key="stop")
|
207 |
+
|
208 |
+
# Main game area
|
209 |
+
st.title("Multiplayer Tile Game")
|
210 |
+
|
211 |
+
# Game clock
|
212 |
+
st.markdown('<div id="game-clock" style="font-size: 24px;"></div>',
|
213 |
+
unsafe_allow_html=True)
|
214 |
+
|
215 |
+
# Game board and JavaScript
|
216 |
+
if st.session_state.player_name:
|
217 |
+
st.components.v1.html(create_game_js(), height=600)
|
218 |
|
219 |
if __name__ == "__main__":
|
|
|
220 |
main()
|