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
loscores.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
+216 -137
View File
@@ -1,7 +1,7 @@
// TODO rename pingscores userscores
use std::{
collections::HashMap,
fs,
io::Write,
ops::{Deref, DerefMut},
sync::Arc,
};
@@ -12,7 +12,7 @@ use axum::{
State,
ws::{Message, WebSocket, WebSocketUpgrade},
},
response::{Html, IntoResponse},
response::IntoResponse,
routing::get,
};
use futures::StreamExt as _;
@@ -20,32 +20,44 @@ use rand::random_bool;
use serde::{Deserialize, Serialize, de};
use serde_json::json;
use tokio::{
sync::{Mutex, mpsc},
io::AsyncWriteExt as _,
sync::{RwLock, mpsc},
time::{Duration, sleep},
};
use tower_http::services::ServeDir;
#[derive(Deserialize, Serialize, Debug, Ord, Eq, PartialEq, PartialOrd, Clone)]
struct Entry {
struct Entry
{
score: u32,
person: String,
}
#[derive(Clone)]
struct AppState {
tx: mpsc::Sender<LeaderboardUpdate>,
hiscores: Arc<Mutex<Vec<Entry>>>,
loscores: Arc<Mutex<Vec<Entry>>>,
pingscores: Arc<Mutex<HashMap<String, (u64, u32)>>>, // u64 is reset count and u32 is PB
struct AppState
{
tx: mpsc::UnboundedSender<LeaderboardUpdate>,
hiscores: Arc<RwLock<Vec<Entry>>>,
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>,
update: LeaderboardUpdateType,
}
enum LeaderboardUpdateType {
Reset { hiscore_pingscore: u32 },
Increment { loscore: u32 },
enum LeaderboardUpdateType
{
Reset
{
hiscore_pingscore: u32
},
Increment
{
loscore: u32
},
}
const CHANCE: f64 = 1.0 / 3.0;
@@ -56,32 +68,47 @@ const PATH_PINGSCORES: &str = "pingscores.json";
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]
async fn main() -> anyhow::Result<()> {
async fn main() -> anyhow::Result<()>
{
fn read_file<T: for<'de> de::Deserialize<'de>>(
file_path: &str,
) -> anyhow::Result<Arc<Mutex<T>>> {
) -> anyhow::Result<Arc<RwLock<T>>>
{
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
/// 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));
for e in old_vec {
for e in old_vec
{
vec.push(e);
}
}
let hiscores: Arc<Mutex<Vec<Entry>>> = read_file(PATH_HISCORES)?;
exact_leaderboard(hiscores.lock().await);
let loscores: Arc<Mutex<Vec<Entry>>> = read_file(PATH_LOSCORES)?;
exact_leaderboard(loscores.lock().await);
let pingscores: Arc<Mutex<HashMap<String, (u64, u32)>>> = read_file(PATH_PINGSCORES)?;
let (tx, rx) = mpsc::channel::<LeaderboardUpdate>(1024);
let hiscores: Arc<RwLock<Vec<Entry>>> = read_file(PATH_HISCORES)?;
exact_leaderboard(hiscores.write().await);
let loscores: Arc<RwLock<Vec<Entry>>> = read_file(PATH_LOSCORES)?;
exact_leaderboard(loscores.write().await);
let pingscores: Arc<RwLock<HashMap<String, (u64, u32)>>> = read_file(PATH_PINGSCORES)?;
let (tx, rx) = mpsc::unbounded_channel::<LeaderboardUpdate>();
{
let (hiscores, loscores, pingscores) =
(hiscores.clone(), loscores.clone(), pingscores.clone());
@@ -94,28 +121,24 @@ async fn main() -> anyhow::Result<()> {
let pingscores = pingscores.clone();
tokio::spawn(async move {
// write pingscores every 30s
loop {
loop
{
sleep(Duration::from_secs(30)).await;
let pingscores = pingscores.lock().await;
let file_contents: String = serde_json::to_string(&pingscores.clone())
.expect("failed to serialize pingscores");
drop(pingscores);
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(PATH_PINGSCORES)
.expect("failed to open pingscores file");
file.write_all(file_contents.as_bytes())
let pingscores = pingscores.read().await.clone();
let file_contents: String =
serde_json::to_string(&pingscores).expect("failed to serialize pingscores");
write_file(PATH_PINGSCORES, &file_contents)
.await
.expect("failed to write pingscores");
drop(file);
}
});
}
let static_files = ServeDir::new("./static");
let app = Router::new()
.route("/", get(index))
.fallback_service(static_files)
.route("/ws", get(ws_handler))
.route("/leaderboard", get(leaderboard))
.route("/ws-leaderboard", get(leaderboard_handler))
.with_state(AppState {
tx,
hiscores: Arc::clone(&hiscores),
@@ -131,31 +154,26 @@ async fn main() -> anyhow::Result<()> {
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
async fn handle_hiscores(
mut rx: mpsc::Receiver<LeaderboardUpdate>,
hiscores: &Mutex<Vec<Entry>>,
loscores: &Mutex<Vec<Entry>>,
pingscores: &Mutex<HashMap<String, (u64, u32)>>,
) {
fn update_scoretable<G: Deref<Target = Vec<Entry>> + DerefMut>(
mut rx: mpsc::UnboundedReceiver<LeaderboardUpdate>,
hiscores: &RwLock<Vec<Entry>>,
loscores: &RwLock<Vec<Entry>>,
pingscores: &RwLock<HashMap<String, (u64, u32)>>,
)
{
async fn update_scoretable<G: Deref<Target = Vec<Entry>> + DerefMut>(
score_name: &str,
mut scoretable_lock: G,
name: &str,
score: u32,
file_path: &str,
) -> anyhow::Result<()> {
) -> anyhow::Result<()>
{
let file_contents = {
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}");
scoretable[index_to_insert_at..].rotate_right(1);
let push_out = std::mem::replace(
@@ -165,64 +183,71 @@ async fn handle_hiscores(
person: name.to_string(),
},
);
if scoretable.len() < MAX_LEADERBOARD {
if scoretable.len() < MAX_LEADERBOARD
{
scoretable.push(push_out);
}
let file_contents: String = serde_json::to_string(&*scoretable_lock)?;
drop(scoretable_lock);
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(file_path)?;
file.write_all(file_contents.as_bytes())?;
file.flush()?;
} else if scoretable.len() < MAX_LEADERBOARD {
Some(serde_json::to_string(&*scoretable_lock)?)
}
else if scoretable.len() < MAX_LEADERBOARD
{
println!("New {score_name} {score} by {name}");
scoretable.push(Entry {
score,
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(())
}
// Panic galore
let mut hiscores_lock = hiscores.lock().await;
let mut hiscores_lock = hiscores.write().await;
hiscores_lock.sort();
hiscores_lock.reverse();
let file_contents: String =
serde_json::to_string(&hiscores_lock.clone()).expect("failed to serialize hiscores");
drop(hiscores_lock);
let mut file = fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(PATH_HISCORES)
.expect("failed to open hiscores");
file.write_all(file_contents.as_bytes())
write_file(PATH_HISCORES, &file_contents)
.await
.expect("failed to write hiscores");
drop(file);
loop {
loop
{
let LeaderboardUpdate { name, update } = rx.recv().await.expect("channel error");
match update {
LeaderboardUpdateType::Reset { hiscore_pingscore } => {
match update
{
LeaderboardUpdateType::Reset { hiscore_pingscore } =>
{
// Hiscore
update_scoretable(
"hiscore",
hiscores.lock().await,
hiscores.write().await,
&name,
hiscore_pingscore,
PATH_HISCORES,
)
.await
.expect("failed to update hiscores");
// Pingscore
let mut pingscores = pingscores.lock().await;
let mut pingscores = pingscores.write().await;
// 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;
println!("{name} new PB: {hiscore_pingscore}");
}
@@ -230,62 +255,99 @@ async fn handle_hiscores(
drop(pingscores);
}
LeaderboardUpdateType::Increment { loscore } => {
LeaderboardUpdateType::Increment { loscore } =>
{
update_scoretable(
"loscore",
loscores.lock().await,
loscores.write().await,
&name,
loscore,
PATH_LOSCORES,
)
.await
.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 {
handle_socket(
socket,
&state.tx,
&state.hiscores,
&state.loscores,
&state.pingscores,
)
.await;
handle_leaderboard(socket, &state.hiscores, &state.loscores, &state.pingscores).await;
})
}
async fn handle_socket(
async fn handle_leaderboard(
mut socket: WebSocket,
tx: &mpsc::Sender<LeaderboardUpdate>,
hiscores: &Mutex<Vec<Entry>>,
loscores: &Mutex<Vec<Entry>>,
pingscores: &Mutex<HashMap<String, (u64, u32)>>,
) {
let mut value: u32 = 0;
let msg = {
json!({
"hiscores": &*hiscores.lock().await,
"loscores": &*loscores.lock().await,
"pingscores": &*pingscores.lock().await
hiscores: &RwLock<Vec<Entry>>,
loscores: &RwLock<Vec<Entry>>,
pingscores: &RwLock<HashMap<String, (u64, u32)>>,
)
{
match socket.next().await
{
Some(Ok(Message::Text(selection))) =>
{
let msg = match selection.as_str()
{
// 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()
}
// 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 {
eprintln!("user gave no username");
async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> impl IntoResponse
{
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;
};
let name: Arc<str> = match name.expect("failed to recv socket msg") {
Message::Text(text)
if let text = text.to_string()
&& validate_name(&text) =>
let name: Arc<str> = match name.expect("failed to recv socket msg")
{
Arc::from(text.into_boxed_str())
}
Message::Text(text) => Arc::from(validate_name(&text)),
_ => Arc::from("anon"),
};
@@ -294,34 +356,34 @@ async fn handle_socket(
let mut resets: u32 = 0;
let mut prev: u32 = 0;
let _ = socket.send(Message::Text(msg.into())).await;
while let Some(msg) = socket.next().await {
match msg {
Ok(Message::Text(_)) => {
if random_bool(CHANCE) {
while let Some(msg) = socket.next().await
{
match msg
{
Ok(Message::Text(_)) =>
{
// 1/3 chance of failing
if random_bool(CHANCE)
{
// reset
let _ = tx
.send(LeaderboardUpdate {
let _ = tx.send(LeaderboardUpdate {
name: name.clone(),
update: LeaderboardUpdateType::Reset {
hiscore_pingscore: value,
},
})
.await;
});
resets += 1;
value = 0;
}
// 1/3 chance of failing
else {
else
{
value += 1;
if prev == 0 {
let _ = tx
.send(LeaderboardUpdate {
if prev == 0
{
let _ = tx.send(LeaderboardUpdate {
name: name.clone(),
update: LeaderboardUpdateType::Increment { loscore: resets },
})
.await;
});
resets = 0;
}
}
@@ -329,22 +391,39 @@ async fn handle_socket(
prev = value;
}
Ok(Message::Close(_)) => {
Ok(Message::Close(_)) =>
{
break;
}
_ => {}
_ =>
{}
}
}
}
fn validate_name(input: &str) -> bool {
fn validate_name(input: &str) -> &str
{
let input = input.trim();
if input == "null"
{
return "anon";
}
// Length check
if input.is_empty() || input.len() > 32 {
return false;
if input.is_empty() || input.len() > 32
{
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>
<html>
<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>
<body>
<h1>Hiscores</h1>
@@ -14,19 +15,11 @@
<p>Total number of resets and personal bests of each username</p>
<div id="pingscore-table"></div>
</body>
<style>
td,tr,table,th {
border: black solid;
}
table {
border-collapse: collapse;
}
</style>
<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_-]+$");
ws.onopen = (event) => {
ws.send("");
ws.send('0'); // send all
};
let latestMessage = 0;
ws.onmessage = (event) => {