24 Commits

Author SHA1 Message Date
deadvey 219f4086aa added local play by pressing cancel on the JS prompt
removed the uncessesary null check in the validate_name
2026-06-02 12:14:14 +01: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 536 additions and 284 deletions
+2
View File
@@ -2,3 +2,5 @@
hiscores.json hiscores.json
loscores.json loscores.json
pingscores.json pingscores.json
.*
address.js
Binary file not shown.
Generated
+1
View File
@@ -103,6 +103,7 @@ dependencies = [
name = "button" name = "button"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"axum", "axum",
"futures", "futures",
"rand 0.10.1", "rand 0.10.1",
+25 -4
View File
@@ -4,11 +4,32 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
axum = {version="0.8.9",features=["ws"]} anyhow = "1.0.102"
axum = { version = "0.8.9", features = ["ws"] }
futures = "0.3.32" futures = "0.3.32"
rand = "0.10.1" rand = "0.10.1"
regex = "1.12.3" regex = "1.12.3"
serde = {version="1.0.228",features=["derive"]} serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
tokio = {version="1.52.3",features=["full"]} tokio = { version = "1.52.3", features = ["full"] }
tower-http = {version="0.6.11",features=["fs"]} 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]}
+1
View File
@@ -0,0 +1 @@
hard_tabs = true
+370 -177
View File
@@ -1,227 +1,430 @@
use axum::{ // TODO rename pingscores userscores
extract::ws::{Message, WebSocket, WebSocketUpgrade}, use std::
response::{Html, IntoResponse}, {
routing::get, collections::HashMap,
Router, fs,
io::Write,
ops::{Deref, DerefMut},
sync::Arc,
}; };
use std::collections::HashMap;
use axum::extract::State; use axum::
{
Router,
extract::
{
State,
ws::{Message, WebSocket, WebSocketUpgrade},
},
response::IntoResponse,
routing::get,
};
use tokio::
{
sync::{Mutex, mpsc},
time::{Duration, sleep},
};
use tower_http::services::ServeDir;
use futures::StreamExt as _;
use rand::random_bool; use rand::random_bool;
use futures::stream::StreamExt; use serde::{Deserialize, Serialize, de};
use serde_json;
use serde_json::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; use regex::Regex;
#[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,
} }
enum Leaderboard
{
Hiscores,
Loscores,
Pingscores,
}
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState
tx: mpsc::Sender<(Entry,Leaderboard)>, {
tx: mpsc::Sender<LeaderboardUpdate>,
hiscores: Arc<Mutex<Vec<Entry>>>, hiscores: Arc<Mutex<Vec<Entry>>>,
loscores: Arc<Mutex<Vec<Entry>>>, loscores: Arc<Mutex<Vec<Entry>>>,
pingscores: Arc<Mutex<HashMap<String,(u64,u32)>>>, // u64 is reset count and u32 is PB pingscores: Arc<Mutex<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;
#[tokio::main] #[tokio::main]
async fn main() { async fn main() -> anyhow::Result<()>
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())); fn read_file<T: for<'de> de::Deserialize<'de>>
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())); file_path: &str,
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())); -> anyhow::Result<Arc<Mutex<T>>>
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
{ {
sleep(Duration::from_millis(30000)).await; let file_contents: String = fs::read_to_string(file_path)?;
let pingscores = pingscore_clone2.lock().unwrap(); Ok(Arc::new(Mutex::new(serde_json::from_str(&file_contents)?)))
let file_contents: String = serde_json::to_string(&pingscores.clone()).unwrap(); }
/// 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 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, 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.lock().await;
let file_contents: String = serde_json::to_string(&pingscores.clone())
.expect("failed to serialize pingscores");
drop(pingscores); drop(pingscores);
let mut file = fs::OpenOptions::new().write(true).truncate(true).open("pingscores.json").unwrap(); let mut file = fs::OpenOptions::new()
file.write_all(file_contents.as_bytes()).unwrap(); .write(true)
file.flush().unwrap(); .truncate(true)
.open(PATH_PINGSCORES)
.expect("failed to open pingscores file");
file.write_all(file_contents.as_bytes())
.expect("failed to write pingscores");
drop(file);
} }
});
} }
);
let state = AppState { tx,hiscores: Arc::clone(&hiscores),loscores: Arc::clone(&loscores), pingscores: Arc::clone(&pingscores)}; 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(state); .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") let listener = tokio::net::TcpListener::bind("0.0.0.0:8084").await?;
.await
.unwrap();
println!("http://0.0.0.0:8084"); println!("http://0.0.0.0:8084");
axum::serve(listener, app).await.unwrap(); axum::serve(listener, app).await?;
}
async fn index() -> Html<&'static str> { Ok(())
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
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::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>
(
score_name: &str,
mut scoretable_lock: G,
name: &str,
score: u32,
file_path: &str,
)
-> anyhow::Result<()>
{
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);
}
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
{
println!("New {score_name} {score} by {name}");
scoretable.push
(Entry {
score,
person: name.to_string(),
});
}
Ok(())
}
// Panic galore // Panic galore
let mut hiscores = hiscores_arc.lock().unwrap(); let mut hiscores_lock = hiscores.lock().await;
hiscores.sort(); hiscores_lock.sort();
hiscores.reverse(); hiscores_lock.reverse();
let file_contents: String = serde_json::to_string(&hiscores.clone()).unwrap(); let file_contents: String =
drop(hiscores); serde_json::to_string(&hiscores_lock.clone()).expect("failed to serialize hiscores");
let mut file = fs::OpenOptions::new().write(true).truncate(true).open("hiscores.json").unwrap(); drop(hiscores_lock);
file.write_all(file_contents.as_bytes()).unwrap(); let mut file = fs::OpenOptions::new()
file.flush().unwrap(); .write(true)
.truncate(true)
.open(PATH_HISCORES)
.expect("failed to open hiscores");
file.write_all(file_contents.as_bytes())
.expect("failed to write hiscores");
drop(file);
loop 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(); // Hiscore
if new_entry.score > hiscores[19].score { update_scoretable
println!("New hiscore {new_entry:?}"); (
hiscores.push(new_entry); "hiscore",
hiscores.sort(); hiscores.lock().await,
hiscores.reverse(); &name,
hiscores.truncate(20); hiscore_pingscore,
let file_contents: String = serde_json::to_string(&hiscores.clone()).unwrap(); PATH_HISCORES,
drop(hiscores); )
let mut file = fs::OpenOptions::new().write(true).truncate(true).open("hiscores.json").unwrap(); .expect("failed to update hiscores");
file.write_all(file_contents.as_bytes()).unwrap();
file.flush().unwrap(); // Pingscore
let mut pingscores = pingscores.lock().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}");
} }
}, pingscores.entry(name.to_string()).or_insert((0, 0)).0 += 1; // reset count
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
drop(pingscores); drop(pingscores);
} }
Err(error) => println!("{error}"),
LeaderboardUpdateType::Increment { loscore } =>
{
update_scoretable
(
"loscore",
loscores.lock().await,
&name,
loscore,
PATH_LOSCORES,
)
.expect("failed to update loscores");
}
} }
} }
} }
async fn ws_handler( async fn leaderboard_handler
(
ws: WebSocketUpgrade, ws: WebSocketUpgrade,
State(state): State<AppState>, State(state): State<AppState>
) -> impl IntoResponse { )
ws.on_upgrade(move |socket| { -> impl IntoResponse
let tx = state.tx.clone(); {
let hiscores = Arc::clone(&state.hiscores); ws.on_upgrade
let loscores = Arc::clone(&state.loscores); (|socket| async move {
let pingscores = Arc::clone(&state.pingscores); handle_leaderboard
async move { (
handle_socket(socket, tx, hiscores, loscores, pingscores).await; socket,
&state.hiscores,
&state.loscores,
&state.pingscores,
)
.await;
})
}
async fn handle_leaderboard
(
mut socket: WebSocket,
hiscores: &Mutex<Vec<Entry>>,
loscores: &Mutex<Vec<Entry>>,
pingscores: &Mutex<HashMap<String, (u64, u32)>>,
)
{
match socket.next().await
{
Some(Ok(Message::Text(selection))) =>
{
let msg;
match selection.as_str()
{
"0" => // all the leaderboards
{
msg =
{
json!
({
"hiscores": &*hiscores.lock().await,
"loscores": &*loscores.lock().await,
"pingscores": &*pingscores.lock().await
})
.to_string()
};
},
"1" => // just the hiscores table
{
msg = json! ({ "hiscores": &*hiscores.lock().await }).to_string()
},
"2" => // just the loscores table
{
msg = json! ({ "loscores": &*hiscores.lock().await }).to_string()
},
"3" => // just the pingscores table
{
msg = json! ({ "pingscores": &*hiscores.lock().await }).to_string()
},
_ => { msg = "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 async fn handle_socket
( (
mut socket: WebSocket, mut socket: WebSocket,
tx: mpsc::Sender<(Entry,Leaderboard)>, tx: &mpsc::Sender<LeaderboardUpdate>,
hiscores_arc: Arc<Mutex<Vec<Entry>>>, )
loscores_arc: Arc<Mutex<Vec<Entry>>>, {
pingscores_arc: Arc<Mutex<HashMap<String,(u64,u32)>>>,
) {
let mut value: u32 = 0; let mut value: u32 = 0;
let msg =
let Some(name) = socket.next().await else
{ {
let hiscores = hiscores_arc.lock().unwrap(); eprintln!("No username");
let loscores = loscores_arc.lock().unwrap(); return;
let pingscores = pingscores_arc.lock().unwrap();
json!({ "hiscores": &*hiscores, "loscores": &*loscores, "pingscores": &*pingscores}).to_string()
}; };
let name_message = socket.next().await.unwrap().unwrap(); let name: Arc<str> = match name.expect("failed to recv socket msg")
let name: String = match name_message
{ {
Message::Text(text) => validate_name(text.to_string()), Message::Text(text) =>
_ => "anon".to_string(), {
Arc::from(validate_name(&text))
}
_ => Arc::from("anon"),
}; };
println!("Client connected: {name}"); println!("Client connected: {name}");
let mut resets: u32 = 0; let mut resets: u32 = 0;
let mut prev: u32 = 0; let mut prev: u32 = 0;
let _ = socket while let Some(msg) = socket.next().await
.send(Message::Text(msg.into())) {
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,
},
})
.await; .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
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((Entry{ person: name.clone(), score: resets },Leaderboard::Loscores));//loscores {
let _ = tx
.send
(LeaderboardUpdate {
name: name.clone(),
update: LeaderboardUpdateType::Increment { loscore: resets },
})
.await;
resets = 0; resets = 0;
} }
} }
@@ -229,7 +432,8 @@ async fn handle_socket
prev = value; prev = value;
} }
Ok(Message::Close(_)) => { Ok(Message::Close(_)) =>
{
break; break;
} }
@@ -238,25 +442,14 @@ async fn handle_socket
} }
} }
fn validate_name(input: &str) -> &str {
fn validate_name(input: String) -> String {
let input = input.trim(); let input = input.trim();
if input == "null"
{
return "anon".to_string();
}
// Length check
if input.is_empty() || input.len() > 32 {
return "anon".to_string();
}
// Allow only letters, numbers, _ and - // Allow only letters, numbers, _ and -
let re = Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap(); let re = Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap();
// Length check
if re.is_match(input) { if input.is_empty() || input.len() > 32 || !re.is_match(input)
input.to_string() {
} else { return "anon";
"anon".to_string()
} }
input
} }
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;
}
+91
View File
@@ -0,0 +1,91 @@
<!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 src="/address.js"> </script>
<script>
let score = 0;
let locally = false;
// get the username
let name = validate_data(prompt("Nickname for the leaderboard\nOnly alphanumeric, - or _ please\nPress cancel to just play locally.","anon") ?? set_local());
let ws = "local";
if (!locally)
{
ws = new WebSocket(`ws://${ADDRESS}:8084/ws`);
};
ws.onopen = (event) => {
if (!locally) { ws.send(name); };
}
ws.onmessage = (event) => {
console.log(event.data);
score = validate_data(event.data);
}
const ws_leaderboard = new WebSocket(`ws://${ADDRESS}:8084/ws-leaderboard`); // download the 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);
};
function send_click()
{
if (!locally)
{
ws.send("");
if (score !== null)
{
document.getElementById("button").textContent=score;
score = null;
}
}
else // locally = true
{
if (Math.random() < (1/3)) { score = 0; } // fail
else { score += 1; } // success
document.getElementById("button").textContent=score;
}
}
function set_local()
{
score = 0
locally = true
return "anon"
}
function get_rand(min, max) {
return Math.round(Math.random() * (max - min) + min);
}
window.addEventListener('beforeunload', () => {
ws.close();
}, !locally);
function validate_data(data) { // Only allow a-z, A-Z, 0-9, - and _ characters, sorry Ramón
const regex = new RegExp("^[a-zA-Z0-9_-]+$");
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,12 @@
<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> <script src="/address.js"></script>
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://${ADDRESS}: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) => {