dwishank commited on
Commit
e60ea2c
·
verified ·
1 Parent(s): 16fed98

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +310 -34
src/streamlit_app.py CHANGED
@@ -1,40 +1,316 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
5
 
6
- """
7
- # Welcome to Streamlit!
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
 
13
- In the meantime, below is an example of what you can do with just a few lines of code:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  """
15
 
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ import streamlit.components.v1 as components
3
 
4
+ st.set_page_config(page_title="Void Dash", page_icon="⚡", layout="wide")
5
+
6
+ st.markdown("""
7
+ <style>
8
+ body, .stApp { background:#05070f !important; }
9
+ #MainMenu, footer, header { visibility:hidden; }
10
+ .block-container { padding:0.5rem 1rem; }
11
+ h1 { color:#a99ff7; font-family:monospace; text-align:center; margin:0; }
12
+ p { color:#7a7f94; text-align:center; font-family:monospace; font-size:13px; }
13
+ </style>
14
+ """, unsafe_allow_html=True)
15
+
16
+ st.markdown("<h1>⚡ VOID DASH</h1>", unsafe_allow_html=True)
17
+ st.markdown("<p>Tap / Space = jump &nbsp;|&nbsp; Hold = glide &nbsp;|&nbsp; Grab cyan boost &nbsp;|&nbsp; Dodge alien bullets!</p>", unsafe_allow_html=True)
18
+
19
+ GAME_HTML = """
20
+ <!DOCTYPE html>
21
+ <html>
22
+ <head>
23
+ <style>
24
+ * { margin:0; padding:0; box-sizing:border-box; }
25
+ body { background:#05070f; display:flex; flex-direction:column; align-items:center; }
26
+ canvas { display:block; border-radius:12px; cursor:pointer; }
27
+ #hud {
28
+ display:flex; justify-content:space-between; align-items:center;
29
+ width:600px; padding:6px 10px; font-family:monospace; font-size:13px;
30
+ color:#ccc; background:#0a0c14; border-radius:0 0 10px 10px;
31
+ }
32
+ #lives-bar span { display:inline-block; width:10px; height:10px; border-radius:50%; background:#f76464; margin:0 2px; }
33
+ #shield-bar { width:70px; height:6px; background:#222; border-radius:3px; overflow:hidden; display:inline-block; vertical-align:middle; margin-left:4px; }
34
+ #shield-fill { height:100%; background:#3ecfcf; width:0%; border-radius:3px; }
35
+ #wrap { position:relative; }
36
+ #msg {
37
+ position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
38
+ color:#fff; text-align:center; pointer-events:none; min-width:220px;
39
+ font-family:monospace;
40
+ }
41
+ #msg h2 { font-size:22px; font-weight:500; margin:0 0 6px; }
42
+ #msg p { font-size:12px; color:#888; margin:2px 0; }
43
+ .tag { display:inline-block; font-size:11px; padding:2px 8px; border-radius:10px; margin:2px; }
44
+ </style>
45
+ </head>
46
+ <body>
47
+ <div id="wrap">
48
+ <canvas id="gc" width="600" height="360"></canvas>
49
+ <div id="msg">
50
+ <h2 style="color:#a99ff7">VOID DASH</h2>
51
+ <p>Tap / Space = jump &nbsp;|&nbsp; Hold = glide</p>
52
+ <p style="margin-top:8px;">
53
+ <span class="tag" style="background:#1a3a1a;color:#3ecf8e">BOOST = speed + shield</span>
54
+ <span class="tag" style="background:#3a1a1a;color:#f76464">ALIENS fire bullets!</span>
55
+ </p>
56
+ <p style="margin-top:6px;color:#7c6af7;font-size:13px;">Tap to start</p>
57
+ </div>
58
+ </div>
59
+ <div id="hud">
60
+ <span>Score: <b id="sc">0</b> &nbsp;|&nbsp; Best: <b id="hi">0</b></span>
61
+ <span>
62
+ Lives: <span id="lives-bar"></span>
63
+ &nbsp; Shield: <span id="shield-bar"><span id="shield-fill"></span></span>
64
+ </span>
65
+ <span id="boost-txt" style="color:#3ecf8e;font-size:12px;min-width:60px;text-align:right;"></span>
66
+ </div>
67
+
68
+ <script>
69
+ const C=document.getElementById('gc'), ctx=C.getContext('2d');
70
+ const W=C.width, H=C.height;
71
+ const GRAV=0.38,JUMP=-7.2,GLIDE=0.14,PIPE_W=44,GAP=128,PIPE_SPD=2.8,PIPE_INT=185;
72
+ const MAX_LIVES=3;
73
+ let state='idle',frame=0,score=0,best=0,held=false;
74
+ let bird,pipes,enemies,bullets,particles,powerups;
75
+ let lives,shield,boostTimer,invTimer;
76
+
77
+ function initLivesUI(){
78
+ const b=document.getElementById('lives-bar'); b.innerHTML='';
79
+ for(let i=0;i<MAX_LIVES;i++) b.appendChild(Object.assign(document.createElement('span'),{id:'life'+i}));
80
+ }
81
+ function updateLivesUI(){ for(let i=0;i<MAX_LIVES;i++){ const s=document.getElementById('life'+i); if(s) s.style.background=i<lives?'#f76464':'#333'; } }
82
+ function updateShieldUI(){ document.getElementById('shield-fill').style.width=Math.round(shield)+'%'; }
83
+ function updateBoostUI(){ document.getElementById('boost-txt').textContent=boostTimer>0?'BOOST '+Math.ceil(boostTimer/60)+'s':''; }
84
+
85
+ function init(){
86
+ bird={x:110,y:H/2,vy:0,r:10,trail:[]};
87
+ pipes=[];enemies=[];bullets=[];particles=[];powerups=[];
88
+ frame=0;score=0;lives=MAX_LIVES;shield=0;boostTimer=0;invTimer=0;
89
+ document.getElementById('sc').textContent=0;
90
+ updateLivesUI();updateShieldUI();updateBoostUI();
91
+ }
92
+
93
+ function jump(){
94
+ if(state==='idle'||state==='dead'){state='play';document.getElementById('msg').style.display='none';init();}
95
+ if(state==='play') bird.vy=JUMP;
96
+ held=true;
97
+ }
98
+ function release(){ held=false; }
99
+ C.addEventListener('mousedown',jump); C.addEventListener('mouseup',release);
100
+ C.addEventListener('touchstart',e=>{e.preventDefault();jump();},{passive:false});
101
+ C.addEventListener('touchend',e=>{e.preventDefault();release();},{passive:false});
102
+ window.addEventListener('keydown',e=>{if(e.code==='Space'){e.preventDefault();jump();}});
103
+ window.addEventListener('keyup',e=>{if(e.code==='Space')release();});
104
+
105
+ function rnd(a,b){return a+Math.random()*(b-a);}
106
+ function hsl(h,s,l){return `hsl(${h},${s}%,${l}%)`;}
107
+
108
+ function spawnPipe(){ const top=55+Math.random()*(H-GAP-95); pipes.push({x:W+10,top,scored:false}); }
109
+ function spawnEnemy(){ enemies.push({x:W+20,y:rnd(30,H-30),vy:rnd(-0.6,0.6),r:14,shootTimer:rnd(40,100),hp:2,frame:0}); }
110
+ function spawnPowerup(){ powerups.push({x:W+10,y:rnd(50,H-50),type:Math.random()<0.6?'boost':'heart',r:12,bob:Math.random()*Math.PI*2}); }
111
+ function spawnParticles(x,y,color,n=8){ for(let i=0;i<n;i++){ const a=Math.random()*Math.PI*2,spd=rnd(1.5,4.5); particles.push({x,y,vx:Math.cos(a)*spd,vy:Math.sin(a)*spd,life:1,color}); } }
112
+
113
+ function drawBird(b,t){
114
+ for(let i=0;i<b.trail.length;i++){
115
+ const pt=b.trail[i],a=(i/b.trail.length)*0.3;
116
+ ctx.beginPath();ctx.arc(pt.x,pt.y,b.r*(i/b.trail.length)*0.7,0,Math.PI*2);
117
+ ctx.fillStyle=`hsla(${(t*1.5+i*5)%360},90%,65%,${a})`;ctx.fill();
118
+ }
119
+ if(boostTimer>0){ctx.beginPath();ctx.arc(b.x,b.y,b.r*2.2,0,Math.PI*2);ctx.fillStyle=`rgba(62,207,207,${0.15+0.1*Math.sin(t*0.3)})`;ctx.fill();}
120
+ if(invTimer>0&&Math.floor(invTimer/4)%2===0) return;
121
+ ctx.beginPath();ctx.arc(b.x,b.y,b.r,0,Math.PI*2);ctx.fillStyle=boostTimer>0?'#3ecfcf':'#fff';ctx.fill();
122
+ ctx.beginPath();ctx.arc(b.x-3,b.y-3,b.r*0.38,0,Math.PI*2);ctx.fillStyle='rgba(0,0,0,0.25)';ctx.fill();
123
+ }
124
+
125
+ function drawEnemy(e,t){
126
+ e.frame++;
127
+ const bob=Math.sin(e.frame*0.05)*3,bx=e.x,by=e.y+bob;
128
+ ctx.save();ctx.translate(bx,by);
129
+ ctx.beginPath();ctx.ellipse(0,0,e.r*1.3,e.r,0,0,Math.PI*2);
130
+ ctx.fillStyle=hsl((t*0.8)%360,80,35);ctx.fill();
131
+ ctx.strokeStyle=hsl((t*0.8+30)%360,90,60);ctx.lineWidth=1.5;ctx.stroke();
132
+ ctx.fillStyle=hsl((t*1.2)%360,100,70);
133
+ for(let i=-1;i<=1;i+=2){ctx.beginPath();ctx.arc(i*5,-3,3,0,Math.PI*2);ctx.fill();}
134
+ ctx.strokeStyle='#f76464';ctx.lineWidth=1;
135
+ for(let i=0;i<3;i++){ctx.beginPath();ctx.moveTo(-e.r*1.3+i*e.r*0.7,-e.r);ctx.lineTo(-e.r*1.3+i*e.r*0.7+3,-e.r-6);ctx.stroke();}
136
+ ctx.restore();
137
+ for(let h=0;h<2;h++){ctx.fillStyle=h<e.hp?'#f76464':'#333';ctx.fillRect(e.x-10+h*12,e.y+bob-e.r-10,10,4);}
138
+ }
139
+
140
+ function drawPowerup(p,t){
141
+ p.bob+=0.06;const y=p.y+Math.sin(p.bob)*5;
142
+ ctx.save();ctx.translate(p.x,y);
143
+ if(p.type==='boost'){
144
+ ctx.beginPath();ctx.moveTo(0,-p.r);ctx.lineTo(p.r*0.8,p.r*0.5);ctx.lineTo(-p.r*0.8,p.r*0.5);ctx.closePath();
145
+ ctx.fillStyle='rgba(62,207,207,0.9)';ctx.fill();ctx.strokeStyle='#fff';ctx.lineWidth=1;ctx.stroke();
146
+ ctx.fillStyle='#fff';ctx.font='bold 9px monospace';ctx.textAlign='center';ctx.fillText('B',0,4);
147
+ } else {
148
+ ctx.beginPath();ctx.arc(0,0,p.r,0,Math.PI*2);ctx.fillStyle='rgba(247,100,100,0.85)';ctx.fill();
149
+ ctx.strokeStyle='#fff';ctx.lineWidth=1;ctx.stroke();
150
+ ctx.fillStyle='#fff';ctx.font='bold 11px monospace';ctx.textAlign='center';ctx.fillText('+',0,4);
151
+ }
152
+ ctx.restore();
153
+ }
154
+
155
+ function drawBullet(bl){
156
+ ctx.beginPath();ctx.ellipse(bl.x,bl.y,6,3,Math.atan2(bl.vy,bl.vx),0,Math.PI*2);
157
+ ctx.fillStyle='#f7c948';ctx.fill();
158
+ ctx.beginPath();ctx.arc(bl.x,bl.y,2,0,Math.PI*2);ctx.fillStyle='#fff';ctx.fill();
159
+ }
160
+
161
+ function drawPipe(p,t){
162
+ const g1=ctx.createLinearGradient(p.x,0,p.x+PIPE_W,0);
163
+ g1.addColorStop(0,hsl((t*0.6)%360,60,25));g1.addColorStop(1,hsl((t*0.6+40)%360,60,40));
164
+ ctx.fillStyle=g1;
165
+ ctx.beginPath();ctx.roundRect(p.x,0,PIPE_W,p.top,[0,0,8,8]);ctx.fill();
166
+ ctx.beginPath();ctx.roundRect(p.x,p.top+GAP,PIPE_W,H-p.top-GAP,[8,8,0,0]);ctx.fill();
167
+ ctx.strokeStyle=hsl((t*1.1)%360,90,60);ctx.lineWidth=1.2;ctx.stroke();
168
+ }
169
+
170
+ function drawGrid(t){
171
+ ctx.strokeStyle='rgba(80,60,200,0.06)';ctx.lineWidth=0.5;
172
+ const spd=boostTimer>0?PIPE_SPD*2:PIPE_SPD,off=(t*spd*0.35)%40;
173
+ for(let x=-off;x<W;x+=40){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
174
+ for(let y=0;y<H;y+=40){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
175
+ }
176
+ function drawStars(t){
177
+ for(let i=0;i<55;i++){
178
+ const sx=((i*137+t*0.25)%W),sy=((i*97)%H),a=0.15+0.25*Math.sin(t*0.04+i);
179
+ ctx.fillStyle=`rgba(255,255,255,${a})`;ctx.fillRect(sx,sy,1.5,1.5);
180
+ }
181
+ }
182
+ function drawParticles(){
183
+ for(let i=particles.length-1;i>=0;i--){
184
+ const p=particles[i];
185
+ ctx.globalAlpha=p.life;ctx.beginPath();ctx.arc(p.x,p.y,3*p.life,0,Math.PI*2);
186
+ ctx.fillStyle=p.color;ctx.fill();ctx.globalAlpha=1;
187
+ p.x+=p.vx;p.y+=p.vy;p.vx*=0.92;p.vy*=0.92;p.life-=0.04;
188
+ if(p.life<=0) particles.splice(i,1);
189
+ }
190
+ }
191
 
192
+ function circle(ax,ay,ar,bx,by,br){return Math.hypot(ax-bx,ay-by)<ar+br;}
193
+ function collidesPipe(b,p){return b.x+b.r>p.x&&b.x-b.r<p.x+PIPE_W&&(b.y-b.r<p.top||b.y+b.r>p.top+GAP);}
 
194
 
195
+ function takeDamage(x,y){
196
+ if(invTimer>0) return;
197
+ if(shield>=30){shield=Math.max(0,shield-30);updateShieldUI();spawnParticles(bird.x,bird.y,'#3ecfcf',6);return;}
198
+ lives--;updateLivesUI();invTimer=90;spawnParticles(x,y,'#f76464',10);
199
+ if(lives<=0) die();
200
+ }
201
+
202
+ function die(){
203
+ state='dead';
204
+ const msg=document.getElementById('msg');msg.style.display='';
205
+ msg.innerHTML=`<h2 style="color:#f76464">DESTROYED</h2><p>Score: ${score} &nbsp;|&nbsp; Best: ${best}</p><p style="margin-top:6px;color:#7c6af7;font-size:13px;">Tap / Space to retry</p>`;
206
+ }
207
+
208
+ function loop(){
209
+ requestAnimationFrame(loop);
210
+ const t=frame;
211
+ ctx.clearRect(0,0,W,H);ctx.fillStyle='#05070f';ctx.fillRect(0,0,W,H);
212
+ drawGrid(t);drawStars(t);
213
+
214
+ if(state==='idle'){
215
+ const iy=H/2+Math.sin(t*0.06)*12;
216
+ const g=ctx.createRadialGradient(110,iy,1,110,iy,18);
217
+ g.addColorStop(0,hsl(t*2%360,90,65));g.addColorStop(1,'transparent');
218
+ ctx.beginPath();ctx.arc(110,iy,18,0,Math.PI*2);ctx.fillStyle=g;ctx.fill();
219
+ ctx.beginPath();ctx.arc(110,iy,10,0,Math.PI*2);ctx.fillStyle='#fff';ctx.fill();
220
+ frame++;return;
221
+ }
222
+
223
+ if(state==='play'){
224
+ frame++;
225
+ const spd=boostTimer>0?PIPE_SPD*1.8:PIPE_SPD;
226
+ if(boostTimer>0){boostTimer--;if(frame%60===0||boostTimer===0) updateBoostUI();}
227
+ if(invTimer>0) invTimer--;
228
+
229
+ bird.vy+=held?GRAV-GLIDE:GRAV;
230
+ bird.vy=Math.max(-10,Math.min(bird.vy,12));
231
+ bird.y+=bird.vy;
232
+ bird.trail.push({x:bird.x,y:bird.y});
233
+ if(bird.trail.length>16) bird.trail.shift();
234
+
235
+ if(frame%PIPE_INT===0) spawnPipe();
236
+ if(frame%220===0) spawnEnemy();
237
+ if(frame%300===0) spawnPowerup();
238
+
239
+ for(let i=pipes.length-1;i>=0;i--){
240
+ pipes[i].x-=spd;
241
+ if(!pipes[i].scored&&pipes[i].x+PIPE_W<bird.x){
242
+ pipes[i].scored=true;score++;
243
+ document.getElementById('sc').textContent=score;
244
+ if(score>best){best=score;document.getElementById('hi').textContent=best;}
245
+ }
246
+ if(pipes[i].x+PIPE_W<-10){pipes.splice(i,1);continue;}
247
+ drawPipe(pipes[i],t);
248
+ if(collidesPipe(bird,pipes[i])) takeDamage(bird.x,bird.y);
249
+ }
250
+
251
+ for(let i=enemies.length-1;i>=0;i--){
252
+ const e=enemies[i];
253
+ e.x-=spd*0.55;e.y+=e.vy;
254
+ if(e.y<20||e.y>H-20) e.vy*=-1;
255
+ e.shootTimer--;
256
+ if(e.shootTimer<=0){
257
+ const dx=bird.x-e.x,dy=bird.y-e.y,dist=Math.hypot(dx,dy),s2=4.5;
258
+ bullets.push({x:e.x-e.r,y:e.y,vx:(dx/dist)*s2,vy:(dy/dist)*s2});
259
+ e.shootTimer=rnd(60,110);spawnParticles(e.x,e.y,'#f7c948',3);
260
+ }
261
+ if(e.x<-30){enemies.splice(i,1);continue;}
262
+ drawEnemy(e,t);
263
+ if(circle(bird.x,bird.y,bird.r,e.x,e.y,e.r*1.2)) takeDamage(e.x,e.y);
264
+ }
265
+
266
+ for(let i=bullets.length-1;i>=0;i--){
267
+ const bl=bullets[i];bl.x+=bl.vx;bl.y+=bl.vy;
268
+ if(bl.x<-10||bl.x>W+10||bl.y<-10||bl.y>H+10){bullets.splice(i,1);continue;}
269
+ drawBullet(bl);
270
+ if(circle(bird.x,bird.y,bird.r,bl.x,bl.y,5)){bullets.splice(i,1);takeDamage(bl.x,bl.y);}
271
+ }
272
+
273
+ for(let i=powerups.length-1;i>=0;i--){
274
+ const p=powerups[i];p.x-=spd*0.5;
275
+ if(p.x<-20){powerups.splice(i,1);continue;}
276
+ drawPowerup(p,t);
277
+ if(circle(bird.x,bird.y,bird.r,p.x,p.y+Math.sin(p.bob)*5,p.r)){
278
+ if(p.type==='boost'){boostTimer=300;shield=Math.min(100,shield+40);spawnParticles(p.x,p.y,'#3ecfcf',12);updateShieldUI();updateBoostUI();}
279
+ else{lives=Math.min(MAX_LIVES,lives+1);spawnParticles(p.x,p.y,'#f76464',10);updateLivesUI();}
280
+ powerups.splice(i,1);
281
+ }
282
+ }
283
+
284
+ drawParticles();drawBird(bird,t);
285
+
286
+ if(bird.y-bird.r<0||bird.y+bird.r>H){takeDamage(bird.x,bird.y);bird.vy*=-0.5;bird.y=Math.max(bird.r,Math.min(H-bird.r,bird.y));}
287
+
288
+ ctx.fillStyle=`hsla(${t%360},80%,80%,0.85)`;
289
+ ctx.font='500 26px monospace';ctx.textAlign='center';
290
+ ctx.fillText(score,W/2,40);ctx.textAlign='left';
291
+
292
+ if(boostTimer>0){ctx.fillStyle='rgba(62,207,207,0.7)';ctx.font='bold 11px monospace';ctx.textAlign='right';ctx.fillText('BOOST',W-12,20);ctx.textAlign='left';}
293
+ }
294
+ }
295
+
296
+ initLivesUI();init();loop();
297
+ </script>
298
+ </body>
299
+ </html>
300
  """
301
 
302
+ components.html(GAME_HTML, height=430, scrolling=False)
303
+
304
+ st.markdown("""
305
+ <div style="text-align:center;margin-top:8px;">
306
+ <span style="background:#1a3a1a;color:#3ecf8e;padding:3px 10px;border-radius:10px;font-size:12px;font-family:monospace;margin:4px;">
307
+ Cyan triangle = BOOST (speed + shield)
308
+ </span>
309
+ <span style="background:#3a1a1a;color:#f76464;padding:3px 10px;border-radius:10px;font-size:12px;font-family:monospace;margin:4px;">
310
+ Red circle = +1 Life
311
+ </span>
312
+ <span style="background:#1a1a3a;color:#a99ff7;padding:3px 10px;border-radius:10px;font-size:12px;font-family:monospace;margin:4px;">
313
+ Aliens fire aimed bullets!
314
+ </span>
315
+ </div>
316
+ """, unsafe_allow_html=True)