Initial DDGuesser. :3

main
BurnyLlama 2023-08-09 02:37:26 +02:00
parent ff6440a418
commit 51b743b858
14 changed files with 272 additions and 8 deletions

3
.gitignore vendored
View File

@ -21,4 +21,5 @@ Cargo.lock
/target
# Added by BurnyLlama
.env
.env
static/location-data

View File

@ -8,6 +8,7 @@ edition = "2021"
[dependencies]
dotenvy = "0.15.7"
rocket = { version = "0.5.0-rc.3", features = ["json"] }
rocket_dyn_templates = { version = "0.1.0-rc.2", features = ["tera"] }
serde = "1.0.183"
sqlx = { version = "0.7.1", features = [ "runtime-tokio-rustls", "postgres", "chrono", "uuid" ] }
tokio-test = "0.4.2"

4
Rocket.toml Normal file
View File

@ -0,0 +1,4 @@
[default]
address = "0.0.0.0"
port = 12345
template_dir = "templates"

68
src/app/mod.rs Normal file
View File

@ -0,0 +1,68 @@
use rocket::{response::Redirect, Route, State};
use rocket_dyn_templates::{context, Template};
use uuid::Uuid;
use crate::database::{
models::{
guess::Guess,
location::{self, Location},
},
DatabaseHandler,
};
#[get("/")]
fn landing() -> Template {
Template::render("landing", context! {})
}
#[get("/start-game")]
async fn start_game(db: &State<DatabaseHandler>) -> Result<Redirect, String> {
Location::get_random(db, true)
.await
.map(|location| Redirect::to(format!("/location/{}", location.id)))
.map_err(|_| {
"Sorry for this, but the game ran into an error. Please try again!".to_string()
})
}
#[get("/location/<id>")]
async fn location_via_id(db: &State<DatabaseHandler>, id: String) -> Result<Template, String> {
let uuid = match Uuid::parse_str(&id) {
Ok(uuid) => uuid,
Err(_) => return Err("Sorry, that id seems invalid!".to_string()),
};
let location = match Location::get_by_id(db, &uuid).await {
Ok(location) => location,
Err(_) => return Err("Sorry, that location seems invalid!".to_string()),
};
Ok(Template::render("location", context! { location }))
}
#[get("/location/<id>?<guess>")]
async fn location_guess_via_id(
db: &State<DatabaseHandler>,
id: String,
guess: String,
) -> Result<Template, String> {
let uuid = match Uuid::parse_str(&id) {
Ok(uuid) => uuid,
Err(_) => return Err("Sorry, that id seems invalid!".to_string()),
};
let location = match Location::get_by_id(db, &uuid).await {
Ok(location) => location,
Err(_) => return Err("Sorry, that location seems invalid!".to_string()),
};
let is_guess_correct = guess == location.map;
let guess_with_results = Guess::create(guess, is_guess_correct);
Ok(Template::render(
"location",
context! { location, guess: guess_with_results },
))
}
pub fn get_all_routes() -> Vec<Route> {
routes![landing, start_game, location_via_id, location_guess_via_id]
}

View File

@ -5,7 +5,7 @@ use std::{env, error::Error};
pub mod models;
pub struct DatabaseHandler {
pool: PgPool,
pub pool: PgPool,
}
impl DatabaseHandler {

View File

@ -0,0 +1,19 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Guess {
pub map: String,
pub result: String,
}
impl Guess {
pub fn create(map: String, is_correct: bool) -> Guess {
Guess {
map,
result: match is_correct {
true => "correct".to_string(),
false => "incorrect".to_string(),
},
}
}
}

View File

@ -4,15 +4,31 @@ use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Location {
id: Uuid,
map: String,
pub id: Uuid,
pub map: String,
pub has_entities: bool,
}
impl Location {
pub async fn get_random(db: &DatabaseHandler) -> Result<Location, sqlx::Error> {
/// Get a random location. Use the `has_entities` option to specify whether the location should be returned with entities. No entities = harder.
pub async fn get_random(
db: &DatabaseHandler,
has_entities: bool,
) -> Result<Location, sqlx::Error> {
sqlx::query_as!(
Location,
"SELECT id, map FROM locations ORDER BY RANDOM() LIMIT 1"
"SELECT id, map, has_entities FROM locations WHERE has_entities = $1 ORDER BY RANDOM() LIMIT 1",
has_entities
)
.fetch_one(&db.pool)
.await
}
pub async fn get_by_id(db: &DatabaseHandler, id: &Uuid) -> Result<Location, sqlx::Error> {
sqlx::query_as!(
Location,
"SELECT id, map, has_entities FROM locations WHERE id = $1",
id
)
.fetch_one(&db.pool)
.await
@ -52,7 +68,7 @@ mod tests {
async fn test() {
let db = get_db().await;
let location = match Location::get_random(&db).await {
let location = match Location::get_random(&db, false).await {
Ok(location) => location,
Err(err) => panic!("Error while getting random location! {:?}", err),
};

View File

@ -1 +1,2 @@
pub mod guess;
pub mod location;

View File

@ -1,9 +1,13 @@
#[macro_use]
extern crate rocket;
mod app;
mod database;
use app::get_all_routes;
use database::DatabaseHandler;
use rocket::fs::FileServer;
use rocket_dyn_templates::Template;
#[rocket::main]
async fn main() {
@ -15,7 +19,14 @@ async fn main() {
),
};
match rocket::build().manage(database).launch().await {
match rocket::build()
.attach(Template::fairing())
.manage(database)
.mount("/", get_all_routes())
.mount("/static", FileServer::from("./static"))
.launch()
.await
{
Ok(_) => (),
Err(err) => println!("Encountered an error while starting rocket:\n{}", err),
}

83
static/ddguesser.css Normal file
View File

@ -0,0 +1,83 @@
body.ddguesser {
display: grid;
grid-template-areas: 'nav' 'main';
grid-template-columns: 1fr;
grid-template-rows: auto 1fr;
}
body.ddguesser > nav {
background-color: #1F1F47;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
body.ddguesser > main {
display: flex;
flex-direction: column;
justify-content: center;
width: min(90ch, 90%);
place-self: start center;
margin-bottom: 5vh;
}
header {
text-align: center;
}
header > h1 {
font-size: 3rem;
}
img#meow {
width: min(45ch, 50%);
aspect-ratio: 1 / 1;
margin: 0 auto 2rem auto;
}
a#start-game {
width: max-content;
margin: 0 auto;
}
form {
width: 100%;
display: flex;
}
form > input[type=text] {
color: #FAFAFF;
flex-grow: 1;
}
.guess {
font-size: 1.5rem;
/* text-align: center; */
}
span.correct {
color: #64DB4F;
}
span.incorrect {
color: #E04F53;
}
.woof {
position: relative;
width: 100%;
}
.woof > img {
width: 100%;
}
.woof > p {
position: absolute;
bottom: 1rem;
user-select: none;
translate: 50%;
right: 50%;
padding: 1rem 2rem;
background-color: #0008;
}

2
static/nebulosa.css Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,11 @@
{% extends "template/app" %}
{% block main %}
<header>
<h1>Welcome to DDGuesser!</h1>
<p>The game where you guess the DDNet map!</p>
</header>
<img src="https://cataas.com/cat/says/DDGuesser?type=square" alt="Random image of a cat, with the caption 'DDGuesser'."
loading="lazy" id="meow">
<a href="/start-game" class="btn primary raised" id="start-game">Play game!</a>
{% endblock main %}

View File

@ -0,0 +1,20 @@
{% extends "template/app" %}
{% block main %}
<header>
<h1>Which map is this image from?</h1>
</header>
{% if guess %}
<div class="woof">
<img src="/static/location-data/{{ location.id }}.png" class="map" loading="lazy">
<p class="guess">Your guess {{ guess.map }} is <span class="{{ guess.result }}">{{ guess.result }}</span>!</p>
</div>
{% else %}
<img src="/static/location-data/{{ location.id }}.png" class="map" loading="lazy">
{% endif %}
<form action="/location/{{ location.id }}" method="get">
<label for="guess">Enter map to guess for:</label>
<input type="text" name="guess" id="guess">
<button class="btn primary outline" type="submit">Guess!</button>
</form>
{% endblock main %}

View File

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" type="text/css" href="/static/nebulosa.css">
<link rel="stylesheet" type="text/css" href="/static/ddguesser.css">
{% block head %}
<title>DDGuesser</title>
{% endblock head %}
</head>
<body class="ddguesser">
<nav>
<a class="btn primary textonly" href="/">DDGuesser</a>
<a class="btn primary textonly" href="/about">About</a>
<a class="btn primary textonly" href="/help">Help</a>
</nav>
<main>
{% block main %}
<p>Hello world!</p>
{% endblock main %}
</main>
</body>
</html>