initial commit
commit
569a7ec7ab
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
*.pem
|
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "worsdle_gemini"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
|
glob = "0.3.1"
|
||||||
|
serde = { version = "1.0.208", features = ["derive"] }
|
||||||
|
tera = "1.20.0"
|
||||||
|
tokio = { version = "1.39.2", features = ["full"] }
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
uuid = { version = "1.10.0", features = ["v4", "fast-rng"] }
|
||||||
|
windmark = { version = "0.3.11", features = ["response-macros", "logger"] }
|
|
@ -0,0 +1,35 @@
|
||||||
|
use crate::status::Status;
|
||||||
|
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct Character {
|
||||||
|
pub char: char,
|
||||||
|
pub status: Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Clone)]
|
||||||
|
pub struct Attempt {
|
||||||
|
pub chars: Vec<Character>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Attempt {
|
||||||
|
pub fn from_string(text: &String, answer: &String) -> Self {
|
||||||
|
let mut output = vec![];
|
||||||
|
for (n, ch) in text.to_uppercase().chars().enumerate() {
|
||||||
|
let answer_nth = answer
|
||||||
|
.chars()
|
||||||
|
.nth(n)
|
||||||
|
.expect("Answer and text has different length");
|
||||||
|
let status = if ch == answer_nth {
|
||||||
|
Status::Found
|
||||||
|
} else if answer.contains(ch) {
|
||||||
|
Status::Almost
|
||||||
|
} else {
|
||||||
|
Status::NotFound
|
||||||
|
};
|
||||||
|
output.push(Character { char: ch, status })
|
||||||
|
}
|
||||||
|
Attempt { chars: output }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,131 @@
|
||||||
|
use crate::attempt::Attempt;
|
||||||
|
use crate::status::Status;
|
||||||
|
use crate::words::get_word_of_the_day;
|
||||||
|
use std::collections::{HashMap, VecDeque};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub type Board = [Option<Attempt>; 6];
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum InsertionStatus {
|
||||||
|
Ok,
|
||||||
|
LengthError,
|
||||||
|
GameOver,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum GameStatus {
|
||||||
|
Playing,
|
||||||
|
Win(String),
|
||||||
|
Fail,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Game {
|
||||||
|
pub uuid: Uuid,
|
||||||
|
pub attempt_index: usize,
|
||||||
|
pub answer: String,
|
||||||
|
pub board: Board,
|
||||||
|
pub status: GameStatus,
|
||||||
|
pub last_insertion_status: InsertionStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Game {
|
||||||
|
pub async fn new() -> Self {
|
||||||
|
Game {
|
||||||
|
uuid: Uuid::new_v4(),
|
||||||
|
attempt_index: 0,
|
||||||
|
answer: get_word_of_the_day().await,
|
||||||
|
board: Default::default(),
|
||||||
|
status: GameStatus::Playing,
|
||||||
|
last_insertion_status: InsertionStatus::Ok,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_attempt(&mut self, word: &String) -> InsertionStatus {
|
||||||
|
let word = word.to_uppercase();
|
||||||
|
|
||||||
|
let win = word.eq(&self.answer);
|
||||||
|
|
||||||
|
let insertion_status = match (self.attempt_index, word.chars().count(), &self.status) {
|
||||||
|
(0..=5, 5, &GameStatus::Playing) => {
|
||||||
|
let new_attempt = Attempt::from_string(&word, &self.answer);
|
||||||
|
self.board[self.attempt_index] = Some(new_attempt);
|
||||||
|
self.attempt_index += 1;
|
||||||
|
|
||||||
|
match (win, self.attempt_index) {
|
||||||
|
(true, _) => {
|
||||||
|
self.status = GameStatus::Win(self.generate_result_pattern());
|
||||||
|
InsertionStatus::GameOver
|
||||||
|
}
|
||||||
|
(false, 6) => {
|
||||||
|
self.status = GameStatus::Fail;
|
||||||
|
InsertionStatus::GameOver
|
||||||
|
}
|
||||||
|
(_, _) => InsertionStatus::Ok,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(0..=5, _, &GameStatus::Playing) => InsertionStatus::LengthError,
|
||||||
|
(_, _, &GameStatus::Win(_) | &GameStatus::Fail) => InsertionStatus::GameOver,
|
||||||
|
_ => todo!(),
|
||||||
|
};
|
||||||
|
self.last_insertion_status = insertion_status.clone();
|
||||||
|
insertion_status
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_result_pattern(&self) -> String {
|
||||||
|
let mut output = String::new();
|
||||||
|
for attempt in &self.board {
|
||||||
|
match attempt {
|
||||||
|
Some(att) => {
|
||||||
|
for ch in &att.chars {
|
||||||
|
output.push(match ch.status {
|
||||||
|
Status::Found => '🟩',
|
||||||
|
Status::Almost => '🟨',
|
||||||
|
Status::NotFound => '⬛',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
output.push('\n');
|
||||||
|
}
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SlotManager {
|
||||||
|
max_length: usize,
|
||||||
|
slots: HashMap<Uuid, Game>,
|
||||||
|
order: VecDeque<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SlotManager {
|
||||||
|
pub fn new(max_length: usize) -> Self {
|
||||||
|
SlotManager {
|
||||||
|
max_length,
|
||||||
|
slots: HashMap::new(),
|
||||||
|
order: VecDeque::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new_game(&mut self) -> &mut Game {
|
||||||
|
if self.order.len() >= self.max_length {
|
||||||
|
if let Some(oldest_id) = self.order.pop_front() {
|
||||||
|
self.slots.remove(&oldest_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_game = Game::new().await;
|
||||||
|
let new_id = new_game.uuid;
|
||||||
|
self.slots.insert(new_id, new_game);
|
||||||
|
self.order.push_back(new_id);
|
||||||
|
|
||||||
|
// This should be safe because the game is inserted previously
|
||||||
|
self.slots.get_mut(&new_id).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_game(&mut self, id: Uuid) -> Option<&mut Game> {
|
||||||
|
self.slots.get_mut(&id)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,182 @@
|
||||||
|
mod attempt;
|
||||||
|
mod gameslot;
|
||||||
|
mod status;
|
||||||
|
mod words;
|
||||||
|
|
||||||
|
use crate::attempt::Attempt;
|
||||||
|
use crate::gameslot::{Board, Game};
|
||||||
|
use crate::words::get_word_of_the_day;
|
||||||
|
use gameslot::{GameStatus, InsertionStatus, SlotManager};
|
||||||
|
use tera::Tera;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
static TERA: OnceCell<Tera> = OnceCell::const_new();
|
||||||
|
|
||||||
|
async fn render<S: AsRef<str>>(name: S, context: &tera::Context) -> Result<String, ()> {
|
||||||
|
let tera = TERA
|
||||||
|
.get_or_init(|| async {
|
||||||
|
let mut tera = Tera::default();
|
||||||
|
for path in glob::glob("templates/*").unwrap().flatten() {
|
||||||
|
let raw_path = path.clone();
|
||||||
|
let filename = raw_path
|
||||||
|
.file_name()
|
||||||
|
.unwrap() // This should be safe (?
|
||||||
|
.to_str()
|
||||||
|
.unwrap() // ._.?
|
||||||
|
.split('.')
|
||||||
|
.next();
|
||||||
|
tera.add_template_file(path, filename).unwrap()
|
||||||
|
}
|
||||||
|
tera
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
tera.render(name.as_ref(), context).map_err(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
static GAMES: OnceCell<Mutex<SlotManager>> = OnceCell::const_new();
|
||||||
|
|
||||||
|
async fn init_games() -> Result<(), ()> {
|
||||||
|
let slot_manager = SlotManager::new(100);
|
||||||
|
GAMES.set(Mutex::new(slot_manager)).map_err(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn new_game() -> Uuid {
|
||||||
|
// This should be safe becasue the initialization is in the main
|
||||||
|
let slot_manager_mutex = GAMES.get().unwrap();
|
||||||
|
let mut slot_manager = slot_manager_mutex.lock().await;
|
||||||
|
let new_game = slot_manager.new_game().await;
|
||||||
|
new_game.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_game(uuid: Uuid) -> Result<Game, ()> {
|
||||||
|
let slot_manager_mutex = GAMES.get().unwrap();
|
||||||
|
let mut slot_manager = slot_manager_mutex.lock().await;
|
||||||
|
|
||||||
|
match slot_manager.get_game(uuid) {
|
||||||
|
Some(game) => Ok(game.clone()),
|
||||||
|
None => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn add_attempt(uuid: Uuid, attempt: &String) -> Option<InsertionStatus> {
|
||||||
|
let slot_manager_mutex = GAMES.get().unwrap();
|
||||||
|
let mut slot_manager = slot_manager_mutex.lock().await;
|
||||||
|
match slot_manager.get_game(uuid) {
|
||||||
|
Some(game) => Some(game.add_attempt(attempt)),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[windmark::main]
|
||||||
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
init_games()
|
||||||
|
.await
|
||||||
|
.expect("Error trying to initialize the game slots");
|
||||||
|
|
||||||
|
windmark::router::Router::new()
|
||||||
|
.set_private_key_file("key.pem")
|
||||||
|
.set_certificate_file("cert.pem")
|
||||||
|
.set_languages(["es"])
|
||||||
|
.set_port(1970)
|
||||||
|
.enable_default_logger(true)
|
||||||
|
.set_fix_path(true)
|
||||||
|
.mount("/", move |_| async {
|
||||||
|
let game = new_game().await;
|
||||||
|
windmark::response::Response::temporary_redirect(format!("/{}", game))
|
||||||
|
// let mut context = tera::Context::new();
|
||||||
|
|
||||||
|
// let board: Board = Default::default();
|
||||||
|
// context.insert("board", &board);
|
||||||
|
|
||||||
|
// windmark::response::Response::success(render("index", &context).await.unwrap())
|
||||||
|
})
|
||||||
|
.mount("/:uuid", move |request| async move {
|
||||||
|
let mut context = tera::Context::new();
|
||||||
|
// This should be safe because you can only get into the route with the right path
|
||||||
|
let possible_uuid = request.parameters.get("uuid").unwrap();
|
||||||
|
|
||||||
|
let board: Board;
|
||||||
|
let error: Option<String>;
|
||||||
|
let result_pattern: Option<String>;
|
||||||
|
|
||||||
|
match Uuid::parse_str(possible_uuid) {
|
||||||
|
Ok(uuid) => match get_game(uuid).await {
|
||||||
|
Ok(game) => {
|
||||||
|
board = game.board;
|
||||||
|
match game.status {
|
||||||
|
GameStatus::Playing => {
|
||||||
|
error = match game.last_insertion_status {
|
||||||
|
InsertionStatus::Ok | InsertionStatus::GameOver => None,
|
||||||
|
InsertionStatus::LengthError => {
|
||||||
|
Some("La palabra debe de ser de 5 letras".to_string())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
result_pattern = None;
|
||||||
|
}
|
||||||
|
GameStatus::Fail => {
|
||||||
|
error = None;
|
||||||
|
result_pattern = Some(
|
||||||
|
"Palabra no encontrada :c\nVuelve mañana para otro reto"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
GameStatus::Win(pattern) => {
|
||||||
|
error = None;
|
||||||
|
result_pattern = Some(format!("Palabra encontrada en {}/6 intentos\n{}Vuelve mañana para otro reto",game.attempt_index, pattern))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
board = Default::default();
|
||||||
|
error = Some(
|
||||||
|
"La partida a la que intentas acceder ya no existe o nunca existió"
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
result_pattern = None;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
board = Default::default();
|
||||||
|
error = Some("Identificador incorrecto".to_string());
|
||||||
|
result_pattern = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.insert("board", &board);
|
||||||
|
context.insert("error", &error);
|
||||||
|
context.insert("result_pattern", &result_pattern);
|
||||||
|
// context.insert("error", "Partida no encontrada");
|
||||||
|
windmark::response::Response::success(render("index", &context).await.unwrap())
|
||||||
|
})
|
||||||
|
.mount("/:uuid/:attempt", move |request| async move {
|
||||||
|
let mut context = tera::Context::new();
|
||||||
|
|
||||||
|
let possible_uuid = request.parameters.get("uuid").unwrap();
|
||||||
|
let attempt = urlencoding::decode(request.parameters.get("attempt").unwrap()).unwrap().to_string();
|
||||||
|
|
||||||
|
let redirect = match Uuid::parse_str(possible_uuid) {
|
||||||
|
Ok(uuid) => match add_attempt(uuid, &attempt).await {
|
||||||
|
Some(_) => true,
|
||||||
|
None => false,
|
||||||
|
},
|
||||||
|
Err(_) => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if redirect {
|
||||||
|
windmark::response::Response::temporary_redirect(format!("/{}", possible_uuid))
|
||||||
|
} else {
|
||||||
|
let board: Board = Default::default();
|
||||||
|
let result_pattern: Option<String> = None;
|
||||||
|
let error = "La partida a la que intentas jugar no existe";
|
||||||
|
|
||||||
|
context.insert("board", &board);
|
||||||
|
context.insert("error", &error);
|
||||||
|
context.insert("result_pattern", &result_pattern);
|
||||||
|
windmark::response::Response::success(render("index", &context).await.unwrap())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
use serde::{Serialize, Serializer};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum Status {
|
||||||
|
NotFound,
|
||||||
|
Found,
|
||||||
|
Almost,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Status {
|
||||||
|
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
|
match self {
|
||||||
|
Status::NotFound => serializer.serialize_char('-'),
|
||||||
|
Status::Almost => serializer.serialize_char('!'),
|
||||||
|
Status::Found => serializer.serialize_char(' '),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
use chrono::Datelike;
|
||||||
|
use tokio::sync::OnceCell;
|
||||||
|
|
||||||
|
static WORDS: OnceCell<Vec<String>> = OnceCell::const_new();
|
||||||
|
|
||||||
|
async fn get_words() -> Vec<String> {
|
||||||
|
let words = WORDS
|
||||||
|
.get_or_init(|| async {
|
||||||
|
include_str!("../words.txt")
|
||||||
|
.trim()
|
||||||
|
.lines()
|
||||||
|
.map(|w| w.trim().into())
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
words.to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_word_of_the_day() -> String {
|
||||||
|
let words = get_words().await;
|
||||||
|
|
||||||
|
let date = chrono::Utc::now();
|
||||||
|
let year = date.year() as usize;
|
||||||
|
let month = date.month() as usize;
|
||||||
|
let day = date.day() as usize;
|
||||||
|
let index = year * month * day;
|
||||||
|
|
||||||
|
words[index % words.len()].clone()
|
||||||
|
}
|
|
@ -0,0 +1,42 @@
|
||||||
|
# Worsdle en español
|
||||||
|
|
||||||
|
{% if error -%}
|
||||||
|
{{ error }}
|
||||||
|
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
```
|
||||||
|
╔═══╦═══╦═══╦═══╦═══╗
|
||||||
|
{% for attempt in board -%}
|
||||||
|
{%- if attempt -%}
|
||||||
|
║{% for char in attempt.chars %}{{ char.status }}{{ char.char }}{{ char.status }}║{% endfor -%}
|
||||||
|
{%- else -%}
|
||||||
|
║ ║ ║ ║ ║ ║
|
||||||
|
{%- endif -%}
|
||||||
|
{% if not loop.last %}
|
||||||
|
╠═══╬═══╬═══╬═══╬═══╣
|
||||||
|
{% endif -%}
|
||||||
|
{% endfor %}
|
||||||
|
╚═══╩═══╩═══╩═══╩═══╝
|
||||||
|
```
|
||||||
|
|
||||||
|
{% if result_pattern -%}
|
||||||
|
{{ result_pattern }}
|
||||||
|
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
## Indicaciones:
|
||||||
|
|
||||||
|
* -A- Letra no se encuentra en la palabra.
|
||||||
|
* !A! Letra se encuentra en la palabra pero en otro lugar.
|
||||||
|
* A Letra en el lugar correcto.
|
||||||
|
|
||||||
|
## Instrucciones:
|
||||||
|
|
||||||
|
* Para comenzar una partida nueva, ingresa a la URL sin ningún atributo o path.
|
||||||
|
* Cada que quieras ingresar un nuevo intento, escribe al final de la URL una diagonal seguido de tu palabra. Ejemplo: "/suelo".
|
||||||
|
* tienes 6 intentos para adivinar la palabra.
|
||||||
|
* La palabra es igual para todas las personas que visitan la página y cada día hay una palabra nueva.
|
||||||
|
|
||||||
|
## Info:
|
||||||
|
Este clon de wordle está hecho por kirbylife utilizando el lenguaje de programación Rust, el framework Windmark y el editor Emacs
|
Loading…
Reference in New Issue