16 Commits

Author SHA1 Message Date
ErrorNoInternet d82127c5fb treewide: add basic optimizations 2026-06-01 21:49:28 -04:00
ErrorNoInternet 36b0c0343d treewide: set up formatting 2026-06-01 21:11:47 -04:00
deadvey 0e4d5d7686 String -> str 2026-06-01 23:11:49 +01:00
deadvey 19cabccd86 LICENSE AND README 2026-06-01 22:30:18 +01:00
deadvey 7c8974f57c removed static files from root 2026-05-31 19:46:32 +01:00
deadvey 9a581ed5fc added a static directory 2026-05-31 19:46:05 +01:00
deadvey 8352b9c7fd fix url 2026-05-31 16:17:52 +01:00
deadvey af1847f5e9 fixed incorrect username reporting 2026-05-31 16:13:31 +01:00
deadvey 9378ab8fed fixed javalsai's mess 2026-05-31 15:50:00 +01:00
deadvey 585d642204 idk 2026-05-31 15:40:00 +01:00
deadvey 10a32423ea removed unstable if let guard 2026-05-31 15:20:29 +01:00
deadvey 0dc92341e5 removed hiscores from tracking 2026-05-31 14:41:53 +01:00
deadvey 83eed86af2 prod address 2026-05-31 14:40:49 +01:00
deadvey 640c1c3acf favicon 2026-05-31 14:39:56 +01:00
deadvey 18e5127462 added seperate route for leaderboards 2026-05-31 14:36:17 +01:00
deadvey 1f9bdb3ac0 did allman indenting 2026-05-31 13:12:37 +01:00
13 changed files with 353 additions and 262 deletions
+1
View File
@@ -2,3 +2,4 @@
hiscores.json hiscores.json
loscores.json loscores.json
pingscores.json pingscores.json
.*
Binary file not shown.
+13
View File
@@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.
+2
View File
@@ -0,0 +1,2 @@
/src/main.rs - code
/static - static files like favicon and html pages
-1
View File
@@ -1 +0,0 @@
[{"score":60,"person":"error"},{"score":57,"person":"deadvey"},{"score":53,"person":"error"},{"score":51,"person":"error"},{"score":51,"person":"deadvey"},{"score":50,"person":"error"},{"score":49,"person":"error"},{"score":49,"person":"error"},{"score":49,"person":"deadvey"},{"score":48,"person":"error"},{"score":48,"person":"error"},{"score":48,"person":"deadvey"},{"score":48,"person":"deadvey"},{"score":48,"person":"deadvey"},{"score":48,"person":"deadvey"},{"score":48,"person":"deadvey"},{"score":48,"person":"deadvey"},{"score":48,"person":"deadvey"},{"score":47,"person":"error"},{"score":47,"person":"error"}]
-89
View File
@@ -1,89 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<h1>Each time you press the button there's a 1/3 chance of returning to 0</h1>
<button id="button" onclick="send_click()">0</button>
<div id="score-table"></div>
</body>
<style>
#button {
background-color: red;
border: darkred solid 15px;
border-radius: 50%;
font-size: 50px;
padding: 100px;
width: 300px;
height: 300px;
font-family: monospace, arial;
}
#button:active {
background-color: red;
border: red solid 20px;
}
#score-table {
position: absolute;
top: 0px;
right: 0px;
}
td,tr,table,th {
border: black solid;
}
table {
border-collapse: collapse;
}
</style>
<script>
const regex = new RegExp("^[a-zA-Z0-9_-]+$");
let name = validate_data(prompt("Nickname for the leaderboard"));
console.log(name)
const ws = new WebSocket('ws://deadvey.com:8084/ws');
ws.onopen = (event) => {
ws.send(name);
};
let firstMessage = true;
let latestMessage = 0;
ws.onmessage = (event) => {
console.log(event.data);
if (firstMessage) {
firstMessage = false;
data = JSON.parse(event.data);
const tableDiv = document.getElementById("score-table");
const table = document.createElement("table");
// Add header row
table.innerHTML = `<tr><th>Rank</th><th>Score</th><th>Name</th><th>P</th></tr>`;
// Add data rows concisely using forEach
data.hiscores.forEach((entry, index) => {
table.innerHTML += `<tr>
<td>#${index + 1}</td>
<td>${validate_data(entry.score)}</td>
<td>${validate_data(entry.person)}</td>
<td>${((2/3)**validate_data(entry.score)).toExponential(2)}</td>
</tr>`;
});
tableDiv.appendChild(table);
}
else { latestMessage = validate_data(event.data); }
};
function send_click()
{
ws.send(true);
if (latestMessage !== null) {
document.getElementById("button").textContent=latestMessage;
latestMessage = null;
}
}
function get_rand(min, max) {
return Math.round(Math.random() * (max - min) + min);
}
window.addEventListener('beforeunload', () => {
ws.close();
});
function validate_data(data) { // Only allow a-z, A-Z, 0-9, - and _ characters, sorry Ramón
if (regex.test(data)) { return data }
else { return "anon" }
}
</script>
</html>
-1
View File
@@ -1 +0,0 @@
{"error":[0,60],"anon":[15,10],"deadvey":[1129258,0]}
+2
View File
@@ -1 +1,3 @@
brace_style = "AlwaysNextLine"
control_brace_style = "AlwaysNextLine"
hard_tabs = true hard_tabs = true
+216 -137
View File
@@ -1,7 +1,7 @@
// TODO rename pingscores userscores
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs, fs,
io::Write,
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
sync::Arc, sync::Arc,
}; };
@@ -12,7 +12,7 @@ use axum::{
State, State,
ws::{Message, WebSocket, WebSocketUpgrade}, ws::{Message, WebSocket, WebSocketUpgrade},
}, },
response::{Html, IntoResponse}, response::IntoResponse,
routing::get, routing::get,
}; };
use futures::StreamExt as _; use futures::StreamExt as _;
@@ -20,32 +20,44 @@ use rand::random_bool;
use serde::{Deserialize, Serialize, de}; use serde::{Deserialize, Serialize, de};
use serde_json::json; use serde_json::json;
use tokio::{ use tokio::{
sync::{Mutex, mpsc}, io::AsyncWriteExt as _,
sync::{RwLock, mpsc},
time::{Duration, sleep}, time::{Duration, sleep},
}; };
use tower_http::services::ServeDir;
#[derive(Deserialize, Serialize, Debug, Ord, Eq, PartialEq, PartialOrd, Clone)] #[derive(Deserialize, Serialize, Debug, Ord, Eq, PartialEq, PartialOrd, Clone)]
struct Entry { struct Entry
{
score: u32, score: u32,
person: String, person: String,
} }
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState
tx: mpsc::Sender<LeaderboardUpdate>, {
hiscores: Arc<Mutex<Vec<Entry>>>, tx: mpsc::UnboundedSender<LeaderboardUpdate>,
loscores: Arc<Mutex<Vec<Entry>>>, hiscores: Arc<RwLock<Vec<Entry>>>,
pingscores: Arc<Mutex<HashMap<String, (u64, u32)>>>, // u64 is reset count and u32 is PB loscores: Arc<RwLock<Vec<Entry>>>,
pingscores: Arc<RwLock<HashMap<String, (u64, u32)>>>, // u64 is reset count and u32 is PB
} }
struct LeaderboardUpdate { struct LeaderboardUpdate
{
name: Arc<str>, name: Arc<str>,
update: LeaderboardUpdateType, update: LeaderboardUpdateType,
} }
enum LeaderboardUpdateType { enum LeaderboardUpdateType
Reset { hiscore_pingscore: u32 }, {
Increment { loscore: u32 }, Reset
{
hiscore_pingscore: u32
},
Increment
{
loscore: u32
},
} }
const CHANCE: f64 = 1.0 / 3.0; const CHANCE: f64 = 1.0 / 3.0;
@@ -56,32 +68,47 @@ const PATH_PINGSCORES: &str = "pingscores.json";
const MAX_LEADERBOARD: usize = 20; const MAX_LEADERBOARD: usize = 20;
async fn write_file(file_path: &str, file_contents: &str) -> anyhow::Result<()>
{
let mut file = tokio::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(file_path)
.await?;
file.write_all(file_contents.as_bytes()).await?;
file.flush().await?;
Ok(())
}
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()>
{
fn read_file<T: for<'de> de::Deserialize<'de>>( fn read_file<T: for<'de> de::Deserialize<'de>>(
file_path: &str, file_path: &str,
) -> anyhow::Result<Arc<Mutex<T>>> { ) -> anyhow::Result<Arc<RwLock<T>>>
{
let file_contents: String = fs::read_to_string(file_path)?; let file_contents: String = fs::read_to_string(file_path)?;
Ok(Arc::new(Mutex::new(serde_json::from_str(&file_contents)?))) Ok(Arc::new(RwLock::new(serde_json::from_str(&file_contents)?)))
} }
/// Makes the vector at `vec` one with a capacity of exactly [`MAX_LEADERBOARD`] if `vec` is /// Makes the vector at `vec` one with a capacity of exactly [`MAX_LEADERBOARD`] if `vec` is
/// smaller or equal. /// smaller or equal.
fn exact_leaderboard<T>(mut vec: impl DerefMut<Target = Vec<T>>) { fn exact_leaderboard<T>(mut vec: impl DerefMut<Target = Vec<T>>)
{
let old_vec = std::mem::replace(&mut *vec, Vec::with_capacity(MAX_LEADERBOARD)); let old_vec = std::mem::replace(&mut *vec, Vec::with_capacity(MAX_LEADERBOARD));
for e in old_vec { for e in old_vec
{
vec.push(e); vec.push(e);
} }
} }
let hiscores: Arc<Mutex<Vec<Entry>>> = read_file(PATH_HISCORES)?; let hiscores: Arc<RwLock<Vec<Entry>>> = read_file(PATH_HISCORES)?;
exact_leaderboard(hiscores.lock().await); exact_leaderboard(hiscores.write().await);
let loscores: Arc<Mutex<Vec<Entry>>> = read_file(PATH_LOSCORES)?; let loscores: Arc<RwLock<Vec<Entry>>> = read_file(PATH_LOSCORES)?;
exact_leaderboard(loscores.lock().await); exact_leaderboard(loscores.write().await);
let pingscores: Arc<Mutex<HashMap<String, (u64, u32)>>> = read_file(PATH_PINGSCORES)?; let pingscores: Arc<RwLock<HashMap<String, (u64, u32)>>> = read_file(PATH_PINGSCORES)?;
let (tx, rx) = mpsc::channel::<LeaderboardUpdate>(1024);
let (tx, rx) = mpsc::unbounded_channel::<LeaderboardUpdate>();
{ {
let (hiscores, loscores, pingscores) = let (hiscores, loscores, pingscores) =
(hiscores.clone(), loscores.clone(), pingscores.clone()); (hiscores.clone(), loscores.clone(), pingscores.clone());
@@ -94,28 +121,24 @@ async fn main() -> anyhow::Result<()> {
let pingscores = pingscores.clone(); let pingscores = pingscores.clone();
tokio::spawn(async move { tokio::spawn(async move {
// write pingscores every 30s // write pingscores every 30s
loop { loop
{
sleep(Duration::from_secs(30)).await; sleep(Duration::from_secs(30)).await;
let pingscores = pingscores.lock().await; let pingscores = pingscores.read().await.clone();
let file_contents: String = serde_json::to_string(&pingscores.clone()) let file_contents: String =
.expect("failed to serialize pingscores"); serde_json::to_string(&pingscores).expect("failed to serialize pingscores");
drop(pingscores); write_file(PATH_PINGSCORES, &file_contents)
let mut file = fs::OpenOptions::new() .await
.write(true)
.truncate(true)
.open(PATH_PINGSCORES)
.expect("failed to open pingscores file");
file.write_all(file_contents.as_bytes())
.expect("failed to write pingscores"); .expect("failed to write pingscores");
drop(file);
} }
}); });
} }
let static_files = ServeDir::new("./static");
let app = Router::new() let app = Router::new()
.route("/", get(index)) .fallback_service(static_files)
.route("/ws", get(ws_handler)) .route("/ws", get(ws_handler))
.route("/leaderboard", get(leaderboard)) .route("/ws-leaderboard", get(leaderboard_handler))
.with_state(AppState { .with_state(AppState {
tx, tx,
hiscores: Arc::clone(&hiscores), hiscores: Arc::clone(&hiscores),
@@ -131,31 +154,26 @@ async fn main() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
async fn index() -> Html<&'static str> {
Html(include_str!("../index.html"))
}
async fn leaderboard() -> Html<&'static str> {
Html(include_str!("../leaderboard.html"))
}
// receiver: 0 for hiscore, 1 for loscore, 2 for pingscore // receiver: 0 for hiscore, 1 for loscore, 2 for pingscore
async fn handle_hiscores( async fn handle_hiscores(
mut rx: mpsc::Receiver<LeaderboardUpdate>, mut rx: mpsc::UnboundedReceiver<LeaderboardUpdate>,
hiscores: &Mutex<Vec<Entry>>, hiscores: &RwLock<Vec<Entry>>,
loscores: &Mutex<Vec<Entry>>, loscores: &RwLock<Vec<Entry>>,
pingscores: &Mutex<HashMap<String, (u64, u32)>>, pingscores: &RwLock<HashMap<String, (u64, u32)>>,
) { )
fn update_scoretable<G: Deref<Target = Vec<Entry>> + DerefMut>( {
async fn update_scoretable<G: Deref<Target = Vec<Entry>> + DerefMut>(
score_name: &str, score_name: &str,
mut scoretable_lock: G, mut scoretable_lock: G,
name: &str, name: &str,
score: u32, score: u32,
file_path: &str, file_path: &str,
) -> anyhow::Result<()> { ) -> anyhow::Result<()>
{
let file_contents = {
let scoretable = &mut *scoretable_lock; let scoretable = &mut *scoretable_lock;
if let Some(index_to_insert_at) = scoretable.iter().position(|e| score > e.score)
if let Some(index_to_insert_at) = scoretable.iter().position(|e| score > e.score) { {
println!("New {score_name} {score} by {name}"); println!("New {score_name} {score} by {name}");
scoretable[index_to_insert_at..].rotate_right(1); scoretable[index_to_insert_at..].rotate_right(1);
let push_out = std::mem::replace( let push_out = std::mem::replace(
@@ -165,64 +183,71 @@ async fn handle_hiscores(
person: name.to_string(), person: name.to_string(),
}, },
); );
if scoretable.len() < MAX_LEADERBOARD { if scoretable.len() < MAX_LEADERBOARD
{
scoretable.push(push_out); scoretable.push(push_out);
} }
let file_contents: String = serde_json::to_string(&*scoretable_lock)?; Some(serde_json::to_string(&*scoretable_lock)?)
drop(scoretable_lock); }
let mut file = fs::OpenOptions::new() else if scoretable.len() < MAX_LEADERBOARD
.write(true) {
.truncate(true)
.open(file_path)?;
file.write_all(file_contents.as_bytes())?;
file.flush()?;
} else if scoretable.len() < MAX_LEADERBOARD {
println!("New {score_name} {score} by {name}"); println!("New {score_name} {score} by {name}");
scoretable.push(Entry { scoretable.push(Entry {
score, score,
person: name.to_string(), person: name.to_string(),
}); });
None
}
else
{
None
}
};
drop(scoretable_lock);
if let Some(file_contents) = file_contents
{
write_file(file_path, &file_contents).await?;
} }
Ok(()) Ok(())
} }
// Panic galore // Panic galore
let mut hiscores_lock = hiscores.lock().await; let mut hiscores_lock = hiscores.write().await;
hiscores_lock.sort(); hiscores_lock.sort();
hiscores_lock.reverse(); hiscores_lock.reverse();
let file_contents: String = let file_contents: String =
serde_json::to_string(&hiscores_lock.clone()).expect("failed to serialize hiscores"); serde_json::to_string(&hiscores_lock.clone()).expect("failed to serialize hiscores");
drop(hiscores_lock); drop(hiscores_lock);
let mut file = fs::OpenOptions::new() write_file(PATH_HISCORES, &file_contents)
.write(true) .await
.truncate(true)
.open(PATH_HISCORES)
.expect("failed to open hiscores");
file.write_all(file_contents.as_bytes())
.expect("failed to write hiscores"); .expect("failed to write hiscores");
drop(file);
loop { loop
{
let LeaderboardUpdate { name, update } = rx.recv().await.expect("channel error"); let LeaderboardUpdate { name, update } = rx.recv().await.expect("channel error");
match update { match update
LeaderboardUpdateType::Reset { hiscore_pingscore } => { {
LeaderboardUpdateType::Reset { hiscore_pingscore } =>
{
// Hiscore // Hiscore
update_scoretable( update_scoretable(
"hiscore", "hiscore",
hiscores.lock().await, hiscores.write().await,
&name, &name,
hiscore_pingscore, hiscore_pingscore,
PATH_HISCORES, PATH_HISCORES,
) )
.await
.expect("failed to update hiscores"); .expect("failed to update hiscores");
// Pingscore // Pingscore
let mut pingscores = pingscores.lock().await; let mut pingscores = pingscores.write().await;
// pb // pb
if hiscore_pingscore > pingscores.get(&*name).unwrap_or(&(0, 0)).1 { if hiscore_pingscore > pingscores.get(&*name).unwrap_or(&(0, 0)).1
{
pingscores.entry(name.to_string()).or_insert((0, 0)).1 = hiscore_pingscore; pingscores.entry(name.to_string()).or_insert((0, 0)).1 = hiscore_pingscore;
println!("{name} new PB: {hiscore_pingscore}"); println!("{name} new PB: {hiscore_pingscore}");
} }
@@ -230,62 +255,99 @@ async fn handle_hiscores(
drop(pingscores); drop(pingscores);
} }
LeaderboardUpdateType::Increment { loscore } => { LeaderboardUpdateType::Increment { loscore } =>
{
update_scoretable( update_scoretable(
"loscore", "loscore",
loscores.lock().await, loscores.write().await,
&name, &name,
loscore, loscore,
PATH_LOSCORES, PATH_LOSCORES,
) )
.await
.expect("failed to update loscores"); .expect("failed to update loscores");
} }
} }
} }
} }
async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> impl IntoResponse { async fn leaderboard_handler(
ws: WebSocketUpgrade,
State(state): State<AppState>,
) -> impl IntoResponse
{
ws.on_upgrade(|socket| async move { ws.on_upgrade(|socket| async move {
handle_socket( handle_leaderboard(socket, &state.hiscores, &state.loscores, &state.pingscores).await;
socket,
&state.tx,
&state.hiscores,
&state.loscores,
&state.pingscores,
)
.await;
}) })
} }
async fn handle_leaderboard(
async fn handle_socket(
mut socket: WebSocket, mut socket: WebSocket,
tx: &mpsc::Sender<LeaderboardUpdate>, hiscores: &RwLock<Vec<Entry>>,
hiscores: &Mutex<Vec<Entry>>, loscores: &RwLock<Vec<Entry>>,
loscores: &Mutex<Vec<Entry>>, pingscores: &RwLock<HashMap<String, (u64, u32)>>,
pingscores: &Mutex<HashMap<String, (u64, u32)>>, )
) { {
let mut value: u32 = 0; match socket.next().await
{
let msg = { Some(Ok(Message::Text(selection))) =>
json!({ {
"hiscores": &*hiscores.lock().await, let msg = match selection.as_str()
"loscores": &*loscores.lock().await, {
"pingscores": &*pingscores.lock().await // all the leaderboards
"0" =>
{
let hiscores = hiscores.read().await.clone();
let loscores = loscores.read().await.clone();
let pingscores = pingscores.read().await.clone();
json!
({
"hiscores": hiscores,
"loscores": loscores,
"pingscores": pingscores
}) })
.to_string() .to_string()
}
// just the hiscores table
"1" => json! ({ "hiscores": hiscores.read().await.clone() }).to_string(),
// just the loscores table
"2" => json! ({ "loscores": hiscores.read().await.clone() }).to_string(),
// just the pingscores table
"3" => json! ({ "pingscores": hiscores.read().await.clone() }).to_string(),
_ => "Invalid leaderboard selection, please use 0,1,2 or 3".to_string(),
}; };
let _ = socket.send(Message::Text(msg.into())).await;
}
_ =>
{
let _ = socket
.send(Message::Text(
"Invalid leaderboard selection, please use 0,1,2 or 3".into(),
))
.await;
}
}
}
let Some(name) = socket.next().await else { async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> impl IntoResponse
eprintln!("user gave no username"); {
ws.on_upgrade(|socket| async move {
handle_socket(socket, &state.tx).await;
})
}
async fn handle_socket(mut socket: WebSocket, tx: &mpsc::UnboundedSender<LeaderboardUpdate>)
{
let mut value: u32 = 0;
let Some(name) = socket.next().await
else
{
eprintln!("No username");
return; return;
}; };
let name: Arc<str> = match name.expect("failed to recv socket msg") { let name: Arc<str> = match name.expect("failed to recv socket msg")
Message::Text(text)
if let text = text.to_string()
&& validate_name(&text) =>
{ {
Arc::from(text.into_boxed_str()) Message::Text(text) => Arc::from(validate_name(&text)),
}
_ => Arc::from("anon"), _ => Arc::from("anon"),
}; };
@@ -294,34 +356,34 @@ async fn handle_socket(
let mut resets: u32 = 0; let mut resets: u32 = 0;
let mut prev: u32 = 0; let mut prev: u32 = 0;
let _ = socket.send(Message::Text(msg.into())).await; while let Some(msg) = socket.next().await
{
while let Some(msg) = socket.next().await { match msg
match msg { {
Ok(Message::Text(_)) => { Ok(Message::Text(_)) =>
if random_bool(CHANCE) { {
// 1/3 chance of failing
if random_bool(CHANCE)
{
// reset // reset
let _ = tx let _ = tx.send(LeaderboardUpdate {
.send(LeaderboardUpdate {
name: name.clone(), name: name.clone(),
update: LeaderboardUpdateType::Reset { update: LeaderboardUpdateType::Reset {
hiscore_pingscore: value, hiscore_pingscore: value,
}, },
}) });
.await;
resets += 1; resets += 1;
value = 0; value = 0;
} }
// 1/3 chance of failing else
else { {
value += 1; value += 1;
if prev == 0 { if prev == 0
let _ = tx {
.send(LeaderboardUpdate { let _ = tx.send(LeaderboardUpdate {
name: name.clone(), name: name.clone(),
update: LeaderboardUpdateType::Increment { loscore: resets }, update: LeaderboardUpdateType::Increment { loscore: resets },
}) });
.await;
resets = 0; resets = 0;
} }
} }
@@ -329,22 +391,39 @@ async fn handle_socket(
prev = value; prev = value;
} }
Ok(Message::Close(_)) => { Ok(Message::Close(_)) =>
{
break; break;
} }
_ => {} _ =>
{}
} }
} }
} }
fn validate_name(input: &str) -> bool { fn validate_name(input: &str) -> &str
{
let input = input.trim(); let input = input.trim();
if input == "null"
{
return "anon";
}
// Length check // Length check
if input.is_empty() || input.len() > 32 { if input.is_empty() || input.len() > 32
return false; {
return "anon";
} }
input.chars().all(|c| c.is_ascii_alphanumeric()) // Allow only letters, numbers, _ and -
if input
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
{
input
}
else
{
"anon"
}
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

+25
View File
@@ -0,0 +1,25 @@
#button {
background-color: red;
border: darkred solid 15px;
border-radius: 50%;
font-size: 50px;
padding: 100px;
width: 300px;
height: 300px;
font-family: monospace, arial;
}
#button:active {
background-color: red;
border: red solid 20px;
}
#score-table {
position: absolute;
top: 0px;
right: 0px;
}
td,tr,table,th {
border: black solid;
}
table {
border-collapse: collapse;
}
+67
View File
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/index.css" />
</head>
<body>
<h1>Each time you press the button there's a 1/3 chance of returning to 0</h1>
<button id="button" onclick="send_click()">0</button>
<div id="score-table"></div>
</body>
<script>
const regex = new RegExp("^[a-zA-Z0-9_-]+$");
const ws = new WebSocket('ws://deadvey.com:8084/ws');
console.log(name)
let latestMessage = 0;
ws.onopen = (event) => {
let name = validate_data(prompt("Nickname for the leaderboard"));
ws.send(name);
}
const ws_leaderboard = new WebSocket('ws://deadvey.com:8084/ws-leaderboard');
ws_leaderboard.onopen = (event) => {
ws_leaderboard.send("1");
};
ws_leaderboard.onmessage = (event) => {
console.log(event.data);
firstMessage = false;
data = JSON.parse(event.data);
const tableDiv = document.getElementById("score-table");
const table = document.createElement("table");
// Add header row
table.innerHTML = `<tr><th>Rank</th><th>Score</th><th>Name</th><th>P</th></tr>`;
// Add data rows concisely using forEach
data.hiscores.forEach((entry, index) => {
table.innerHTML += `<tr>
<td>#${index + 1}</td>
<td>${validate_data(entry.score)}</td>
<td>${validate_data(entry.person)}</td>
<td>${((2/3)**validate_data(entry.score)).toExponential(2)}</td>
</tr>`;
});
tableDiv.appendChild(table);
};
ws.onmessage = (event) => {
console.log(event.data);
latestMessage = validate_data(event.data);
}
function send_click()
{
ws.send("");
if (latestMessage !== null) {
document.getElementById("button").textContent=latestMessage;
latestMessage = null;
}
}
function get_rand(min, max) {
return Math.round(Math.random() * (max - min) + min);
}
window.addEventListener('beforeunload', () => {
ws.close();
});
function validate_data(data) { // Only allow a-z, A-Z, 0-9, - and _ characters, sorry Ramón
if (regex.test(data)) { return data }
else { return "anon" }
}
</script>
</html>
@@ -1,7 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="/index.css" />
</head> </head>
<body> <body>
<h1>Hiscores</h1> <h1>Hiscores</h1>
@@ -14,19 +15,11 @@
<p>Total number of resets and personal bests of each username</p> <p>Total number of resets and personal bests of each username</p>
<div id="pingscore-table"></div> <div id="pingscore-table"></div>
</body> </body>
<style>
td,tr,table,th {
border: black solid;
}
table {
border-collapse: collapse;
}
</style>
<script> <script>
const ws = new WebSocket('ws://deadvey.com:8084/ws'); const ws = new WebSocket('ws://deadvey.com:8084/ws-leaderboard');
const regex = new RegExp("^[a-zA-Z0-9_-]+$"); const regex = new RegExp("^[a-zA-Z0-9_-]+$");
ws.onopen = (event) => { ws.onopen = (event) => {
ws.send(""); ws.send('0'); // send all
}; };
let latestMessage = 0; let latestMessage = 0;
ws.onmessage = (event) => { ws.onmessage = (event) => {