25 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
javalsai 14a16aa89f fix: optimistic programming and add anyhow 2026-05-31 01:18:34 +02:00
javalsai 9493acf385 lint: add exhaustive clippy lints 2026-05-31 01:18:13 +02:00
javalsai 45a8da1053 perf: optimize release build 2026-05-31 00:59:59 +02:00
javalsai 978350ffdf perf: improve top-20 leaderboard algorithm 2026-05-31 00:54:40 +02:00
javalsai 8bee404f80 perf: use async locks 2026-05-31 00:54:36 +02:00
javalsai fed9d4d8e9 style: format 2026-05-31 00:53:57 +02:00
javalsai a183b33ce8 perf: remove arc clones, general clones, and unnecessary regex 2026-05-31 00:53:56 +02:00
javalsai e55b698cb3 style: order imports 2026-05-31 00:37:07 +02:00
javalsai 76643dce94 fix: panic on empty leaderboards 2026-05-30 05:25:22 +02:00
15 changed files with 492 additions and 330 deletions
+1
View File
@@ -2,3 +2,4 @@
hiscores.json
loscores.json
pingscores.json
.*
Binary file not shown.
Generated
+1 -39
View File
@@ -2,15 +2,6 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anyhow"
version = "1.0.102"
@@ -103,10 +94,10 @@ dependencies = [
name = "button"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"futures",
"rand 0.10.1",
"regex",
"serde",
"serde_json",
"tokio",
@@ -677,35 +668,6 @@ dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "ryu"
version = "1.0.23"
+25 -5
View File
@@ -4,11 +4,31 @@ version = "0.1.0"
edition = "2024"
[dependencies]
axum = {version="0.8.9",features=["ws"]}
anyhow = "1.0.102"
axum = { version = "0.8.9", features = ["ws"] }
futures = "0.3.32"
rand = "0.10.1"
regex = "1.12.3"
serde = {version="1.0.228",features=["derive"]}
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
tokio = {version="1.52.3",features=["full"]}
tower-http = {version="0.6.11",features=["fs"]}
tokio = { version = "1.52.3", features = ["full"] }
tower-http = { version = "0.6.11", features = ["fs"] }
[profile.release]
codegen-units = 1
opt-level = 3
lto = true
strip = "symbols"
panic = "abort"
[lints.clippy]
cargo = { level = "warn", priority = -1 }
correctness = { level = "deny", priority = -1 }
nursery = { level = "deny", priority = -1 }
option_if_let_else = { level = "allow" }
cargo_common_metadata = "allow"
multiple_crate_versions = "allow"
pedantic = { level = "deny", priority = -1 }
perf = { level = "deny", priority = -1 }
style = { level = "deny", priority = -1 }
unwrap_used = "deny"
+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]}
+3
View File
@@ -0,0 +1,3 @@
brace_style = "AlwaysNextLine"
control_brace_style = "AlwaysNextLine"
hard_tabs = true
+350 -183
View File
@@ -1,227 +1,389 @@
use axum::{
extract::ws::{Message, WebSocket, WebSocketUpgrade},
response::{Html, IntoResponse},
routing::get,
Router,
// TODO rename pingscores userscores
use std::{
collections::HashMap,
fs,
ops::{Deref, DerefMut},
sync::Arc,
};
use std::collections::HashMap;
use axum::extract::State;
use rand::random_bool;
use futures::stream::StreamExt;
use serde_json;
use serde_json::json;
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::Write;
use std::sync::mpsc;
use std::sync::{Mutex, Arc};
use tokio::time::{sleep,Duration};
use regex::Regex;
#[derive(Deserialize,Serialize,Debug,Ord,Eq,PartialEq,PartialOrd,Clone)]
use axum::{
Router,
extract::{
State,
ws::{Message, WebSocket, WebSocketUpgrade},
},
response::IntoResponse,
routing::get,
};
use futures::StreamExt as _;
use rand::random_bool;
use serde::{Deserialize, Serialize, de};
use serde_json::json;
use tokio::{
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
{
score: u32,
person: String,
}
enum Leaderboard
{
Hiscores,
Loscores,
Pingscores,
}
#[derive(Clone)]
struct AppState {
tx: mpsc::Sender<(Entry,Leaderboard)>,
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
}
static CHANCE: f64 = 1.0/3.0;
struct LeaderboardUpdate
{
name: Arc<str>,
update: LeaderboardUpdateType,
}
enum LeaderboardUpdateType
{
Reset
{
hiscore_pingscore: u32
},
Increment
{
loscore: u32
},
}
const CHANCE: f64 = 1.0 / 3.0;
const PATH_HISCORES: &str = "hiscores.json";
const PATH_LOSCORES: &str = "loscores.json";
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() {
let file_contents: String = fs::read_to_string("hiscores.json").unwrap();
let hiscores: Arc<Mutex<Vec<Entry>>> = Arc::new(Mutex::new(serde_json::from_str(&file_contents).unwrap()));
let file_contents: String = fs::read_to_string("loscores.json").unwrap();
let loscores: Arc<Mutex<Vec<Entry>>> = Arc::new(Mutex::new(serde_json::from_str(&file_contents).unwrap()));
let file_contents: String = fs::read_to_string("pingscores.json").unwrap();
let pingscores: Arc<Mutex<HashMap<String,(u64,u32)>>> = Arc::new(Mutex::new(serde_json::from_str(&file_contents).unwrap()));
let hiscore_clone1 = Arc::clone(&hiscores);
let loscore_clone1 = Arc::clone(&loscores);
let pingscore_clone1 = Arc::clone(&pingscores);
let (tx, rx) = mpsc::channel::<(Entry,Leaderboard)>();
tokio::spawn(
async move {
handle_hiscores(rx, hiscore_clone1, loscore_clone1, pingscore_clone1);
}
);
let pingscore_clone2 = Arc::clone(&pingscores);
tokio::spawn(
async move {
loop // write pingscores every 30s
async fn main() -> anyhow::Result<()>
{
fn read_file<T: for<'de> de::Deserialize<'de>>(
file_path: &str,
) -> anyhow::Result<Arc<RwLock<T>>>
{
sleep(Duration::from_millis(30000)).await;
let pingscores = pingscore_clone2.lock().unwrap();
let file_contents: String = serde_json::to_string(&pingscores.clone()).unwrap();
drop(pingscores);
let mut file = fs::OpenOptions::new().write(true).truncate(true).open("pingscores.json").unwrap();
file.write_all(file_contents.as_bytes()).unwrap();
file.flush().unwrap();
let file_contents: String = fs::read_to_string(file_path)?;
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>>)
{
let old_vec = std::mem::replace(&mut *vec, Vec::with_capacity(MAX_LEADERBOARD));
for e in old_vec
{
vec.push(e);
}
}
);
let state = AppState { tx,hiscores: Arc::clone(&hiscores),loscores: Arc::clone(&loscores), pingscores: Arc::clone(&pingscores)};
let app = Router::new()
.route("/", get(index))
.route("/ws", get(ws_handler))
.route("/leaderboard", get(leaderboard))
.with_state(state);
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 listener = tokio::net::TcpListener::bind("0.0.0.0:8084")
let (tx, rx) = mpsc::unbounded_channel::<LeaderboardUpdate>();
{
let (hiscores, loscores, pingscores) =
(hiscores.clone(), loscores.clone(), pingscores.clone());
tokio::spawn(async move {
handle_hiscores(rx, &hiscores, &loscores, &pingscores).await;
});
}
{
let pingscores = pingscores.clone();
tokio::spawn(async move {
// write pingscores every 30s
loop
{
sleep(Duration::from_secs(30)).await;
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
.unwrap();
.expect("failed to write pingscores");
}
});
}
let static_files = ServeDir::new("./static");
let app = Router::new()
.fallback_service(static_files)
.route("/ws", get(ws_handler))
.route("/ws-leaderboard", get(leaderboard_handler))
.with_state(AppState {
tx,
hiscores: Arc::clone(&hiscores),
loscores: Arc::clone(&loscores),
pingscores: Arc::clone(&pingscores),
});
let listener = tokio::net::TcpListener::bind("0.0.0.0:8084").await?;
println!("http://0.0.0.0:8084");
axum::serve(listener, app).await.unwrap();
}
axum::serve(listener, app).await?;
async fn index() -> Html<&'static str> {
Html(include_str!("../index.html"))
}
async fn leaderboard() -> Html<&'static str> {
Html(include_str!("../leaderboard.html"))
Ok(())
}
// receiver: 0 for hiscore, 1 for loscore, 2 for pingscore
fn handle_hiscores(rx: mpsc::Receiver<(Entry, Leaderboard)>, hiscores_arc: Arc<Mutex<Vec<Entry>>>, loscores_arc: Arc<Mutex<Vec<Entry>>>,pingscores_arc: Arc<Mutex<HashMap<String,(u64,u32)>>>,)
async fn handle_hiscores(
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<()>
{
let file_contents = {
let scoretable = &mut *scoretable_lock;
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(
&mut scoretable[index_to_insert_at],
Entry {
score,
person: name.to_string(),
},
);
if scoretable.len() < MAX_LEADERBOARD
{
scoretable.push(push_out);
}
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 = hiscores_arc.lock().unwrap();
hiscores.sort();
hiscores.reverse();
let file_contents: String = serde_json::to_string(&hiscores.clone()).unwrap();
drop(hiscores);
let mut file = fs::OpenOptions::new().write(true).truncate(true).open("hiscores.json").unwrap();
file.write_all(file_contents.as_bytes()).unwrap();
file.flush().unwrap();
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);
write_file(PATH_HISCORES, &file_contents)
.await
.expect("failed to write hiscores");
loop
{
match rx.recv()
let LeaderboardUpdate { name, update } = rx.recv().await.expect("channel error");
match update
{
Ok((new_entry,Leaderboard::Hiscores)) =>
LeaderboardUpdateType::Reset { hiscore_pingscore } =>
{
let mut hiscores = hiscores_arc.lock().unwrap();
if new_entry.score > hiscores[19].score {
println!("New hiscore {new_entry:?}");
hiscores.push(new_entry);
hiscores.sort();
hiscores.reverse();
hiscores.truncate(20);
let file_contents: String = serde_json::to_string(&hiscores.clone()).unwrap();
drop(hiscores);
let mut file = fs::OpenOptions::new().write(true).truncate(true).open("hiscores.json").unwrap();
file.write_all(file_contents.as_bytes()).unwrap();
file.flush().unwrap();
// Hiscore
update_scoretable(
"hiscore",
hiscores.write().await,
&name,
hiscore_pingscore,
PATH_HISCORES,
)
.await
.expect("failed to update hiscores");
// Pingscore
let mut pingscores = pingscores.write().await;
// pb
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}");
}
},
Ok((new_entry,Leaderboard::Loscores)) =>
{
let mut loscores = loscores_arc.lock().unwrap();
if new_entry.score > loscores[19].score {
println!("New loscore {new_entry:?}");
loscores.push(new_entry);
loscores.sort();
loscores.reverse();
loscores.truncate(20);
let file_contents: String = serde_json::to_string(&loscores.clone()).unwrap();
drop(loscores);
let mut file = fs::OpenOptions::new().write(true).truncate(true).open("loscores.json").unwrap();
file.write_all(file_contents.as_bytes()).unwrap();
file.flush().unwrap();
}
},
Ok((new_entry,Leaderboard::Pingscores)) =>
{
let name = new_entry.person;
let mut pingscores = pingscores_arc.lock().unwrap();
if new_entry.score > pingscores.get(&name).unwrap_or(&(0,0)).1 // pb
{
pingscores.entry(name.clone()).or_insert((0,0)).1 = new_entry.score;
println!("{name} new PB: {}",new_entry.score);
};
pingscores.entry(name.clone()).or_insert((0,0)).0 += 1; // reset count
pingscores.entry(name.to_string()).or_insert((0, 0)).0 += 1; // reset count
drop(pingscores);
}
Err(error) => println!("{error}"),
LeaderboardUpdateType::Increment { loscore } =>
{
update_scoretable(
"loscore",
loscores.write().await,
&name,
loscore,
PATH_LOSCORES,
)
.await
.expect("failed to update loscores");
}
}
}
}
async fn ws_handler(
async fn leaderboard_handler(
ws: WebSocketUpgrade,
State(state): State<AppState>,
) -> impl IntoResponse {
ws.on_upgrade(move |socket| {
let tx = state.tx.clone();
let hiscores = Arc::clone(&state.hiscores);
let loscores = Arc::clone(&state.loscores);
let pingscores = Arc::clone(&state.pingscores);
async move {
handle_socket(socket, tx, hiscores, loscores, pingscores).await;
) -> impl IntoResponse
{
ws.on_upgrade(|socket| async move {
handle_leaderboard(socket, &state.hiscores, &state.loscores, &state.pingscores).await;
})
}
async fn handle_leaderboard(
mut socket: WebSocket,
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;
}
}
}
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::Sender<(Entry,Leaderboard)>,
hiscores_arc: Arc<Mutex<Vec<Entry>>>,
loscores_arc: Arc<Mutex<Vec<Entry>>>,
pingscores_arc: Arc<Mutex<HashMap<String,(u64,u32)>>>,
) {
async fn handle_socket(mut socket: WebSocket, tx: &mpsc::UnboundedSender<LeaderboardUpdate>)
{
let mut value: u32 = 0;
let msg =
let Some(name) = socket.next().await
else
{
let hiscores = hiscores_arc.lock().unwrap();
let loscores = loscores_arc.lock().unwrap();
let pingscores = pingscores_arc.lock().unwrap();
json!({ "hiscores": &*hiscores, "loscores": &*loscores, "pingscores": &*pingscores}).to_string()
eprintln!("No username");
return;
};
let name_message = socket.next().await.unwrap().unwrap();
let name: String = match name_message
let name: Arc<str> = match name.expect("failed to recv socket msg")
{
Message::Text(text) => validate_name(text.to_string()),
_ => "anon".to_string(),
Message::Text(text) => Arc::from(validate_name(&text)),
_ => Arc::from("anon"),
};
println!("Client connected: {name}");
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) { // reset
let _ = tx.send((Entry{ person: name.clone(), score: value },Leaderboard::Hiscores)); //hiscores
let _ = tx.send((Entry{ person: name.clone(), score: value },Leaderboard::Pingscores)); //pingscores
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 {
name: name.clone(),
update: LeaderboardUpdateType::Reset {
hiscore_pingscore: value,
},
});
resets += 1;
value = 0
} // 1/3 chance of failing
else {
value = 0;
}
else
{
value += 1;
if prev == 0 {
let _ = tx.send((Entry{ person: name.clone(), score: resets },Leaderboard::Loscores));//loscores
if prev == 0
{
let _ = tx.send(LeaderboardUpdate {
name: name.clone(),
update: LeaderboardUpdateType::Increment { loscore: resets },
});
resets = 0;
}
}
@@ -229,34 +391,39 @@ async fn handle_socket
prev = value;
}
Ok(Message::Close(_)) => {
Ok(Message::Close(_)) =>
{
break;
}
_ => {}
_ =>
{}
}
}
}
fn validate_name(input: String) -> String {
fn validate_name(input: &str) -> &str
{
let input = input.trim();
if input == "null"
{
return "anon".to_string();
return "anon";
}
// Length check
if input.is_empty() || input.len() > 32 {
return "anon".to_string();
if input.is_empty() || input.len() > 32
{
return "anon";
}
// Allow only letters, numbers, _ and -
let re = Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap();
if re.is_match(input) {
input.to_string()
} else {
"anon".to_string()
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) => {