14 Commits

Author SHA1 Message Date
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
14 changed files with 357 additions and 193 deletions
+1
View File
@@ -2,3 +2,4 @@
hiscores.json hiscores.json
loscores.json loscores.json
pingscores.json pingscores.json
.*
Binary file not shown.
Generated
+39
View File
@@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.102" version = "1.0.102"
@@ -98,6 +107,7 @@ dependencies = [
"axum", "axum",
"futures", "futures",
"rand 0.10.1", "rand 0.10.1",
"regex",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
@@ -668,6 +678,35 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.23" version = "1.0.23"
+1
View File
@@ -8,6 +8,7 @@ anyhow = "1.0.102"
axum = { version = "0.8.9", features = ["ws"] } 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"
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"] }
+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]}
+205 -91
View File
@@ -1,4 +1,6 @@
use std::{ // TODO rename pingscores userscores
use std::
{
collections::HashMap, collections::HashMap,
fs, fs,
io::Write, io::Write,
@@ -6,44 +8,53 @@ use std::{
sync::Arc, sync::Arc,
}; };
use axum::{ use axum::
{
Router, Router,
extract::{ extract::
{
State, State,
ws::{Message, WebSocket, WebSocketUpgrade}, ws::{Message, WebSocket, WebSocketUpgrade},
}, },
response::{Html, IntoResponse}, response::IntoResponse,
routing::get, routing::get,
}; };
use tokio::
{
sync::{Mutex, mpsc},
time::{Duration, sleep},
};
use tower_http::services::ServeDir;
use futures::StreamExt as _; use futures::StreamExt as _;
use rand::random_bool; 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 regex::Regex;
sync::{Mutex, mpsc},
time::{Duration, sleep},
};
#[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>, 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
} }
struct LeaderboardUpdate { struct LeaderboardUpdate
{
name: Arc<str>, name: Arc<str>,
update: LeaderboardUpdateType, update: LeaderboardUpdateType,
} }
enum LeaderboardUpdateType { enum LeaderboardUpdateType
{
Reset { hiscore_pingscore: u32 }, Reset { hiscore_pingscore: u32 },
Increment { loscore: u32 }, Increment { loscore: u32 },
} }
@@ -57,19 +68,25 @@ const PATH_PINGSCORES: &str = "pingscores.json";
const MAX_LEADERBOARD: usize = 20; const MAX_LEADERBOARD: usize = 20;
#[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<Mutex<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(Mutex::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);
} }
} }
@@ -81,20 +98,22 @@ async fn main() -> anyhow::Result<()> {
let pingscores: Arc<Mutex<HashMap<String, (u64, u32)>>> = read_file(PATH_PINGSCORES)?; let pingscores: Arc<Mutex<HashMap<String, (u64, u32)>>> = read_file(PATH_PINGSCORES)?;
let (tx, rx) = mpsc::channel::<LeaderboardUpdate>(1024); let (tx, rx) = mpsc::channel::<LeaderboardUpdate>(1024);
{ {
let (hiscores, loscores, pingscores) = let (hiscores, loscores, pingscores) =
(hiscores.clone(), loscores.clone(), pingscores.clone()); (hiscores.clone(), loscores.clone(), pingscores.clone());
tokio::spawn(async move { tokio::spawn
(async move {
handle_hiscores(rx, &hiscores, &loscores, &pingscores).await; handle_hiscores(rx, &hiscores, &loscores, &pingscores).await;
}); });
} }
{ {
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.lock().await;
let file_contents: String = serde_json::to_string(&pingscores.clone()) let file_contents: String = serde_json::to_string(&pingscores.clone())
@@ -112,11 +131,13 @@ async fn main() -> anyhow::Result<()> {
}); });
} }
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),
loscores: Arc::clone(&loscores), loscores: Arc::clone(&loscores),
@@ -131,41 +152,41 @@ 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::Receiver<LeaderboardUpdate>,
hiscores: &Mutex<Vec<Entry>>, hiscores: &Mutex<Vec<Entry>>,
loscores: &Mutex<Vec<Entry>>, loscores: &Mutex<Vec<Entry>>,
pingscores: &Mutex<HashMap<String, (u64, u32)>>, pingscores: &Mutex<HashMap<String, (u64, u32)>>,
) { )
fn update_scoretable<G: Deref<Target = Vec<Entry>> + DerefMut>( {
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 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
(
&mut scoretable[index_to_insert_at], &mut scoretable[index_to_insert_at],
Entry { Entry
{
score, score,
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);
} }
@@ -177,9 +198,12 @@ async fn handle_hiscores(
.open(file_path)?; .open(file_path)?;
file.write_all(file_contents.as_bytes())?; file.write_all(file_contents.as_bytes())?;
file.flush()?; file.flush()?;
} else if scoretable.len() < MAX_LEADERBOARD { }
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(),
}); });
@@ -204,13 +228,17 @@ async fn handle_hiscores(
.expect("failed to write hiscores"); .expect("failed to write hiscores");
drop(file); 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.lock().await,
&name, &name,
@@ -222,7 +250,8 @@ async fn handle_hiscores(
// Pingscore // Pingscore
let mut pingscores = pingscores.lock().await; let mut pingscores = pingscores.lock().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,8 +259,10 @@ async fn handle_hiscores(
drop(pingscores); drop(pingscores);
} }
LeaderboardUpdateType::Increment { loscore } => { LeaderboardUpdateType::Increment { loscore } =>
update_scoretable( {
update_scoretable
(
"loscore", "loscore",
loscores.lock().await, loscores.lock().await,
&name, &name,
@@ -244,11 +275,18 @@ async fn handle_hiscores(
} }
} }
async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> impl IntoResponse { async fn leaderboard_handler
ws.on_upgrade(|socket| async move { (
handle_socket( ws: WebSocketUpgrade,
State(state): State<AppState>
)
-> impl IntoResponse
{
ws.on_upgrade
(|socket| async move {
handle_leaderboard
(
socket, socket,
&state.tx,
&state.hiscores, &state.hiscores,
&state.loscores, &state.loscores,
&state.pingscores, &state.pingscores,
@@ -256,35 +294,93 @@ async fn ws_handler(ws: WebSocketUpgrade, State(state): State<AppState>) -> impl
.await; .await;
}) })
} }
async fn handle_leaderboard
async fn handle_socket( (
mut socket: WebSocket, mut socket: WebSocket,
tx: &mpsc::Sender<LeaderboardUpdate>,
hiscores: &Mutex<Vec<Entry>>, hiscores: &Mutex<Vec<Entry>>,
loscores: &Mutex<Vec<Entry>>, loscores: &Mutex<Vec<Entry>>,
pingscores: &Mutex<HashMap<String, (u64, u32)>>, 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
(
mut socket: WebSocket,
tx: &mpsc::Sender<LeaderboardUpdate>,
)
{
let mut value: u32 = 0; let mut value: u32 = 0;
let msg = { let Some(name) = socket.next().await else
json!({ {
"hiscores": &*hiscores.lock().await, eprintln!("No username");
"loscores": &*loscores.lock().await,
"pingscores": &*pingscores.lock().await
})
.to_string()
};
let Some(name) = socket.next().await else {
eprintln!("user gave 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() Message::Text(text) =>
&& validate_name(&text) =>
{ {
Arc::from(text.into_boxed_str()) Arc::from(validate_name(&text))
} }
_ => Arc::from("anon"), _ => Arc::from("anon"),
}; };
@@ -294,17 +390,22 @@ 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,
}, },
}) })
@@ -312,12 +413,14 @@ async fn handle_socket(
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 let _ = tx
.send(LeaderboardUpdate { .send
(LeaderboardUpdate {
name: name.clone(), name: name.clone(),
update: LeaderboardUpdateType::Increment { loscore: resets }, update: LeaderboardUpdateType::Increment { loscore: resets },
}) })
@@ -329,7 +432,8 @@ async fn handle_socket(
prev = value; prev = value;
} }
Ok(Message::Close(_)) => { Ok(Message::Close(_)) =>
{
break; break;
} }
@@ -338,13 +442,23 @@ async fn handle_socket(
} }
} }
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 -
let re = Regex::new(r"^[a-zA-Z0-9_-]+$").unwrap();
if re.is_match(input) {
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) => {