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