Initial DDGuesser. :3
This commit is contained in:
parent
ff6440a418
commit
51b743b858
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -21,4 +21,5 @@ Cargo.lock
|
|||
/target
|
||||
|
||||
# Added by BurnyLlama
|
||||
.env
|
||||
.env
|
||||
static/location-data
|
|
@ -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
4
Rocket.toml
Normal file
|
@ -0,0 +1,4 @@
|
|||
[default]
|
||||
address = "0.0.0.0"
|
||||
port = 12345
|
||||
template_dir = "templates"
|
68
src/app/mod.rs
Normal file
68
src/app/mod.rs
Normal 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]
|
||||
}
|
|
@ -5,7 +5,7 @@ use std::{env, error::Error};
|
|||
pub mod models;
|
||||
|
||||
pub struct DatabaseHandler {
|
||||
pool: PgPool,
|
||||
pub pool: PgPool,
|
||||
}
|
||||
|
||||
impl DatabaseHandler {
|
||||
|
|
19
src/database/models/guess.rs
Normal file
19
src/database/models/guess.rs
Normal 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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
pub mod guess;
|
||||
pub mod location;
|
||||
|
|
13
src/main.rs
13
src/main.rs
|
@ -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
83
static/ddguesser.css
Normal 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
2
static/nebulosa.css
Normal file
File diff suppressed because one or more lines are too long
11
templates/landing.html.tera
Normal file
11
templates/landing.html.tera
Normal 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 %}
|
20
templates/location.html.tera
Normal file
20
templates/location.html.tera
Normal 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 %}
|
27
templates/template/app.html.tera
Normal file
27
templates/template/app.html.tera
Normal 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>
|
Loading…
Reference in New Issue
Block a user