initial commit at a basic partial working state
This commit is contained in:
commit
4ba77630c8
|
|
@ -0,0 +1,5 @@
|
|||
/werewolves/dist/
|
||||
/target/
|
||||
.vscode
|
||||
build-and-send.fish
|
||||
werewolves-saves/
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,8 @@
|
|||
[workspace]
|
||||
resolver = "3"
|
||||
members = [
|
||||
"werewolves",
|
||||
"werewolves-macros",
|
||||
"werewolves-proto",
|
||||
"werewolves-server",
|
||||
]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "werewolves-macros"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "2", features = ["full", "extra-traits"] }
|
||||
convert_case = { version = "0.8" }
|
||||
|
|
@ -0,0 +1,321 @@
|
|||
use core::hash::Hash;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use quote::{ToTokens, quote, quote_spanned};
|
||||
use syn::{parenthesized, parse::Parse, spanned::Spanned};
|
||||
|
||||
use crate::hashlist::HashList;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChecksAs {
|
||||
name: syn::Ident,
|
||||
checks_as: HashList<ChecksAsArg, syn::Variant>,
|
||||
total_fields: usize,
|
||||
}
|
||||
|
||||
// impl<V> HashList<ChecksAsArg, V> {
|
||||
// pub fn add_spanned(&mut self, key: ChecksAsArg, value: V) {
|
||||
// if let Some(orig_key) = self.0.keys().find(|k| **k == key).cloned() {
|
||||
// let new_span = key
|
||||
// .span()
|
||||
// .join(orig_key.span())
|
||||
// .unwrap_or(key.span().located_at(orig_key.span()));
|
||||
// let mut vals = self.0.remove(&key).unwrap();
|
||||
// vals.push(value);
|
||||
// self.0.insert(key.with_span(new_span), vals);
|
||||
// return;
|
||||
// }
|
||||
// self.0.insert(key, vec![value]);
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum ChecksAsArg {
|
||||
AsSelf(proc_macro2::Span),
|
||||
Name(syn::Ident),
|
||||
Path(syn::Path),
|
||||
}
|
||||
|
||||
impl PartialEq for ChecksAsArg {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::AsSelf(_), Self::AsSelf(_)) => true,
|
||||
(Self::Name(l0), Self::Name(r0)) => l0 == r0,
|
||||
(Self::Path(l0), Self::Path(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ChecksAsArg {}
|
||||
|
||||
impl Hash for ChecksAsArg {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
#[derive(Hash)]
|
||||
enum ChecksAsArgHash<'a> {
|
||||
AsSelf,
|
||||
Name(&'a syn::Ident),
|
||||
Path(&'a syn::Path),
|
||||
}
|
||||
core::mem::discriminant(&match self {
|
||||
ChecksAsArg::AsSelf(_) => ChecksAsArgHash::AsSelf,
|
||||
ChecksAsArg::Name(ident) => ChecksAsArgHash::Name(ident),
|
||||
ChecksAsArg::Path(path) => ChecksAsArgHash::Path(path),
|
||||
})
|
||||
.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl ChecksAsArg {
|
||||
fn span(&self) -> proc_macro2::Span {
|
||||
match self {
|
||||
ChecksAsArg::AsSelf(span) => *span,
|
||||
ChecksAsArg::Name(ident) => ident.span(),
|
||||
ChecksAsArg::Path(path) => path.span(),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_span(&self, span: proc_macro2::Span) -> Self {
|
||||
match self {
|
||||
ChecksAsArg::AsSelf(_) => ChecksAsArg::AsSelf(span),
|
||||
ChecksAsArg::Name(ident) => {
|
||||
ChecksAsArg::Name(syn::Ident::new(ident.to_string().as_str(), span))
|
||||
}
|
||||
ChecksAsArg::Path(path) => ChecksAsArg::Path(syn::Path {
|
||||
leading_colon: path.leading_colon,
|
||||
segments: path
|
||||
.segments
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|c| syn::PathSegment {
|
||||
ident: syn::Ident::new(c.ident.to_string().as_str(), span),
|
||||
arguments: c.arguments,
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PathOrName {
|
||||
Path(syn::Path),
|
||||
Name(syn::LitStr),
|
||||
}
|
||||
|
||||
impl Parse for PathOrName {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
if input.peek(syn::LitStr) {
|
||||
Ok(Self::Name(input.parse()?))
|
||||
} else {
|
||||
Ok(Self::Path(input.parse()?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ChecksAsArg {
|
||||
fn from_attr(attr: &syn::Attribute) -> syn::Result<Self> {
|
||||
match &attr.meta {
|
||||
syn::Meta::Path(path) => {
|
||||
path.require_ident()?;
|
||||
Ok(Self::AsSelf(path.span()))
|
||||
}
|
||||
syn::Meta::NameValue(v) => Err(syn::Error::new(
|
||||
attr.meta.span(),
|
||||
format!("meta_name_value: {v:?}").as_str(),
|
||||
)),
|
||||
syn::Meta::List(list) => match list.parse_args::<PathOrName>()? {
|
||||
PathOrName::Path(path) => Ok(Self::Path(path)),
|
||||
PathOrName::Name(lit_str) => Ok(Self::Name(syn::Ident::new(
|
||||
lit_str.value().as_str(),
|
||||
lit_str.span(),
|
||||
))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn filter(attr: &&syn::Attribute) -> bool {
|
||||
attr.path()
|
||||
.get_ident()
|
||||
.map(|id| id.to_string().as_str() == CHECKS_AS_PATH)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
const CHECKS_AS_PATH: &str = "checks";
|
||||
|
||||
impl ChecksAs {
|
||||
pub fn parse(input: syn::DeriveInput) -> Result<Self, syn::Error> {
|
||||
let name = input.ident;
|
||||
let mut checks_as = HashList::new();
|
||||
let total_fields;
|
||||
match &input.data {
|
||||
syn::Data::Enum(data) => {
|
||||
total_fields = data.variants.len();
|
||||
for field in data.variants.iter() {
|
||||
for attr in field
|
||||
.attrs
|
||||
.iter()
|
||||
.filter(ChecksAsArg::filter)
|
||||
.map(ChecksAsArg::from_attr)
|
||||
{
|
||||
let attr = attr?;
|
||||
let mut field = field.clone();
|
||||
field.ident.set_span(attr.span());
|
||||
|
||||
checks_as.add(attr, field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => todo!(),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
checks_as,
|
||||
total_fields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn fields_args(field: &syn::Variant) -> proc_macro2::TokenStream {
|
||||
match &field.fields {
|
||||
syn::Fields::Named(f) => {
|
||||
let named = f.named.iter().map(|f| f.ident.clone());
|
||||
quote! { {#(#named: _),*} }
|
||||
}
|
||||
syn::Fields::Unnamed(f) => {
|
||||
let f = f.unnamed.iter().map(|_| quote! {_});
|
||||
quote! {
|
||||
(#(#f),*)
|
||||
}
|
||||
}
|
||||
syn::Fields::Unit => quote! {},
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for ChecksAs {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
// panic!("{self:?}");
|
||||
let name = self.name.clone();
|
||||
let (path, non_path) = self
|
||||
.checks_as
|
||||
.clone()
|
||||
.decompose()
|
||||
.into_iter()
|
||||
.partition::<Vec<_>, _>(|(c, _)| matches!(c, ChecksAsArg::Path(_)));
|
||||
for (check, fields) in non_path {
|
||||
match check {
|
||||
ChecksAsArg::AsSelf(_) => {
|
||||
let fields = fields.into_iter().map(|f| {
|
||||
let fn_name = syn::Ident::new(
|
||||
format!("is_{}", f.ident.to_string().to_case(Case::Snake)).as_str(),
|
||||
f.ident.span(),
|
||||
);
|
||||
let ident = f.ident.clone();
|
||||
let args = fields_args(&f);
|
||||
quote_spanned! { f.span() =>
|
||||
pub const fn #fn_name(&self) -> bool {
|
||||
matches!(self, Self::#ident #args)
|
||||
}
|
||||
}
|
||||
});
|
||||
tokens.extend(quote! {
|
||||
impl #name {
|
||||
#(#fields)*
|
||||
}
|
||||
});
|
||||
}
|
||||
ChecksAsArg::Name(ident) => {
|
||||
let fields = fields.into_iter().map(|f| {
|
||||
let args = fields_args(&f);
|
||||
let ident = f.ident.clone();
|
||||
|
||||
quote_spanned! {f.span() =>
|
||||
#name::#ident #args
|
||||
}
|
||||
});
|
||||
tokens.extend(quote! {
|
||||
impl #name {
|
||||
pub const fn #ident(&self) -> bool {
|
||||
matches!(self, #(#fields)|*)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
ChecksAsArg::Path(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
let mut by_path_start = HashList::new();
|
||||
let mut tmp = path.iter();
|
||||
while let Some((ChecksAsArg::Path(path), fields)) = tmp.next() {
|
||||
let start = path.segments.iter().next().unwrap().clone();
|
||||
by_path_start.add(start, (path.clone(), fields));
|
||||
}
|
||||
let by_path = by_path_start.decompose();
|
||||
|
||||
for (start, paths) in by_path {
|
||||
let mut unique_count = HashMap::new();
|
||||
let mut total_count = 0usize;
|
||||
paths.iter().for_each(|(_, variants)| {
|
||||
variants.iter().for_each(|v| {
|
||||
unique_count.insert(v, ());
|
||||
total_count += 1;
|
||||
})
|
||||
});
|
||||
let unique_variants = unique_count.keys().count();
|
||||
if unique_variants != total_count {
|
||||
panic!("some fields have multiple ChecksAs values for the same type")
|
||||
}
|
||||
// nowadays everywhere i look i see patterns
|
||||
let patterns = paths.into_iter().map(|(val, variants)| {
|
||||
let idents = variants.iter().map(|v| {
|
||||
let args = fields_args(v);
|
||||
let ident = v.ident.clone();
|
||||
quote! {
|
||||
#ident #args
|
||||
}
|
||||
});
|
||||
quote! {
|
||||
#(#name::#idents)|* => #val,
|
||||
}
|
||||
});
|
||||
|
||||
let fn_name = syn::Ident::new(
|
||||
start.ident.to_string().to_case(Case::Snake).as_str(),
|
||||
start.span(),
|
||||
);
|
||||
|
||||
let (ret, match_statement) = if unique_variants != self.total_fields {
|
||||
(
|
||||
quote! {Option<#start>},
|
||||
quote! {
|
||||
Some(match self {
|
||||
#(#patterns)*
|
||||
_ => return None,
|
||||
})
|
||||
},
|
||||
)
|
||||
} else {
|
||||
(
|
||||
quote! {#start},
|
||||
quote! {
|
||||
match self {
|
||||
#(#patterns)*
|
||||
}
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
tokens.extend(quote! {
|
||||
impl #name {
|
||||
pub const fn #fn_name(&self) -> #ret {
|
||||
#match_statement
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
|
||||
pub struct HashList<K: Eq + Hash, V>(HashMap<K, Vec<V>>);
|
||||
|
||||
impl<K: Eq + Hash + core::fmt::Debug, V: core::fmt::Debug> core::fmt::Debug for HashList<K, V> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_tuple("HashList").field(&self.0).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Eq + Hash + Clone, V: Clone> Clone for HashList<K, V> {
|
||||
fn clone(&self) -> Self {
|
||||
Self(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Eq + Hash, V> HashList<K, V> {
|
||||
pub fn new() -> Self {
|
||||
Self(HashMap::new())
|
||||
}
|
||||
|
||||
pub fn add(&mut self, key: K, value: V) {
|
||||
match self.0.get_mut(&key) {
|
||||
Some(values) => values.push(value),
|
||||
None => {
|
||||
self.0.insert(key, vec![value]);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn decompose(&mut self) -> Box<[(K, Vec<V>)]> {
|
||||
let mut body = HashMap::new();
|
||||
core::mem::swap(&mut self.0, &mut body);
|
||||
body.into_iter().collect()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,545 @@
|
|||
use core::error::Error;
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use convert_case::Casing;
|
||||
use proc_macro2::Span;
|
||||
use quote::{ToTokens, quote};
|
||||
use syn::{braced, bracketed, parenthesized, parse::Parse, parse_macro_input};
|
||||
|
||||
mod checks;
|
||||
pub(crate) mod hashlist;
|
||||
mod targets;
|
||||
|
||||
struct IncludePath {
|
||||
dir_path: PathBuf,
|
||||
modules: Vec<syn::Ident>,
|
||||
}
|
||||
|
||||
fn display_err(span: Span, err: impl Error) -> syn::Error {
|
||||
syn::Error::new(span, err.to_string().as_str())
|
||||
}
|
||||
|
||||
fn read_modules(span: Span, path: &Path) -> syn::Result<Vec<syn::Ident>> {
|
||||
let read_dir = std::fs::read_dir(path).map_err(|err| display_err(span, err))?;
|
||||
|
||||
Ok(read_dir
|
||||
.map(|file| file.map_err(|err| display_err(span, err)))
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.filter(|file| file.file_type().map(|ft| ft.is_file()).unwrap_or_default())
|
||||
.filter_map(|file| {
|
||||
file.file_name()
|
||||
.to_string_lossy()
|
||||
.strip_suffix(".rs")
|
||||
.map(|f| f.to_string())
|
||||
})
|
||||
.map(|module_name| syn::Ident::new(&module_name, span))
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
impl Parse for IncludePath {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let directory = input.parse::<syn::LitStr>()?;
|
||||
|
||||
let dir_path = std::env::current_dir()
|
||||
.unwrap()
|
||||
.join(directory.value())
|
||||
.to_path_buf();
|
||||
|
||||
let modules = read_modules(directory.span(), &dir_path)?;
|
||||
|
||||
Ok(Self { modules, dir_path })
|
||||
}
|
||||
}
|
||||
impl IncludePath {
|
||||
fn posts_tokens(&self) -> proc_macro2::TokenStream {
|
||||
let mod_decl = self.modules.iter().map(|module| {
|
||||
let path = self.dir_path.join(format!("{module}.rs"));
|
||||
let path = path.to_str().unwrap();
|
||||
quote! {
|
||||
mod #module {
|
||||
include!(#path);
|
||||
}}
|
||||
});
|
||||
let posts = self.modules.iter().map(|module| {
|
||||
quote! {
|
||||
#module::POST.into_post()
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#(#mod_decl)*
|
||||
|
||||
pub const POSTS: &[Post] = &[#(#posts),*];
|
||||
}
|
||||
}
|
||||
fn normal_tokens(&self) -> proc_macro2::TokenStream {
|
||||
let mod_decl = self.modules.iter().map(|module| {
|
||||
quote! {mod #module;}
|
||||
});
|
||||
let pub_use = self.modules.iter().map(|module| {
|
||||
quote! {
|
||||
#module::*
|
||||
}
|
||||
});
|
||||
|
||||
quote! {
|
||||
#(#mod_decl)*
|
||||
pub use {#(#pub_use),*};
|
||||
}
|
||||
}
|
||||
}
|
||||
enum IncludeOutput {
|
||||
Normal(IncludePath),
|
||||
Posts(IncludePath),
|
||||
}
|
||||
|
||||
impl ToTokens for IncludeOutput {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
tokens.extend(match self {
|
||||
IncludeOutput::Normal(include_path) => include_path.normal_tokens(),
|
||||
IncludeOutput::Posts(include_path) => include_path.posts_tokens(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct FileWithPath {
|
||||
// bytes: Vec<u8>,
|
||||
include: proc_macro2::TokenStream,
|
||||
rel_path: String,
|
||||
}
|
||||
impl FileWithPath {
|
||||
fn from_path(path: impl AsRef<Path>, origin_path: impl AsRef<Path>) -> Result<Self, io::Error> {
|
||||
let abs_path = path
|
||||
.as_ref()
|
||||
.canonicalize()
|
||||
.ok()
|
||||
.and_then(|p| p.to_str().map(|t| t.to_string()))
|
||||
.ok_or(io::Error::new(io::ErrorKind::InvalidData, "invalid path"))?;
|
||||
let include = quote! {
|
||||
include_bytes!(#abs_path)
|
||||
};
|
||||
let origin_abs = origin_path.as_ref().canonicalize().and_then(|p| {
|
||||
p.to_str()
|
||||
.ok_or(io::Error::new(io::ErrorKind::InvalidData, "invalid path"))
|
||||
.map(|t| t.to_string())
|
||||
})?;
|
||||
let rel_path = abs_path.replace(&origin_abs, "");
|
||||
|
||||
Ok(Self { include, rel_path })
|
||||
}
|
||||
}
|
||||
|
||||
fn find_matching_files_recursive<P, F, D>(
|
||||
p: P,
|
||||
include_in_rerun: F,
|
||||
directory_filter: D,
|
||||
out: &mut Vec<FileWithPath>,
|
||||
origin_path: &PathBuf,
|
||||
) -> Result<(), io::Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
F: Fn(&str) -> bool + Copy,
|
||||
D: Fn(&str) -> bool + Copy,
|
||||
{
|
||||
for item in std::fs::read_dir(p.as_ref())? {
|
||||
let item = item?;
|
||||
if item.file_type()?.is_dir() {
|
||||
if directory_filter(item.file_name().to_str().ok_or(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"file has no name",
|
||||
))?) {
|
||||
find_matching_files_recursive(
|
||||
item.path(),
|
||||
include_in_rerun,
|
||||
directory_filter,
|
||||
out,
|
||||
origin_path,
|
||||
)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(file_name) = item.file_name().to_str() {
|
||||
if include_in_rerun(file_name) {
|
||||
out.push(FileWithPath::from_path(item.path(), origin_path)?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct IncludeDist {
|
||||
name: syn::Ident,
|
||||
files: Vec<FileWithPath>,
|
||||
}
|
||||
|
||||
impl IncludeDist {
|
||||
fn read_dist_path(
|
||||
name: syn::Ident,
|
||||
path_span: Span,
|
||||
path: &Path,
|
||||
origin_path: &PathBuf,
|
||||
) -> syn::Result<Self> {
|
||||
let mut files = Vec::new();
|
||||
find_matching_files_recursive(path, |_| true, |_| true, &mut files, origin_path)
|
||||
.map_err(|err| syn::Error::new(path_span, err.to_string()))?;
|
||||
Ok(Self { name, files })
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for IncludeDist {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let name = input.parse::<syn::Ident>()?;
|
||||
input.parse::<syn::Token![,]>()?;
|
||||
let directory = input.parse::<syn::LitStr>()?;
|
||||
|
||||
let dir_path = std::env::current_dir()
|
||||
.unwrap()
|
||||
.join(directory.value())
|
||||
.to_path_buf();
|
||||
|
||||
Self::read_dist_path(name, directory.span(), &dir_path, &dir_path)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for IncludeDist {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
// std::collections::HashMap
|
||||
let array_contents = self.files.iter().map(|file| {
|
||||
let FileWithPath { include, rel_path } = file;
|
||||
|
||||
quote! {
|
||||
(#rel_path, #include)
|
||||
}
|
||||
});
|
||||
let name = &self.name;
|
||||
tokens.extend(quote! {
|
||||
pub const #name: &[(&'static str, &'static [u8])] = &[#(#array_contents),*];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct StaticLinks {
|
||||
collect_into_const: Option<syn::Ident>,
|
||||
relative_to: PathBuf,
|
||||
files: Vec<(String, PathBuf)>,
|
||||
}
|
||||
|
||||
impl Parse for StaticLinks {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let collect_into_const = if input.peek(syn::Ident) {
|
||||
let out = Some(input.parse()?);
|
||||
input.parse::<syn::Token![=>]>()?;
|
||||
out
|
||||
} else {
|
||||
None
|
||||
};
|
||||
const EXPECTED: &str = "expected 'relative to'";
|
||||
let directory = input.parse::<syn::LitStr>()?;
|
||||
{
|
||||
let rel = input
|
||||
.parse::<syn::Ident>()
|
||||
.map_err(|err| syn::Error::new(err.span(), EXPECTED))?;
|
||||
if rel != "relative" {
|
||||
return Err(syn::Error::new(rel.span(), EXPECTED));
|
||||
}
|
||||
}
|
||||
{
|
||||
let to = input
|
||||
.parse::<syn::Ident>()
|
||||
.map_err(|err| syn::Error::new(err.span(), EXPECTED))?;
|
||||
if to != "to" {
|
||||
return Err(syn::Error::new(to.span(), EXPECTED));
|
||||
}
|
||||
}
|
||||
let current_dir = std::env::current_dir().unwrap();
|
||||
let relative_to = current_dir
|
||||
.join(input.parse::<syn::LitStr>()?.value())
|
||||
.canonicalize()
|
||||
.expect("cannonicalize relative to path");
|
||||
|
||||
let span = directory.span();
|
||||
|
||||
let path_dir = current_dir
|
||||
.join(directory.value())
|
||||
.canonicalize()
|
||||
.expect("canonicalize base path")
|
||||
.to_path_buf();
|
||||
|
||||
let read_dir = std::fs::read_dir(&path_dir).map_err(|err| display_err(span, err))?;
|
||||
|
||||
let files = read_dir
|
||||
.map(|file| file.map_err(|err| display_err(span, err)))
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.filter(|file| file.file_type().map(|ft| ft.is_file()).unwrap_or_default())
|
||||
.map(|file| {
|
||||
(
|
||||
file.file_name().to_str().expect("bad filename").to_string(),
|
||||
file.path(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Ok(StaticLinks {
|
||||
collect_into_const,
|
||||
relative_to,
|
||||
files,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for StaticLinks {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let base_path = self.relative_to.to_str().expect("base path to_string");
|
||||
let ident_with_decl = self.files.iter().map(|(file, path)| {
|
||||
let file_ident = syn::Ident::new(
|
||||
file.replace(['.', ' ', '-'], "_").to_uppercase().as_str(),
|
||||
Span::call_site(),
|
||||
);
|
||||
let filepath = path
|
||||
.to_str()
|
||||
.expect("path to string")
|
||||
.replace(base_path, "");
|
||||
let decl = quote! {
|
||||
pub const #file_ident: &'static str = #filepath;
|
||||
};
|
||||
(file_ident, decl)
|
||||
});
|
||||
let files = ident_with_decl.clone().map(|(_, q)| q);
|
||||
|
||||
tokens.extend(quote! {
|
||||
#(#files)*
|
||||
});
|
||||
if let Some(collect_into_const) = &self.collect_into_const {
|
||||
let idents = ident_with_decl.map(|(ident, _)| ident);
|
||||
tokens.extend(quote! {
|
||||
pub const #collect_into_const: &[&str] = &[#(#idents),*];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StaticLinks {
|
||||
fn into_demand_metadata(self) -> StaticLinksDemandMetadata {
|
||||
let StaticLinks {
|
||||
collect_into_const,
|
||||
relative_to,
|
||||
files,
|
||||
} = self;
|
||||
StaticLinksDemandMetadata {
|
||||
collect_into_const,
|
||||
relative_to,
|
||||
files,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StaticLinksDemandMetadata {
|
||||
collect_into_const: Option<syn::Ident>,
|
||||
relative_to: PathBuf,
|
||||
files: Vec<(String, PathBuf)>,
|
||||
}
|
||||
|
||||
impl ToTokens for StaticLinksDemandMetadata {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
let base_path = self.relative_to.to_str().expect("base path to_string");
|
||||
let ident_with_decl = self.files.iter().map(|(file, path)| {
|
||||
let file_ident = syn::Ident::new(
|
||||
file.replace(['.', ' ', '-'], "_").to_uppercase().as_str(),
|
||||
Span::call_site(),
|
||||
);
|
||||
let filepath = path
|
||||
.to_str()
|
||||
.expect("path to string")
|
||||
.replace(base_path, "");
|
||||
let decl = quote! {
|
||||
pub const #file_ident: &'static str = #filepath;
|
||||
};
|
||||
(file_ident, filepath, decl)
|
||||
});
|
||||
let files = ident_with_decl.clone().map(|(_, _, q)| q);
|
||||
|
||||
tokens.extend(quote! {
|
||||
#(#files)*
|
||||
});
|
||||
if let Some(collect_into_const) = &self.collect_into_const {
|
||||
let idents = ident_with_decl.clone().map(|(ident, _, _)| ident);
|
||||
tokens.extend(quote! {
|
||||
pub const #collect_into_const: &[&str] = &[#(#idents),*];
|
||||
});
|
||||
}
|
||||
|
||||
let (paths_and_snake_idents, members): (Vec<_>, Vec<_>) = ident_with_decl
|
||||
.clone()
|
||||
.map(|(i, filepath, _)| {
|
||||
let snake_ident = syn::Ident::new(
|
||||
i.to_string().to_case(convert_case::Case::Snake).as_str(),
|
||||
i.span(),
|
||||
);
|
||||
let member = quote! {
|
||||
pub #snake_ident: Metadata,
|
||||
};
|
||||
((filepath, snake_ident), member)
|
||||
})
|
||||
.unzip();
|
||||
|
||||
let find_by_key = paths_and_snake_idents.iter().map(|(path, ident)| {
|
||||
quote! {
|
||||
#path => &self.#ident
|
||||
}
|
||||
});
|
||||
|
||||
tokens.extend(quote! {
|
||||
pub struct MetadataMap {
|
||||
#(#members)*
|
||||
}
|
||||
|
||||
impl MetadataMap {
|
||||
pub fn find_by_key(&self, key: &str) -> Option<&Metadata> {
|
||||
Some(match key {
|
||||
#(#find_by_key,)*
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn include_dist(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let incl_dist = parse_macro_input!(input as IncludeDist);
|
||||
quote! {#incl_dist}.into()
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn posts(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let pubuse = IncludeOutput::Posts(parse_macro_input!(input as IncludePath));
|
||||
quote! {#pubuse}.into()
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn include_path(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let incl_path = IncludeOutput::Normal(parse_macro_input!(input as IncludePath));
|
||||
quote! {#incl_path}.into()
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn static_links(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let static_links = parse_macro_input!(input as StaticLinks);
|
||||
quote! {#static_links}.into()
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn static_links_demand_metadata(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let demand_metadata = parse_macro_input!(input as StaticLinks).into_demand_metadata();
|
||||
quote! {#demand_metadata}.into()
|
||||
}
|
||||
|
||||
fn fields_args(field: &syn::Variant) -> proc_macro2::TokenStream {
|
||||
match &field.fields {
|
||||
syn::Fields::Named(f) => {
|
||||
let named = f.named.iter().map(|f| f.ident.clone());
|
||||
quote! { {#(#named: _),*} }
|
||||
}
|
||||
syn::Fields::Unnamed(f) => {
|
||||
let f = f.unnamed.iter().map(|_| quote! {_});
|
||||
quote! {
|
||||
(#(#f),*)
|
||||
}
|
||||
}
|
||||
syn::Fields::Unit => quote! {},
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Titles)]
|
||||
pub fn villager_roles(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let villager_roles: syn::DeriveInput = parse_macro_input!(input);
|
||||
let name = syn::Ident::new(
|
||||
format!("{}Title", villager_roles.ident).as_str(),
|
||||
villager_roles.ident.span(),
|
||||
);
|
||||
let original_name = &villager_roles.ident;
|
||||
let variants = match &villager_roles.data {
|
||||
syn::Data::Enum(data_enum) => data_enum.variants.iter().collect::<Vec<_>>(),
|
||||
_ => todo!(),
|
||||
};
|
||||
let display_match = variants.iter().map(|v| {
|
||||
let name_str = v.ident.to_string();
|
||||
let name = v.ident.clone();
|
||||
quote! {
|
||||
Self::#name => f.write_str(#name_str),
|
||||
}
|
||||
});
|
||||
|
||||
let title_match = variants.iter().map(|v| {
|
||||
let args = fields_args(v);
|
||||
let field_name = v.ident.clone();
|
||||
quote! {
|
||||
#original_name::#field_name #args => #name::#field_name,
|
||||
}
|
||||
});
|
||||
|
||||
let names_count = variants.len();
|
||||
let names_const_val = variants
|
||||
.iter()
|
||||
.map(|v| {
|
||||
let name = v.ident.clone();
|
||||
quote! {Self::#name}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let enum_variant_decl = variants.iter().map(|v| {
|
||||
let ident = v.ident.clone();
|
||||
let attrs = v.attrs.iter();
|
||||
quote! {
|
||||
#(#attrs)*
|
||||
#ident
|
||||
}
|
||||
});
|
||||
quote! {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Eq, Hash, werewolves_macros::ChecksAs)]
|
||||
pub enum #name {
|
||||
#(#enum_variant_decl,)*
|
||||
}
|
||||
|
||||
impl #original_name {
|
||||
pub const fn title(&self) -> #name {
|
||||
match self {
|
||||
#(#title_match)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl #name {
|
||||
pub const ALL: [Self; #names_count] = [#(#names_const_val,)*];
|
||||
}
|
||||
|
||||
impl core::fmt::Display for #name {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
#(#display_match)*
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(ChecksAs, attributes(checks))]
|
||||
pub fn checks_as(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let checks_as = checks::ChecksAs::parse(parse_macro_input!(input)).unwrap();
|
||||
|
||||
quote! {#checks_as}.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(Extract, attributes(extract))]
|
||||
pub fn extract(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let checks_as = targets::Targets::parse(parse_macro_input!(input)).unwrap();
|
||||
|
||||
quote! {#checks_as}.into()
|
||||
}
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{ToTokens, quote, quote_spanned};
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
use crate::hashlist::HashList;
|
||||
|
||||
const TARGETS_PATH: &str = "extract";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
enum VariantFieldIndex {
|
||||
Ident(syn::Ident),
|
||||
Numeric(syn::LitInt),
|
||||
None,
|
||||
}
|
||||
|
||||
impl VariantFieldIndex {
|
||||
const fn is_numeric(&self) -> bool {
|
||||
match self {
|
||||
Self::Numeric(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl syn::parse::Parse for VariantFieldIndex {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
if input.is_empty() {
|
||||
Ok(VariantFieldIndex::None)
|
||||
} else if input.peek(syn::LitInt) {
|
||||
Ok(VariantFieldIndex::Numeric(input.parse()?))
|
||||
} else {
|
||||
Ok(VariantFieldIndex::Ident(input.parse()?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct VariantAttr {
|
||||
index: VariantFieldIndex,
|
||||
alias: Option<syn::Ident>,
|
||||
}
|
||||
|
||||
impl syn::parse::Parse for VariantAttr {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let index = if input.peek(syn::token::As) {
|
||||
VariantFieldIndex::None
|
||||
} else {
|
||||
input.parse::<VariantFieldIndex>()?
|
||||
};
|
||||
if !input.peek(syn::token::As) {
|
||||
return Ok(Self { index, alias: None });
|
||||
}
|
||||
input.parse::<syn::Token![as]>()?;
|
||||
|
||||
Ok(Self {
|
||||
index,
|
||||
alias: Some(input.parse()?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TargetVariant {
|
||||
attr: VariantAttr,
|
||||
access: TokenStream,
|
||||
ty: syn::Type,
|
||||
variant: syn::Variant,
|
||||
}
|
||||
|
||||
impl TargetVariant {
|
||||
fn name(&self) -> syn::Ident {
|
||||
if let Some(alias) = self.attr.alias.as_ref() {
|
||||
return alias.clone();
|
||||
}
|
||||
match &self.attr.index {
|
||||
VariantFieldIndex::Ident(ident) => ident.clone(),
|
||||
VariantFieldIndex::Numeric(_) | VariantFieldIndex::None => self.variant.ident.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Targets {
|
||||
name: syn::Ident,
|
||||
targets: Vec<TargetVariant>,
|
||||
total_fields: usize,
|
||||
}
|
||||
|
||||
impl Targets {
|
||||
pub fn parse(input: syn::DeriveInput) -> Result<Self, syn::Error> {
|
||||
let name = input.ident;
|
||||
let mut targets: Vec<TargetVariant> = Vec::new();
|
||||
let total_fields;
|
||||
match &input.data {
|
||||
syn::Data::Enum(data) => {
|
||||
total_fields = data.variants.len();
|
||||
for field in data.variants.iter() {
|
||||
let field_ident = &field.ident;
|
||||
for attr in field.attrs.iter().filter(|attr| {
|
||||
attr.path()
|
||||
.get_ident()
|
||||
.map(|id| id.to_string().as_str() == TARGETS_PATH)
|
||||
.unwrap_or_default()
|
||||
}) {
|
||||
let attr_span = attr.span();
|
||||
let attr = attr.parse_args::<VariantAttr>()?;
|
||||
let (access, ty) = match &field.fields {
|
||||
syn::Fields::Named(fields) => match &attr.index {
|
||||
VariantFieldIndex::Ident(ident) => {
|
||||
let mut matching_val = None;
|
||||
let mut accesses = Vec::new();
|
||||
for field in fields.named.iter() {
|
||||
let matching = field
|
||||
.ident
|
||||
.as_ref()
|
||||
.map(|i| i.to_string() == ident.to_string())
|
||||
.unwrap_or_default();
|
||||
if matching_val.is_some() && matching {
|
||||
panic!("duplicate?")
|
||||
}
|
||||
|
||||
let ident = field.ident.clone().unwrap();
|
||||
if matching {
|
||||
matching_val = Some((ident.clone(), field.ty.clone()));
|
||||
accesses.push(quote! {
|
||||
#ident
|
||||
});
|
||||
} else {
|
||||
accesses.push(quote! {
|
||||
#ident: _
|
||||
});
|
||||
}
|
||||
}
|
||||
let (mut matching_ident, matching_ty) = matching_val.ok_or(
|
||||
syn::Error::new(ident.span(), "no such variant field"),
|
||||
)?;
|
||||
matching_ident.set_span(ident.span());
|
||||
|
||||
(
|
||||
quote! {
|
||||
#name::#field_ident { #(#accesses),* } => &#matching_ident,
|
||||
},
|
||||
matching_ty,
|
||||
)
|
||||
}
|
||||
|
||||
VariantFieldIndex::Numeric(num) => {
|
||||
return Err(syn::Error::new(
|
||||
num.span(),
|
||||
"cannot used numeric index for a variant with named fields",
|
||||
));
|
||||
}
|
||||
VariantFieldIndex::None => {
|
||||
if fields.named.len() == 1 {
|
||||
let field = fields.named.iter().next().unwrap();
|
||||
let field_ident = field.ident.as_ref().unwrap();
|
||||
(
|
||||
quote! {
|
||||
#name::#field_ident { #field_ident } => &#field_ident,
|
||||
},
|
||||
field.ty.clone(),
|
||||
)
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
fields.named.span(),
|
||||
"unnamed field index with more than one field",
|
||||
));
|
||||
}
|
||||
}
|
||||
},
|
||||
syn::Fields::Unnamed(fields) => {
|
||||
if let VariantFieldIndex::Numeric(num) = &attr.index {
|
||||
let num = num.base10_parse::<u8>()? as usize;
|
||||
let field = fields
|
||||
.unnamed
|
||||
.iter()
|
||||
.nth(num)
|
||||
.ok_or(syn::Error::new(
|
||||
attr_span,
|
||||
"field index out of range",
|
||||
))?
|
||||
.clone();
|
||||
let left = (0..num).map(|_| quote! {_});
|
||||
let right = (num..fields.unnamed.len()).map(|_| quote! {_});
|
||||
(
|
||||
quote! {
|
||||
#name::#field_ident(#(#left),*, val, #(#right),*) => &val,
|
||||
},
|
||||
field.ty.clone(),
|
||||
)
|
||||
} else if fields.unnamed.len() == 1 {
|
||||
(
|
||||
quote! {
|
||||
#name::#field_ident(val) => &val,
|
||||
},
|
||||
fields.unnamed.iter().next().unwrap().ty.clone(),
|
||||
)
|
||||
} else {
|
||||
return Err(syn::Error::new(
|
||||
fields.span(),
|
||||
"unnamed fields without numeric index",
|
||||
));
|
||||
}
|
||||
}
|
||||
syn::Fields::Unit => {
|
||||
return Err(syn::Error::new(
|
||||
field.span(),
|
||||
"target cannot be a unit field",
|
||||
));
|
||||
}
|
||||
};
|
||||
let target = TargetVariant {
|
||||
attr,
|
||||
ty,
|
||||
access,
|
||||
variant: field.clone(),
|
||||
};
|
||||
// if targets.iter().any(|t| t.name() == target.name()) {
|
||||
// return Err(syn::Error::new(attr_span, "duplicate target field"));
|
||||
// }
|
||||
// target.variant.ident.set_span(attr_span);
|
||||
|
||||
targets.push(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => todo!(),
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
name,
|
||||
targets,
|
||||
total_fields,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ToTokens for Targets {
|
||||
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||
// this is so horrid and haphazard but idc rn
|
||||
// commenting to make the criticizing voice stfu
|
||||
let mut by_target = {
|
||||
let mut out = HashList::new();
|
||||
for target in self.targets.iter() {
|
||||
out.add(target.name().to_string(), target.clone());
|
||||
}
|
||||
out
|
||||
};
|
||||
let fns = by_target.decompose().into_iter().map(|(_, targets)| {
|
||||
if targets.is_empty() {
|
||||
return quote! {};
|
||||
}
|
||||
let all_variants = targets.len() == self.total_fields;
|
||||
let fn_ret = {
|
||||
let ty = targets.first().unwrap().ty.clone();
|
||||
if all_variants {
|
||||
quote! {&#ty}
|
||||
} else {
|
||||
quote! {Option<&#ty>}
|
||||
}
|
||||
};
|
||||
let fn_name = {
|
||||
let name = targets.first().unwrap().name();
|
||||
syn::Ident::new(name.to_string().to_case(Case::Snake).as_str(), name.span())
|
||||
};
|
||||
let raw_accesses = targets.iter().map(|t| {
|
||||
let access = &t.access;
|
||||
|
||||
quote_spanned! { t.attr.alias.as_ref().unwrap().span() =>
|
||||
#access
|
||||
}
|
||||
});
|
||||
let accesses = if all_variants {
|
||||
quote! {
|
||||
match self {
|
||||
#(#raw_accesses)*
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
Some(match self {
|
||||
#(#raw_accesses)*
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
quote! {
|
||||
pub const fn #fn_name(&self) -> #fn_ret {
|
||||
#accesses
|
||||
}
|
||||
}
|
||||
});
|
||||
let name = &self.name;
|
||||
tokens.extend(quote! {
|
||||
impl #name {
|
||||
#(#fns)*
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "werewolves-proto"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
thiserror = { version = "2" }
|
||||
log = { version = "0.4" }
|
||||
serde_json = { version = "1.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
uuid = { version = "1.17", features = ["v4", "serde"] }
|
||||
rand = { version = "0.9" }
|
||||
werewolves-macros = { path = "../werewolves-macros" }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { version = "1" }
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
use core::{fmt::Debug, num::NonZeroU8};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{game::DateTime, player::CharacterId};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum DiedTo {
|
||||
Execution {
|
||||
day: NonZeroU8,
|
||||
},
|
||||
MapleWolf {
|
||||
source: CharacterId,
|
||||
night: NonZeroU8,
|
||||
starves_if_fails: bool,
|
||||
},
|
||||
MapleWolfStarved {
|
||||
night: NonZeroU8,
|
||||
},
|
||||
Militia {
|
||||
killer: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
Wolfpack {
|
||||
night: NonZeroU8,
|
||||
},
|
||||
AlphaWolf {
|
||||
killer: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
Shapeshift {
|
||||
into: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
Hunter {
|
||||
killer: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
Guardian {
|
||||
killer: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
}
|
||||
|
||||
impl DiedTo {
|
||||
pub const fn date_time(&self) -> DateTime {
|
||||
match self {
|
||||
DiedTo::Execution { day } => DateTime::Day { number: *day },
|
||||
|
||||
DiedTo::Guardian { killer: _, night }
|
||||
| DiedTo::MapleWolf {
|
||||
source: _,
|
||||
night,
|
||||
starves_if_fails: _,
|
||||
}
|
||||
| DiedTo::MapleWolfStarved { night }
|
||||
| DiedTo::Militia { killer: _, night }
|
||||
| DiedTo::Wolfpack { night }
|
||||
| DiedTo::AlphaWolf { killer: _, night }
|
||||
| DiedTo::Shapeshift { into: _, night }
|
||||
| DiedTo::Hunter { killer: _, night } => DateTime::Night {
|
||||
number: night.get(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{player::CharacterId, role::RoleTitle};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
|
||||
pub enum GameError {
|
||||
#[error("too many roles. have {players} players, but {roles} roles (incl wolves)")]
|
||||
TooManyRoles { players: u8, roles: u8 },
|
||||
#[error("wolves range must start at 1")]
|
||||
NoWolves,
|
||||
#[error("message invalid for game state")]
|
||||
InvalidMessageForGameState,
|
||||
#[error("no executions during night time")]
|
||||
NoExecutionsAtNight,
|
||||
#[error("no-trial not allowed")]
|
||||
NoTrialNotAllowed,
|
||||
#[error("chracter is already dead")]
|
||||
CharacterAlreadyDead,
|
||||
#[error("no matching character found")]
|
||||
NoMatchingCharacterFound,
|
||||
#[error("character not in joined player pool")]
|
||||
CharacterNotInJoinedPlayers,
|
||||
#[error("{0}")]
|
||||
GenericError(String),
|
||||
#[error("invalid cause of death")]
|
||||
InvalidCauseOfDeath,
|
||||
#[error("invalid target")]
|
||||
InvalidTarget,
|
||||
#[error("timed out")]
|
||||
TimedOut,
|
||||
#[error("host channel closed")]
|
||||
HostChannelClosed,
|
||||
#[error("not all players connected")]
|
||||
NotAllPlayersConnected,
|
||||
#[error("too few players: got {got} but the settings require at least {need}")]
|
||||
TooFewPlayers { got: u8, need: u8 },
|
||||
#[error("it's already daytime")]
|
||||
AlreadyDaytime,
|
||||
#[error("it's not the end of the night yet")]
|
||||
NotEndOfNight,
|
||||
#[error("it's not day yet")]
|
||||
NotDayYet,
|
||||
#[error("it's not night")]
|
||||
NotNight,
|
||||
#[error("invalid role, expected {expected:?} got {got:?}")]
|
||||
InvalidRole { expected: RoleTitle, got: RoleTitle },
|
||||
#[error("villagers cannot be added to settings")]
|
||||
CantAddVillagerToSettings,
|
||||
#[error("no mentor for an apprentice to be an apprentice to :(")]
|
||||
NoApprenticeMentor,
|
||||
#[error("BUG: cannot find character in village, but they should be there")]
|
||||
CannotFindTargetButShouldBeThere,
|
||||
#[error("inactive game object")]
|
||||
InactiveGameObject,
|
||||
#[error("socket error: {0}")]
|
||||
SocketError(String),
|
||||
#[error("this night is over")]
|
||||
NightOver,
|
||||
#[error("no night actions")]
|
||||
NoNightActions,
|
||||
#[error("still awaiting response")]
|
||||
AwaitingResponse,
|
||||
#[error("current state already has a response")]
|
||||
NightNeedsNext,
|
||||
#[error("night zero actions can only be obtained on night zero")]
|
||||
NotNightZero,
|
||||
#[error("wolves intro in progress")]
|
||||
WolvesIntroInProgress,
|
||||
#[error("a game is still ongoing")]
|
||||
GameOngoing,
|
||||
#[error("needs a role reveal")]
|
||||
NeedRoleReveal,
|
||||
}
|
||||
|
|
@ -0,0 +1,298 @@
|
|||
mod night;
|
||||
mod settings;
|
||||
mod village;
|
||||
|
||||
use core::{
|
||||
fmt::Debug,
|
||||
num::NonZeroU8,
|
||||
ops::{Deref, Range, RangeBounds},
|
||||
};
|
||||
|
||||
use rand::{Rng, seq::SliceRandom};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
error::GameError,
|
||||
game::night::Night,
|
||||
message::{
|
||||
CharacterState, Identification,
|
||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||
},
|
||||
player::CharacterId,
|
||||
};
|
||||
pub use {settings::GameSettings, village::Village};
|
||||
|
||||
type Result<T> = core::result::Result<T, GameError>;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Game {
|
||||
previous: Vec<GameState>,
|
||||
state: GameState,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
||||
Ok(Self {
|
||||
previous: Vec::new(),
|
||||
state: GameState::Night {
|
||||
night: Night::new(Village::new(players, settings)?)?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn village(&self) -> &Village {
|
||||
match &self.state {
|
||||
GameState::Day { village, marked: _ } => village,
|
||||
GameState::Night { night } => night.village(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process(&mut self, message: HostGameMessage) -> Result<ServerToHostMessage> {
|
||||
match (&mut self.state, message) {
|
||||
(GameState::Night { night }, HostGameMessage::Night(HostNightMessage::Next)) => {
|
||||
night.next()?;
|
||||
self.process(HostGameMessage::GetState)
|
||||
}
|
||||
(
|
||||
GameState::Day { village: _, marked },
|
||||
HostGameMessage::Day(HostDayMessage::MarkForExecution(target)),
|
||||
) => {
|
||||
match marked
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(idx, mark)| (mark == &target).then_some(idx))
|
||||
{
|
||||
Some(idx) => {
|
||||
marked.swap_remove(idx);
|
||||
}
|
||||
None => marked.push(target),
|
||||
}
|
||||
|
||||
self.process(HostGameMessage::GetState)
|
||||
}
|
||||
(GameState::Day { village, marked }, HostGameMessage::Day(HostDayMessage::Execute)) => {
|
||||
if let Some(outcome) = village.execute(marked)? {
|
||||
return Ok(ServerToHostMessage::GameOver(outcome));
|
||||
}
|
||||
let night = Night::new(village.clone())?;
|
||||
self.previous.push(self.state.clone());
|
||||
self.state = GameState::Night { night };
|
||||
self.process(HostGameMessage::GetState)
|
||||
}
|
||||
(GameState::Day { village, marked }, HostGameMessage::GetState) => {
|
||||
if let Some(outcome) = village.is_game_over() {
|
||||
return Ok(ServerToHostMessage::GameOver(outcome));
|
||||
}
|
||||
Ok(ServerToHostMessage::Daytime {
|
||||
marked: marked.clone().into_boxed_slice(),
|
||||
characters: village
|
||||
.characters()
|
||||
.into_iter()
|
||||
.map(|c| CharacterState {
|
||||
player_id: c.player_id().clone(),
|
||||
character_id: c.character_id().clone(),
|
||||
public_identity: c.public_identity().clone(),
|
||||
role: c.role().title(),
|
||||
died_to: c.died_to().cloned(),
|
||||
})
|
||||
.collect(),
|
||||
day: match village.date_time() {
|
||||
DateTime::Day { number } => number,
|
||||
DateTime::Night { number: _ } => unreachable!(),
|
||||
},
|
||||
})
|
||||
}
|
||||
(GameState::Night { night }, HostGameMessage::GetState) => {
|
||||
if let Some(res) = night.current_result() {
|
||||
let char = night.current_character().unwrap();
|
||||
return Ok(ServerToHostMessage::ActionResult(
|
||||
char.public_identity().clone(),
|
||||
res.clone(),
|
||||
));
|
||||
}
|
||||
if let Some(prompt) = night.current_prompt() {
|
||||
let char = night.current_character().unwrap();
|
||||
return Ok(ServerToHostMessage::ActionPrompt(
|
||||
char.public_identity().clone(),
|
||||
prompt.clone(),
|
||||
));
|
||||
}
|
||||
match night.next() {
|
||||
Ok(_) => self.process(HostGameMessage::GetState),
|
||||
Err(GameError::NightOver) => {
|
||||
let village = night.collect_completed()?;
|
||||
self.previous.push(self.state.clone());
|
||||
self.state = GameState::Day {
|
||||
village,
|
||||
marked: Vec::new(),
|
||||
};
|
||||
|
||||
self.process(HostGameMessage::GetState)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
(
|
||||
GameState::Night { night },
|
||||
HostGameMessage::Night(HostNightMessage::ActionResponse(resp)),
|
||||
) => match night.received_response(resp.clone()) {
|
||||
Ok(res) => Ok(ServerToHostMessage::ActionResult(
|
||||
night.current_character().unwrap().public_identity().clone(),
|
||||
res,
|
||||
)),
|
||||
Err(GameError::NightNeedsNext) => match night.next() {
|
||||
Ok(_) => self.process(HostGameMessage::Night(
|
||||
HostNightMessage::ActionResponse(resp),
|
||||
)),
|
||||
Err(GameError::NightOver) => {
|
||||
// since the block handling HostGameMessage::GetState for night
|
||||
// already manages the NightOver state, just invoke it
|
||||
self.process(HostGameMessage::GetState)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
(GameState::Night { night: _ }, HostGameMessage::Day(_))
|
||||
| (
|
||||
GameState::Day {
|
||||
village: _,
|
||||
marked: _,
|
||||
},
|
||||
HostGameMessage::Night(_),
|
||||
) => Err(GameError::InvalidMessageForGameState),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn game_over(&self) -> Option<GameOver> {
|
||||
self.state.game_over()
|
||||
}
|
||||
|
||||
pub fn game_state(&self) -> &GameState {
|
||||
&self.state
|
||||
}
|
||||
|
||||
pub fn previous_game_states(&self) -> &[GameState] {
|
||||
&self.previous
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum GameState {
|
||||
Day {
|
||||
village: Village,
|
||||
marked: Vec<CharacterId>,
|
||||
},
|
||||
Night {
|
||||
night: Night,
|
||||
},
|
||||
}
|
||||
|
||||
impl GameState {
|
||||
pub fn game_over(&self) -> Option<GameOver> {
|
||||
match self {
|
||||
GameState::Day { village, marked: _ } => village.is_game_over(),
|
||||
GameState::Night { night: _ } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum GameOver {
|
||||
VillageWins,
|
||||
WolvesWin,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Pool<T>
|
||||
where
|
||||
T: Debug + Clone,
|
||||
{
|
||||
pub pool: Vec<T>,
|
||||
pub range: Range<u8>,
|
||||
}
|
||||
|
||||
impl<T> Pool<T>
|
||||
where
|
||||
T: Debug + Clone,
|
||||
{
|
||||
pub const fn new(pool: Vec<T>, range: Range<u8>) -> Self {
|
||||
Self { pool, range }
|
||||
}
|
||||
|
||||
pub fn collapse(mut self, rng: &mut impl Rng, max: u8) -> Vec<T> {
|
||||
let range = match self.range.end_bound() {
|
||||
core::ops::Bound::Included(end) => {
|
||||
if max < *end {
|
||||
self.range.start..max + 1
|
||||
} else {
|
||||
self.range.clone()
|
||||
}
|
||||
}
|
||||
core::ops::Bound::Excluded(end) => {
|
||||
if max <= *end {
|
||||
self.range.start..max + 1
|
||||
} else {
|
||||
self.range.clone()
|
||||
}
|
||||
}
|
||||
core::ops::Bound::Unbounded => self.range.start..max + 1,
|
||||
};
|
||||
let count = rng.random_range(range);
|
||||
self.pool.shuffle(rng);
|
||||
self.pool.truncate(count as _);
|
||||
self.pool
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Pool<T>
|
||||
where
|
||||
T: Debug + Clone,
|
||||
{
|
||||
type Target = [T];
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.pool
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Maybe {
|
||||
Yes,
|
||||
No,
|
||||
Maybe,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum DateTime {
|
||||
Day { number: NonZeroU8 },
|
||||
Night { number: u8 },
|
||||
}
|
||||
|
||||
impl Default for DateTime {
|
||||
fn default() -> Self {
|
||||
DateTime::Day {
|
||||
number: NonZeroU8::new(1).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DateTime {
|
||||
pub const fn is_day(&self) -> bool {
|
||||
matches!(self, DateTime::Day { number: _ })
|
||||
}
|
||||
|
||||
pub const fn is_night(&self) -> bool {
|
||||
matches!(self, DateTime::Night { number: _ })
|
||||
}
|
||||
|
||||
pub const fn next(self) -> Self {
|
||||
match self {
|
||||
DateTime::Day { number } => DateTime::Night {
|
||||
number: number.get(),
|
||||
},
|
||||
DateTime::Night { number } => DateTime::Day {
|
||||
number: NonZeroU8::new(number + 1).unwrap(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,964 @@
|
|||
use core::{num::NonZeroU8, ops::Not};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use werewolves_macros::Extract;
|
||||
|
||||
use super::Result;
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{DateTime, Village},
|
||||
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
||||
player::{Character, CharacterId, Protection},
|
||||
role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Night {
|
||||
village: Village,
|
||||
night: u8,
|
||||
action_queue: VecDeque<(ActionPrompt, Character)>,
|
||||
changes: Vec<NightChange>,
|
||||
night_state: NightState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Extract)]
|
||||
pub enum NightChange {
|
||||
RoleChange(CharacterId, RoleTitle),
|
||||
HunterTarget {
|
||||
source: CharacterId,
|
||||
target: CharacterId,
|
||||
},
|
||||
Kill {
|
||||
target: CharacterId,
|
||||
died_to: DiedTo,
|
||||
},
|
||||
RoleBlock {
|
||||
source: CharacterId,
|
||||
target: CharacterId,
|
||||
block_type: RoleBlock,
|
||||
},
|
||||
Shapeshift {
|
||||
source: CharacterId,
|
||||
},
|
||||
Protection {
|
||||
target: CharacterId,
|
||||
protection: Protection,
|
||||
},
|
||||
}
|
||||
|
||||
struct ResponseOutcome {
|
||||
pub result: ActionResult,
|
||||
pub change: Option<NightChange>,
|
||||
pub unless: Option<Unless>,
|
||||
}
|
||||
|
||||
enum Unless {
|
||||
TargetBlocked(CharacterId),
|
||||
TargetsBlocked(CharacterId, CharacterId),
|
||||
}
|
||||
|
||||
impl From<Unless> for ActionResult {
|
||||
fn from(value: Unless) -> Self {
|
||||
match value {
|
||||
Unless::TargetBlocked(_) | Unless::TargetsBlocked(_, _) => ActionResult::RoleBlocked,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
enum NightState {
|
||||
Active {
|
||||
current_prompt: ActionPrompt,
|
||||
current_char: CharacterId,
|
||||
current_result: Option<ActionResult>,
|
||||
},
|
||||
Complete,
|
||||
}
|
||||
|
||||
impl Night {
|
||||
pub fn new(village: Village) -> Result<Self> {
|
||||
let night = match village.date_time() {
|
||||
DateTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||
DateTime::Night { number } => number,
|
||||
};
|
||||
|
||||
let mut action_queue = village
|
||||
.characters()
|
||||
.into_iter()
|
||||
.map(|c| c.night_action_prompt(&village).map(|prompt| (prompt, c)))
|
||||
.collect::<Result<Box<[_]>>>()?
|
||||
.into_iter()
|
||||
.filter_map(|(prompt, char)| prompt.map(|p| (p, char)))
|
||||
.collect::<Vec<_>>();
|
||||
action_queue.sort_by(|(left_prompt, _), (right_prompt, _)| {
|
||||
left_prompt
|
||||
.partial_cmp(right_prompt)
|
||||
.unwrap_or(core::cmp::Ordering::Equal)
|
||||
});
|
||||
let mut action_queue = VecDeque::from(action_queue);
|
||||
let (current_prompt, current_char) = if night == 0 {
|
||||
(
|
||||
ActionPrompt::WolvesIntro {
|
||||
wolves: village
|
||||
.living_wolf_pack_players()
|
||||
.into_iter()
|
||||
.map(|w| (w.target(), w.role().title()))
|
||||
.collect(),
|
||||
},
|
||||
village
|
||||
.living_wolf_pack_players()
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.character_id()
|
||||
.clone(),
|
||||
)
|
||||
} else {
|
||||
action_queue
|
||||
.pop_front()
|
||||
.map(|(p, c)| (p, c.character_id().clone()))
|
||||
.ok_or(GameError::NoNightActions)?
|
||||
};
|
||||
let night_state = NightState::Active {
|
||||
current_char,
|
||||
current_prompt,
|
||||
current_result: None,
|
||||
};
|
||||
let mut changes = Vec::new();
|
||||
if let Some(night_nz) = NonZeroU8::new(night) {
|
||||
// TODO: prob should be an end-of-night thing
|
||||
changes = village
|
||||
.dead_characters()
|
||||
.into_iter()
|
||||
.filter_map(|c| c.died_to().map(|d| (c, d)))
|
||||
.filter_map(|(c, d)| match c.role() {
|
||||
Role::Hunter { target } => target.clone().map(|t| (c, t, d)),
|
||||
_ => None,
|
||||
})
|
||||
.filter_map(|(c, t, d)| match d.date_time() {
|
||||
DateTime::Day { number } => (number.get() == night).then_some((c, t)),
|
||||
DateTime::Night { number: _ } => None,
|
||||
})
|
||||
.map(|(c, target)| NightChange::Kill {
|
||||
target,
|
||||
died_to: DiedTo::Hunter {
|
||||
killer: c.character_id().clone(),
|
||||
night: night_nz,
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
night,
|
||||
changes,
|
||||
village,
|
||||
night_state,
|
||||
action_queue,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn collect_completed(&self) -> Result<Village> {
|
||||
if !matches!(self.night_state, NightState::Complete) {
|
||||
return Err(GameError::NotEndOfNight);
|
||||
}
|
||||
let mut new_village = self.village.clone();
|
||||
let changes = ChangesLookup(&self.changes);
|
||||
for change in self.changes.iter() {
|
||||
match change {
|
||||
NightChange::RoleChange(character_id, role_title) => new_village
|
||||
.character_by_id_mut(character_id)
|
||||
.unwrap()
|
||||
.role_change(*role_title, DateTime::Night { number: self.night })?,
|
||||
NightChange::HunterTarget { source, target } => {
|
||||
if let Role::Hunter { target: t } =
|
||||
new_village.character_by_id_mut(source).unwrap().role_mut()
|
||||
{
|
||||
t.replace(target.clone());
|
||||
}
|
||||
if changes.killed(source).is_some()
|
||||
&& changes.protected(source).is_none()
|
||||
&& changes.protected(target).is_none()
|
||||
{
|
||||
new_village
|
||||
.character_by_id_mut(target)
|
||||
.unwrap()
|
||||
.kill(DiedTo::Hunter {
|
||||
killer: source.clone(),
|
||||
night: NonZeroU8::new(self.night).unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
NightChange::Kill { target, died_to } => {
|
||||
if let DiedTo::MapleWolf {
|
||||
source,
|
||||
night,
|
||||
starves_if_fails: true,
|
||||
} = died_to
|
||||
&& changes.protected(target).is_some()
|
||||
{
|
||||
// kill maple first, then act as if they get their kill attempt
|
||||
new_village
|
||||
.character_by_id_mut(source)
|
||||
.unwrap()
|
||||
.kill(DiedTo::MapleWolfStarved { night: *night });
|
||||
}
|
||||
|
||||
if let Some(prot) = changes.protected(target) {
|
||||
match prot {
|
||||
Protection::Guardian {
|
||||
source,
|
||||
guarding: true,
|
||||
} => {
|
||||
let kill_source = match died_to {
|
||||
DiedTo::MapleWolfStarved { night } => {
|
||||
new_village
|
||||
.character_by_id_mut(target)
|
||||
.unwrap()
|
||||
.kill(DiedTo::MapleWolfStarved { night: *night });
|
||||
continue;
|
||||
}
|
||||
DiedTo::Execution { day: _ } => unreachable!(),
|
||||
DiedTo::MapleWolf {
|
||||
source,
|
||||
night: _,
|
||||
starves_if_fails: _,
|
||||
}
|
||||
| DiedTo::Militia {
|
||||
killer: source,
|
||||
night: _,
|
||||
}
|
||||
| DiedTo::AlphaWolf {
|
||||
killer: source,
|
||||
night: _,
|
||||
}
|
||||
| DiedTo::Hunter {
|
||||
killer: source,
|
||||
night: _,
|
||||
} => source.clone(),
|
||||
DiedTo::Wolfpack { night: _ } => {
|
||||
if let Some(wolf_to_kill) = new_village
|
||||
.living_wolf_pack_players()
|
||||
.into_iter()
|
||||
.find(|w| matches!(w.role(), Role::Werewolf))
|
||||
.map(|w| w.character_id().clone())
|
||||
.or_else(|| {
|
||||
new_village
|
||||
.living_wolf_pack_players()
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|w| w.character_id().clone())
|
||||
})
|
||||
{
|
||||
wolf_to_kill
|
||||
} else {
|
||||
// No wolves? Game over?
|
||||
continue;
|
||||
}
|
||||
}
|
||||
DiedTo::Shapeshift { into: _, night: _ } => target.clone(),
|
||||
DiedTo::Guardian {
|
||||
killer: _,
|
||||
night: _,
|
||||
} => continue,
|
||||
};
|
||||
new_village.character_by_id_mut(&kill_source).unwrap().kill(
|
||||
DiedTo::Guardian {
|
||||
killer: source.clone(),
|
||||
night: NonZeroU8::new(self.night).unwrap(),
|
||||
},
|
||||
);
|
||||
new_village.character_by_id_mut(source).unwrap().kill(
|
||||
DiedTo::Wolfpack {
|
||||
night: NonZeroU8::new(self.night).unwrap(),
|
||||
},
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Protection::Guardian {
|
||||
source: _,
|
||||
guarding: false,
|
||||
} => continue,
|
||||
Protection::Protector { source: _ } => continue,
|
||||
}
|
||||
}
|
||||
|
||||
new_village
|
||||
.character_by_id_mut(target)
|
||||
.unwrap()
|
||||
.kill(DiedTo::Wolfpack {
|
||||
night: NonZeroU8::new(self.night).unwrap(),
|
||||
});
|
||||
}
|
||||
NightChange::Shapeshift { source } => {
|
||||
// TODO: shapeshift should probably notify immediately after it happens
|
||||
if let Some(target) = changes.wolf_pack_kill_target()
|
||||
&& changes.protected(target).is_none()
|
||||
{
|
||||
let ss = new_village.character_by_id_mut(source).unwrap();
|
||||
match ss.role_mut() {
|
||||
Role::Shapeshifter { shifted_into } => {
|
||||
*shifted_into = Some(target.clone())
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
ss.kill(DiedTo::Shapeshift {
|
||||
into: target.clone(),
|
||||
night: NonZeroU8::new(self.night).unwrap(),
|
||||
});
|
||||
let target = new_village.find_by_character_id_mut(target).unwrap();
|
||||
target
|
||||
.role_change(
|
||||
RoleTitle::Werewolf,
|
||||
DateTime::Night { number: self.night },
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
NightChange::RoleBlock {
|
||||
source: _,
|
||||
target: _,
|
||||
block_type: _,
|
||||
}
|
||||
| NightChange::Protection {
|
||||
target: _,
|
||||
protection: _,
|
||||
} => {}
|
||||
}
|
||||
}
|
||||
new_village.to_day()?;
|
||||
Ok(new_village)
|
||||
}
|
||||
|
||||
pub fn received_response(&mut self, resp: ActionResponse) -> Result<ActionResult> {
|
||||
if let (
|
||||
NightState::Active {
|
||||
current_prompt: ActionPrompt::WolvesIntro { wolves: _ },
|
||||
current_char: _,
|
||||
current_result,
|
||||
},
|
||||
ActionResponse::WolvesIntroAck,
|
||||
) = (&mut self.night_state, &resp)
|
||||
{
|
||||
*current_result = Some(ActionResult::WolvesIntroDone);
|
||||
return Ok(ActionResult::WolvesIntroDone);
|
||||
}
|
||||
|
||||
match self.received_response_with_role_blocks(resp) {
|
||||
Ok((result, Some(change))) => {
|
||||
match &mut self.night_state {
|
||||
NightState::Active {
|
||||
current_prompt: _,
|
||||
current_char: _,
|
||||
current_result,
|
||||
} => current_result.replace(result.clone()),
|
||||
NightState::Complete => return Err(GameError::NightOver),
|
||||
};
|
||||
self.changes.push(change);
|
||||
Ok(result)
|
||||
}
|
||||
Ok((result, None)) => {
|
||||
match &mut self.night_state {
|
||||
NightState::Active {
|
||||
current_prompt: _,
|
||||
current_char: _,
|
||||
current_result,
|
||||
} => {
|
||||
current_result.replace(result.clone());
|
||||
}
|
||||
NightState::Complete => return Err(GameError::NightOver),
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
fn received_response_with_role_blocks(
|
||||
&self,
|
||||
resp: ActionResponse,
|
||||
) -> Result<(ActionResult, Option<NightChange>)> {
|
||||
match self.received_response_inner(resp) {
|
||||
Ok(ResponseOutcome {
|
||||
result,
|
||||
change,
|
||||
unless: Some(Unless::TargetBlocked(unless_blocked)),
|
||||
}) => {
|
||||
if self.changes.iter().any(|c| match c {
|
||||
NightChange::RoleBlock {
|
||||
source: _,
|
||||
target,
|
||||
block_type: _,
|
||||
} => target == &unless_blocked,
|
||||
_ => false,
|
||||
}) {
|
||||
Ok((ActionResult::RoleBlocked, None))
|
||||
} else {
|
||||
Ok((result, change))
|
||||
}
|
||||
}
|
||||
Ok(ResponseOutcome {
|
||||
result,
|
||||
change,
|
||||
unless: Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)),
|
||||
}) => {
|
||||
if self.changes.iter().any(|c| match c {
|
||||
NightChange::RoleBlock {
|
||||
source: _,
|
||||
target,
|
||||
block_type: _,
|
||||
} => target == &unless_blocked1 || target == &unless_blocked2,
|
||||
_ => false,
|
||||
}) {
|
||||
Ok((ActionResult::RoleBlocked, None))
|
||||
} else {
|
||||
Ok((result, change))
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ResponseOutcome {
|
||||
result,
|
||||
change,
|
||||
unless: None,
|
||||
}) => Ok((result, change)),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn received_response_inner(&self, resp: ActionResponse) -> Result<ResponseOutcome> {
|
||||
let (current_prompt, current_char) = match &self.night_state {
|
||||
NightState::Active {
|
||||
current_prompt: _,
|
||||
current_char: _,
|
||||
current_result: Some(_),
|
||||
} => return Err(GameError::NightNeedsNext),
|
||||
NightState::Active {
|
||||
current_prompt,
|
||||
current_char,
|
||||
current_result: None,
|
||||
} => (current_prompt, current_char),
|
||||
NightState::Complete => return Err(GameError::NightOver),
|
||||
};
|
||||
|
||||
match (current_prompt, resp) {
|
||||
(ActionPrompt::RoleChange { new_role }, ActionResponse::RoleChangeAck) => {
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::RoleChange(current_char.clone(), *new_role)),
|
||||
unless: None,
|
||||
})
|
||||
}
|
||||
(ActionPrompt::Seer { living_players }, ActionResponse::Seer(target)) => {
|
||||
if !living_players.iter().any(|p| p.character_id == target) {
|
||||
return Err(GameError::InvalidTarget);
|
||||
}
|
||||
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::Seer(
|
||||
self.village
|
||||
.character_by_id(&target)
|
||||
.unwrap()
|
||||
.role()
|
||||
.alignment(),
|
||||
),
|
||||
change: None,
|
||||
unless: Some(Unless::TargetBlocked(target)),
|
||||
})
|
||||
}
|
||||
(
|
||||
ActionPrompt::Arcanist { living_players },
|
||||
ActionResponse::Arcanist(target1, target2),
|
||||
) => {
|
||||
if !(living_players.iter().any(|p| p.character_id == target1)
|
||||
&& living_players.iter().any(|p| p.character_id == target2))
|
||||
{
|
||||
return Err(GameError::InvalidTarget);
|
||||
}
|
||||
let target1_align = self
|
||||
.village
|
||||
.character_by_id(&target1)
|
||||
.unwrap()
|
||||
.role()
|
||||
.alignment();
|
||||
let target2_align = self
|
||||
.village
|
||||
.character_by_id(&target2)
|
||||
.unwrap()
|
||||
.role()
|
||||
.alignment();
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::Arcanist {
|
||||
same: target1_align == target2_align,
|
||||
},
|
||||
change: None,
|
||||
unless: Some(Unless::TargetsBlocked(target1, target2)),
|
||||
})
|
||||
}
|
||||
(ActionPrompt::Gravedigger { dead_players }, ActionResponse::Gravedigger(target)) => {
|
||||
if !dead_players.iter().any(|p| p.character_id == target) {
|
||||
return Err(GameError::InvalidTarget);
|
||||
}
|
||||
let target_role = self.village.character_by_id(&target).unwrap().role();
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GraveDigger(
|
||||
matches!(
|
||||
target_role,
|
||||
Role::Shapeshifter {
|
||||
shifted_into: Some(_)
|
||||
}
|
||||
)
|
||||
.not()
|
||||
.then(|| target_role.title()),
|
||||
),
|
||||
change: None,
|
||||
unless: Some(Unless::TargetBlocked(target)),
|
||||
})
|
||||
}
|
||||
(
|
||||
ActionPrompt::Hunter {
|
||||
current_target: _,
|
||||
living_players,
|
||||
},
|
||||
ActionResponse::Hunter(target),
|
||||
) => {
|
||||
if !living_players.iter().any(|p| p.character_id == target) {
|
||||
return Err(GameError::InvalidTarget);
|
||||
}
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::HunterTarget {
|
||||
target: target.clone(),
|
||||
source: current_char.clone(),
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(target)),
|
||||
})
|
||||
}
|
||||
(ActionPrompt::Militia { living_players }, ActionResponse::Militia(Some(target))) => {
|
||||
if !living_players.iter().any(|p| p.character_id == target) {
|
||||
return Err(GameError::InvalidTarget);
|
||||
}
|
||||
let night = if let Some(night) = NonZeroU8::new(self.night) {
|
||||
night
|
||||
} else {
|
||||
return Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: None,
|
||||
unless: None,
|
||||
});
|
||||
};
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::Kill {
|
||||
target: target.clone(),
|
||||
died_to: DiedTo::Militia {
|
||||
night,
|
||||
killer: current_char.clone(),
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(target)),
|
||||
})
|
||||
}
|
||||
(ActionPrompt::Militia { living_players: _ }, ActionResponse::Militia(None)) => {
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: None,
|
||||
unless: None,
|
||||
})
|
||||
}
|
||||
(
|
||||
ActionPrompt::MapleWolf {
|
||||
kill_or_die,
|
||||
living_players,
|
||||
},
|
||||
ActionResponse::MapleWolf(Some(target)),
|
||||
) => {
|
||||
if !living_players.iter().any(|p| p.character_id == target) {
|
||||
return Err(GameError::InvalidTarget);
|
||||
}
|
||||
let night = if let Some(night) = NonZeroU8::new(self.night) {
|
||||
night
|
||||
} else {
|
||||
return Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: None,
|
||||
unless: None,
|
||||
});
|
||||
};
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::Kill {
|
||||
target: target.clone(),
|
||||
died_to: DiedTo::MapleWolf {
|
||||
night,
|
||||
source: current_char.clone(),
|
||||
starves_if_fails: *kill_or_die,
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(target)),
|
||||
})
|
||||
}
|
||||
(
|
||||
ActionPrompt::MapleWolf {
|
||||
kill_or_die: true,
|
||||
living_players: _,
|
||||
},
|
||||
ActionResponse::MapleWolf(None),
|
||||
) => {
|
||||
let night = if let Some(night) = NonZeroU8::new(self.night) {
|
||||
night
|
||||
} else {
|
||||
return Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: None,
|
||||
unless: None,
|
||||
});
|
||||
};
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::Kill {
|
||||
target: current_char.clone(),
|
||||
died_to: DiedTo::MapleWolfStarved { night },
|
||||
}),
|
||||
unless: None,
|
||||
})
|
||||
}
|
||||
|
||||
(
|
||||
ActionPrompt::MapleWolf {
|
||||
kill_or_die: false,
|
||||
living_players: _,
|
||||
},
|
||||
ActionResponse::MapleWolf(None),
|
||||
) => Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: None,
|
||||
unless: None,
|
||||
}),
|
||||
(
|
||||
ActionPrompt::Guardian {
|
||||
previous: Some(previous),
|
||||
living_players,
|
||||
},
|
||||
ActionResponse::Guardian(target),
|
||||
) => {
|
||||
if !living_players.iter().any(|p| p.character_id == target) {
|
||||
return Err(GameError::InvalidTarget);
|
||||
}
|
||||
let guarding = match previous {
|
||||
PreviousGuardianAction::Protect(prev_target) => {
|
||||
prev_target.character_id == target
|
||||
}
|
||||
PreviousGuardianAction::Guard(prev_target) => {
|
||||
if prev_target.character_id == target {
|
||||
return Err(GameError::InvalidTarget);
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::Protection {
|
||||
target: target.clone(),
|
||||
protection: Protection::Guardian {
|
||||
source: current_char.clone(),
|
||||
guarding,
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(target)),
|
||||
})
|
||||
}
|
||||
(
|
||||
ActionPrompt::Guardian {
|
||||
previous: None,
|
||||
living_players,
|
||||
},
|
||||
ActionResponse::Guardian(target),
|
||||
) => {
|
||||
if !living_players.iter().any(|p| p.character_id == target) {
|
||||
return Err(GameError::InvalidTarget);
|
||||
}
|
||||
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::Protection {
|
||||
target: target.clone(),
|
||||
protection: Protection::Guardian {
|
||||
source: current_char.clone(),
|
||||
guarding: false,
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(target)),
|
||||
})
|
||||
}
|
||||
(
|
||||
ActionPrompt::WolfPackKill { living_villagers },
|
||||
ActionResponse::WolfPackKillVote(target),
|
||||
) => {
|
||||
let night = match NonZeroU8::new(self.night) {
|
||||
Some(night) => night,
|
||||
None => {
|
||||
return Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: None,
|
||||
unless: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
if !living_villagers.iter().any(|p| p.character_id == target) {
|
||||
return Err(GameError::InvalidTarget);
|
||||
}
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::Kill {
|
||||
target: target.clone(),
|
||||
died_to: DiedTo::Wolfpack { night },
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(target)),
|
||||
})
|
||||
}
|
||||
(ActionPrompt::Shapeshifter, ActionResponse::Shapeshifter(true)) => {
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::Shapeshift {
|
||||
source: current_char.clone(),
|
||||
}),
|
||||
unless: None,
|
||||
})
|
||||
}
|
||||
(
|
||||
ActionPrompt::AlphaWolf {
|
||||
living_villagers: _,
|
||||
},
|
||||
ActionResponse::AlphaWolf(None),
|
||||
)
|
||||
| (ActionPrompt::Shapeshifter, ActionResponse::Shapeshifter(false)) => {
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: None,
|
||||
unless: None,
|
||||
})
|
||||
}
|
||||
(
|
||||
ActionPrompt::AlphaWolf { living_villagers },
|
||||
ActionResponse::AlphaWolf(Some(target)),
|
||||
) => {
|
||||
let night = match NonZeroU8::new(self.night) {
|
||||
Some(night) => night,
|
||||
None => {
|
||||
return Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: None,
|
||||
unless: None,
|
||||
});
|
||||
}
|
||||
};
|
||||
if !living_villagers.iter().any(|p| p.character_id == target) {
|
||||
return Err(GameError::InvalidTarget);
|
||||
}
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::Kill {
|
||||
target: target.clone(),
|
||||
died_to: DiedTo::AlphaWolf {
|
||||
killer: current_char.clone(),
|
||||
night,
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(target)),
|
||||
})
|
||||
}
|
||||
(ActionPrompt::DireWolf { living_players }, ActionResponse::Direwolf(target)) => {
|
||||
if !living_players.iter().any(|p| p.character_id == target) {
|
||||
return Err(GameError::InvalidTarget);
|
||||
}
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::RoleBlock {
|
||||
source: current_char.clone(),
|
||||
target,
|
||||
block_type: RoleBlock::Direwolf,
|
||||
}),
|
||||
unless: None,
|
||||
})
|
||||
}
|
||||
(ActionPrompt::Protector { targets }, ActionResponse::Protector(target)) => {
|
||||
if !targets.iter().any(|p| p.character_id == target) {
|
||||
return Err(GameError::InvalidTarget);
|
||||
}
|
||||
|
||||
Ok(ResponseOutcome {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::Protection {
|
||||
target: target.clone(),
|
||||
protection: Protection::Protector {
|
||||
source: current_char.clone(),
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(target)),
|
||||
})
|
||||
}
|
||||
|
||||
// For other responses that are invalid -- this allows the match to error
|
||||
// if a new prompt is added
|
||||
(ActionPrompt::RoleChange { new_role: _ }, _)
|
||||
| (ActionPrompt::Seer { living_players: _ }, _)
|
||||
| (ActionPrompt::Protector { targets: _ }, _)
|
||||
| (ActionPrompt::Arcanist { living_players: _ }, _)
|
||||
| (ActionPrompt::Gravedigger { dead_players: _ }, _)
|
||||
| (
|
||||
ActionPrompt::Hunter {
|
||||
current_target: _,
|
||||
living_players: _,
|
||||
},
|
||||
_,
|
||||
)
|
||||
| (ActionPrompt::Militia { living_players: _ }, _)
|
||||
| (
|
||||
ActionPrompt::MapleWolf {
|
||||
kill_or_die: _,
|
||||
living_players: _,
|
||||
},
|
||||
_,
|
||||
)
|
||||
| (
|
||||
ActionPrompt::Guardian {
|
||||
previous: _,
|
||||
living_players: _,
|
||||
},
|
||||
_,
|
||||
)
|
||||
| (
|
||||
ActionPrompt::WolfPackKill {
|
||||
living_villagers: _,
|
||||
},
|
||||
_,
|
||||
)
|
||||
| (ActionPrompt::Shapeshifter, _)
|
||||
| (
|
||||
ActionPrompt::AlphaWolf {
|
||||
living_villagers: _,
|
||||
},
|
||||
_,
|
||||
)
|
||||
| (ActionPrompt::DireWolf { living_players: _ }, _) => {
|
||||
Err(GameError::InvalidMessageForGameState)
|
||||
}
|
||||
|
||||
(ActionPrompt::WolvesIntro { wolves: _ }, _) => {
|
||||
Err(GameError::InvalidMessageForGameState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn village(&self) -> &Village {
|
||||
&self.village
|
||||
}
|
||||
|
||||
pub const fn current_result(&self) -> Option<&ActionResult> {
|
||||
match &self.night_state {
|
||||
NightState::Active {
|
||||
current_prompt: _,
|
||||
current_char: _,
|
||||
current_result,
|
||||
} => current_result.as_ref(),
|
||||
NightState::Complete => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn current_prompt(&self) -> Option<&ActionPrompt> {
|
||||
match &self.night_state {
|
||||
NightState::Active {
|
||||
current_prompt,
|
||||
current_char: _,
|
||||
current_result: _,
|
||||
} => Some(current_prompt),
|
||||
NightState::Complete => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn current_character_id(&self) -> Option<&CharacterId> {
|
||||
match &self.night_state {
|
||||
NightState::Active {
|
||||
current_prompt: _,
|
||||
current_char,
|
||||
current_result: _,
|
||||
} => Some(current_char),
|
||||
NightState::Complete => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_character(&self) -> Option<&Character> {
|
||||
self.current_character_id()
|
||||
.and_then(|id| self.village.character_by_id(id))
|
||||
}
|
||||
|
||||
pub const fn complete(&self) -> bool {
|
||||
matches!(self.night_state, NightState::Complete)
|
||||
}
|
||||
|
||||
pub fn next(&mut self) -> Result<()> {
|
||||
match &self.night_state {
|
||||
NightState::Active {
|
||||
current_prompt: _,
|
||||
current_char: _,
|
||||
current_result: Some(_),
|
||||
} => {}
|
||||
NightState::Active {
|
||||
current_prompt: _,
|
||||
current_char: _,
|
||||
current_result: None,
|
||||
} => return Err(GameError::AwaitingResponse),
|
||||
NightState::Complete => return Err(GameError::NightOver),
|
||||
}
|
||||
if let Some((prompt, character)) = self.action_queue.pop_front() {
|
||||
self.night_state = NightState::Active {
|
||||
current_prompt: prompt,
|
||||
current_char: character.character_id().clone(),
|
||||
current_result: None,
|
||||
};
|
||||
} else {
|
||||
self.night_state = NightState::Complete;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const fn changes(&self) -> &[NightChange] {
|
||||
self.changes.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
struct ChangesLookup<'a>(&'a [NightChange]);
|
||||
|
||||
impl<'a> ChangesLookup<'a> {
|
||||
pub fn killed(&self, target: &CharacterId) -> Option<&'a DiedTo> {
|
||||
self.0.iter().find_map(|c| match c {
|
||||
NightChange::Kill { target: t, died_to } => (t == target).then_some(died_to),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> {
|
||||
self.0.iter().find_map(|c| match c {
|
||||
NightChange::Protection {
|
||||
target: t,
|
||||
protection,
|
||||
} => (t == target).then_some(protection),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn wolf_pack_kill_target(&self) -> Option<&'a CharacterId> {
|
||||
self.0.iter().find_map(|c| match c {
|
||||
NightChange::Kill {
|
||||
target,
|
||||
died_to: DiedTo::Wolfpack { night: _ },
|
||||
} => Some(target),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
use super::Result;
|
||||
use core::num::NonZeroU8;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{error::GameError, role::RoleTitle};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct GameSettings {
|
||||
roles: HashMap<RoleTitle, NonZeroU8>,
|
||||
}
|
||||
|
||||
impl Default for GameSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
roles: [
|
||||
(RoleTitle::Werewolf, NonZeroU8::new(1).unwrap()),
|
||||
(RoleTitle::Seer, NonZeroU8::new(1).unwrap()),
|
||||
// (RoleTitle::Militia, NonZeroU8::new(1).unwrap()),
|
||||
// (RoleTitle::Guardian, NonZeroU8::new(1).unwrap()),
|
||||
(RoleTitle::Apprentice, NonZeroU8::new(1).unwrap()),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GameSettings {
|
||||
pub fn spread(&self) -> Box<[RoleTitle]> {
|
||||
self.roles
|
||||
.iter()
|
||||
.flat_map(|(r, c)| [*r].repeat(c.get() as _))
|
||||
.collect()
|
||||
}
|
||||
pub fn wolves_count(&self) -> usize {
|
||||
self.roles
|
||||
.iter()
|
||||
.filter_map(|(r, c)| {
|
||||
if r.wolf() {
|
||||
Some(c.get() as usize)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
pub fn village_roles_count(&self) -> usize {
|
||||
self.roles
|
||||
.iter()
|
||||
.filter_map(|(r, c)| {
|
||||
if !r.wolf() {
|
||||
Some(c.get() as usize)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
pub fn roles(&self) -> Box<[(RoleTitle, NonZeroU8)]> {
|
||||
self.roles.iter().map(|(r, c)| (*r, *c)).collect()
|
||||
}
|
||||
|
||||
pub fn villagers_needed_for_player_count(&self, players: usize) -> Result<usize> {
|
||||
let min = self.min_players_needed();
|
||||
if min > players {
|
||||
return Err(GameError::TooFewPlayers {
|
||||
got: players as _,
|
||||
need: min as _,
|
||||
});
|
||||
}
|
||||
Ok(players - self.roles.values().map(|c| c.get() as usize).sum::<usize>())
|
||||
}
|
||||
|
||||
pub fn check(&self) -> Result<()> {
|
||||
if self.wolves_count() == 0 {
|
||||
return Err(GameError::NoWolves);
|
||||
}
|
||||
if self
|
||||
.roles
|
||||
.iter()
|
||||
.any(|(r, _)| matches!(r, RoleTitle::Apprentice))
|
||||
&& self.roles.iter().filter(|(r, _)| r.is_mentor()).count() == 0
|
||||
{
|
||||
return Err(GameError::NoApprenticeMentor);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn min_players_needed(&self) -> usize {
|
||||
let (wolves, villagers) = (self.wolves_count(), self.village_roles_count());
|
||||
|
||||
if wolves > villagers {
|
||||
wolves + 1 + wolves
|
||||
} else if wolves < villagers {
|
||||
wolves + villagers
|
||||
} else {
|
||||
wolves + villagers + 1
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, role: RoleTitle) -> Result<()> {
|
||||
if role == RoleTitle::Villager {
|
||||
return Err(GameError::CantAddVillagerToSettings);
|
||||
}
|
||||
match self.roles.get_mut(&role) {
|
||||
Some(count) => *count = NonZeroU8::new(count.get() + 1).unwrap(),
|
||||
None => {
|
||||
self.roles.insert(role, NonZeroU8::new(1).unwrap());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sub(&mut self, role: RoleTitle) {
|
||||
if let Some(count) = self.roles.get_mut(&role)
|
||||
&& count.get() != 1
|
||||
{
|
||||
*count = NonZeroU8::new(count.get() - 1).unwrap();
|
||||
} else {
|
||||
self.roles.remove(&role);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,248 @@
|
|||
use core::num::NonZeroU8;
|
||||
|
||||
use rand::{Rng, seq::SliceRandom};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::Result;
|
||||
use crate::{
|
||||
error::GameError,
|
||||
game::{DateTime, GameOver, GameSettings},
|
||||
message::{Identification, Target},
|
||||
player::{Character, CharacterId, PlayerId},
|
||||
role::{Role, RoleTitle},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Village {
|
||||
characters: Vec<Character>,
|
||||
date_time: DateTime,
|
||||
}
|
||||
|
||||
impl Village {
|
||||
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
||||
if settings.min_players_needed() > players.len() {
|
||||
return Err(GameError::TooManyRoles {
|
||||
players: players.len() as u8,
|
||||
roles: settings.min_players_needed() as u8,
|
||||
});
|
||||
}
|
||||
settings.check()?;
|
||||
|
||||
let roles_spread = settings.spread();
|
||||
let potential_apprentice_havers = roles_spread
|
||||
.iter()
|
||||
.filter(|r| r.is_mentor())
|
||||
.map(|r| r.title_to_role_excl_apprentice())
|
||||
.collect::<Box<[_]>>();
|
||||
|
||||
let mut roles = roles_spread
|
||||
.into_iter()
|
||||
.chain(
|
||||
(0..settings.villagers_needed_for_player_count(players.len())?)
|
||||
.map(|_| RoleTitle::Villager),
|
||||
)
|
||||
.map(|title| match title {
|
||||
RoleTitle::Apprentice => Role::Apprentice(Box::new(
|
||||
potential_apprentice_havers
|
||||
[rand::rng().random_range(0..potential_apprentice_havers.len())]
|
||||
.clone(),
|
||||
)),
|
||||
_ => title.title_to_role_excl_apprentice(),
|
||||
})
|
||||
.collect::<Box<[_]>>();
|
||||
|
||||
assert_eq!(players.len(), roles.len());
|
||||
roles.shuffle(&mut rand::rng());
|
||||
Ok(Self {
|
||||
characters: players
|
||||
.iter()
|
||||
.cloned()
|
||||
.zip(roles)
|
||||
.map(|(player, role)| Character::new(player, role))
|
||||
.collect(),
|
||||
date_time: DateTime::Night { number: 0 },
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn date_time(&self) -> DateTime {
|
||||
self.date_time
|
||||
}
|
||||
|
||||
pub fn find_by_character_id(&self, character_id: &CharacterId) -> Option<&Character> {
|
||||
self.characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == character_id)
|
||||
}
|
||||
|
||||
pub fn find_by_character_id_mut(
|
||||
&mut self,
|
||||
character_id: &CharacterId,
|
||||
) -> Option<&mut Character> {
|
||||
self.characters
|
||||
.iter_mut()
|
||||
.find(|c| c.character_id() == character_id)
|
||||
}
|
||||
|
||||
fn wolves_count(&self) -> usize {
|
||||
self.characters.iter().filter(|c| c.is_wolf()).count()
|
||||
}
|
||||
|
||||
fn villager_count(&self) -> usize {
|
||||
self.characters.iter().filter(|c| c.is_village()).count()
|
||||
}
|
||||
|
||||
pub fn is_game_over(&self) -> Option<GameOver> {
|
||||
let wolves = self.wolves_count();
|
||||
let villagers = self.villager_count();
|
||||
|
||||
if wolves == 0 {
|
||||
return Some(GameOver::VillageWins);
|
||||
}
|
||||
|
||||
if wolves >= villagers {
|
||||
return Some(GameOver::WolvesWin);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn execute(&mut self, characters: &[CharacterId]) -> Result<Option<GameOver>> {
|
||||
let day = match self.date_time {
|
||||
DateTime::Day { number } => number,
|
||||
DateTime::Night { number: _ } => return Err(GameError::NoExecutionsAtNight),
|
||||
};
|
||||
|
||||
if characters.is_empty() {
|
||||
return Err(GameError::NoTrialNotAllowed);
|
||||
}
|
||||
let targets = self
|
||||
.characters
|
||||
.iter_mut()
|
||||
.filter(|c| characters.contains(c.character_id()))
|
||||
.collect::<Box<[_]>>();
|
||||
if targets.len() != characters.len() {
|
||||
return Err(GameError::CannotFindTargetButShouldBeThere);
|
||||
}
|
||||
for t in targets {
|
||||
t.execute(day)?;
|
||||
}
|
||||
|
||||
self.date_time = self.date_time.next();
|
||||
Ok(self.is_game_over())
|
||||
}
|
||||
|
||||
pub fn to_day(&mut self) -> Result<DateTime> {
|
||||
if self.date_time.is_day() {
|
||||
return Err(GameError::AlreadyDaytime);
|
||||
}
|
||||
self.date_time = self.date_time.next();
|
||||
Ok(self.date_time)
|
||||
}
|
||||
|
||||
pub fn living_wolf_pack_players(&self) -> Box<[Character]> {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| c.role().wolf() && c.alive())
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn living_players(&self) -> Box<[Target]> {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| c.alive())
|
||||
.map(Character::target)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn target_by_id(&self, character_id: &CharacterId) -> Option<Target> {
|
||||
self.character_by_id(character_id).map(Character::target)
|
||||
}
|
||||
|
||||
pub fn living_villagers(&self) -> Box<[Target]> {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| c.alive() && c.is_village())
|
||||
.map(Character::target)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn living_players_excluding(&self, exclude: &CharacterId) -> Box<[Target]> {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| c.alive() && c.character_id() != exclude)
|
||||
.map(Character::target)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn dead_targets(&self) -> Box<[Target]> {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| !c.alive())
|
||||
.map(Character::target)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn dead_characters(&self) -> Box<[&Character]> {
|
||||
self.characters.iter().filter(|c| !c.alive()).collect()
|
||||
}
|
||||
|
||||
pub fn living_characters_by_role(&self, role: RoleTitle) -> Box<[Character]> {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| c.role().title() == role)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn characters(&self) -> Box<[Character]> {
|
||||
self.characters.iter().cloned().collect()
|
||||
}
|
||||
|
||||
pub fn character_by_id_mut(&mut self, character_id: &CharacterId) -> Option<&mut Character> {
|
||||
self.characters
|
||||
.iter_mut()
|
||||
.find(|c| c.character_id() == character_id)
|
||||
}
|
||||
|
||||
pub fn character_by_id(&self, character_id: &CharacterId) -> Option<&Character> {
|
||||
self.characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == character_id)
|
||||
}
|
||||
|
||||
pub fn character_by_player_id(&self, player_id: &PlayerId) -> Option<&Character> {
|
||||
self.characters.iter().find(|c| c.player_id() == player_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl RoleTitle {
|
||||
pub fn title_to_role_excl_apprentice(self) -> Role {
|
||||
match self {
|
||||
RoleTitle::Villager => Role::Villager,
|
||||
RoleTitle::Scapegoat => Role::Scapegoat,
|
||||
RoleTitle::Seer => Role::Seer,
|
||||
RoleTitle::Arcanist => Role::Arcanist,
|
||||
RoleTitle::Elder => Role::Elder {
|
||||
knows_on_night: NonZeroU8::new(rand::rng().random_range(1u8..3)).unwrap(),
|
||||
},
|
||||
RoleTitle::Werewolf => Role::Werewolf,
|
||||
RoleTitle::AlphaWolf => Role::AlphaWolf { killed: None },
|
||||
RoleTitle::DireWolf => Role::DireWolf,
|
||||
RoleTitle::Shapeshifter => Role::Shapeshifter { shifted_into: None },
|
||||
RoleTitle::Apprentice => panic!("title_to_role_excl_apprentice got an apprentice role"),
|
||||
RoleTitle::Protector => Role::Protector {
|
||||
last_protected: None,
|
||||
},
|
||||
RoleTitle::Gravedigger => Role::Gravedigger,
|
||||
RoleTitle::Hunter => Role::Hunter { target: None },
|
||||
RoleTitle::Militia => Role::Militia { targeted: None },
|
||||
RoleTitle::MapleWolf => Role::MapleWolf {
|
||||
last_kill_on_night: 0,
|
||||
},
|
||||
RoleTitle::Guardian => Role::Guardian {
|
||||
last_protected: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
#![allow(clippy::new_without_default)]
|
||||
use error::GameError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
// pub mod action;
|
||||
pub mod diedto;
|
||||
pub mod error;
|
||||
pub mod game;
|
||||
pub mod message;
|
||||
pub mod modifier;
|
||||
pub mod nonzero;
|
||||
pub mod player;
|
||||
pub mod role;
|
||||
|
||||
#[derive(Debug, Error, Clone, Serialize, Deserialize)]
|
||||
pub enum MessageError {
|
||||
#[error("{0}")]
|
||||
GameError(#[from] GameError),
|
||||
}
|
||||
|
||||
pub(crate) trait MustBeInVillage<T> {
|
||||
fn must_be_in_village(self) -> Result<T, GameError>;
|
||||
}
|
||||
|
||||
impl<T> MustBeInVillage<T> for Option<T> {
|
||||
fn must_be_in_village(self) -> Result<T, GameError> {
|
||||
self.ok_or(GameError::CannotFindTargetButShouldBeThere)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
pub mod host;
|
||||
mod ident;
|
||||
pub mod night;
|
||||
|
||||
use core::{fmt::Display, num::NonZeroU8};
|
||||
|
||||
pub use ident::*;
|
||||
use night::{ActionPrompt, ActionResponse, ActionResult, RoleChange};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
error::GameError,
|
||||
game::GameOver,
|
||||
player::{Character, CharacterId},
|
||||
role::RoleTitle,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ClientMessage {
|
||||
Hello,
|
||||
Goodbye,
|
||||
GetState,
|
||||
RoleAck,
|
||||
UpdateSelf(UpdateSelf),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum UpdateSelf {
|
||||
Name(String),
|
||||
Number(NonZeroU8),
|
||||
Pronouns(Option<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DayCharacter {
|
||||
pub character_id: CharacterId,
|
||||
pub name: String,
|
||||
pub alive: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub struct Target {
|
||||
pub character_id: CharacterId,
|
||||
pub public: PublicIdentity,
|
||||
}
|
||||
|
||||
impl Display for Target {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let Target {
|
||||
character_id,
|
||||
public,
|
||||
} = self;
|
||||
write!(f, "{public} [(c){character_id}]")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ServerMessage {
|
||||
Disconnect,
|
||||
LobbyInfo {
|
||||
joined: bool,
|
||||
players: Box<[PublicIdentity]>,
|
||||
},
|
||||
GameInProgress,
|
||||
GameStart {
|
||||
role: RoleTitle,
|
||||
},
|
||||
InvalidMessageForGameState,
|
||||
NoSuchTarget,
|
||||
GameOver(GameOver),
|
||||
Update(PlayerUpdate),
|
||||
Sleep,
|
||||
Reset,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum PlayerUpdate {
|
||||
Number(NonZeroU8),
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
use core::num::NonZeroU8;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
error::GameError,
|
||||
game::{GameOver, GameSettings},
|
||||
message::{
|
||||
PublicIdentity, Target,
|
||||
night::{ActionPrompt, ActionResponse, ActionResult},
|
||||
},
|
||||
player::{CharacterId, PlayerId},
|
||||
};
|
||||
|
||||
use super::{CharacterState, PlayerState};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum HostMessage {
|
||||
GetState,
|
||||
Lobby(HostLobbyMessage),
|
||||
InGame(HostGameMessage),
|
||||
ForceRoleAckFor(CharacterId),
|
||||
NewLobby,
|
||||
Echo(ServerToHostMessage),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum HostGameMessage {
|
||||
Day(HostDayMessage),
|
||||
Night(HostNightMessage),
|
||||
GetState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum HostNightMessage {
|
||||
ActionResponse(ActionResponse),
|
||||
Next,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum HostDayMessage {
|
||||
Execute,
|
||||
MarkForExecution(CharacterId),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum HostLobbyMessage {
|
||||
GetState,
|
||||
Kick(PlayerId),
|
||||
GetGameSettings,
|
||||
SetGameSettings(GameSettings),
|
||||
Start,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ServerToHostMessage {
|
||||
Disconnect,
|
||||
Daytime {
|
||||
characters: Box<[CharacterState]>,
|
||||
marked: Box<[CharacterId]>,
|
||||
day: NonZeroU8,
|
||||
},
|
||||
ActionPrompt(PublicIdentity, ActionPrompt),
|
||||
ActionResult(PublicIdentity, ActionResult),
|
||||
Lobby(Box<[PlayerState]>),
|
||||
GameSettings(GameSettings),
|
||||
Error(GameError),
|
||||
GameOver(GameOver),
|
||||
WaitingForRoleRevealAcks {
|
||||
ackd: Box<[Target]>,
|
||||
waiting: Box<[Target]>,
|
||||
},
|
||||
CoverOfDarkness,
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
use core::{fmt::Display, num::NonZeroU8};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
player::{CharacterId, PlayerId},
|
||||
role::RoleTitle,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Identification {
|
||||
pub player_id: PlayerId,
|
||||
pub public: PublicIdentity,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PublicIdentity {
|
||||
pub name: String,
|
||||
pub pronouns: Option<String>,
|
||||
pub number: NonZeroU8,
|
||||
}
|
||||
|
||||
impl Default for PublicIdentity {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: Default::default(),
|
||||
pronouns: Default::default(),
|
||||
number: NonZeroU8::new(1).unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PublicIdentity {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let PublicIdentity {
|
||||
name,
|
||||
pronouns,
|
||||
number,
|
||||
} = self;
|
||||
let pronouns = pronouns
|
||||
.as_ref()
|
||||
.map(|p| format!(" ({p})"))
|
||||
.unwrap_or_default();
|
||||
write!(f, "[{number}] {name}{pronouns}")
|
||||
}
|
||||
}
|
||||
|
||||
impl core::hash::Hash for Identification {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.player_id.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Identification {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let Identification { player_id, public } = self;
|
||||
write!(f, "{public} [{player_id}]")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct PlayerState {
|
||||
pub identification: Identification,
|
||||
pub connected: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct CharacterState {
|
||||
pub player_id: PlayerId,
|
||||
pub character_id: CharacterId,
|
||||
pub public_identity: PublicIdentity,
|
||||
pub role: RoleTitle,
|
||||
pub died_to: Option<DiedTo>,
|
||||
}
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use werewolves_macros::ChecksAs;
|
||||
|
||||
use crate::{
|
||||
player::CharacterId,
|
||||
role::{Alignment, PreviousGuardianAction, Role, RoleTitle},
|
||||
};
|
||||
|
||||
use super::Target;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum ActionPrompt {
|
||||
WolvesIntro {
|
||||
wolves: Box<[(Target, RoleTitle)]>,
|
||||
},
|
||||
RoleChange {
|
||||
new_role: RoleTitle,
|
||||
},
|
||||
Seer {
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
Protector {
|
||||
targets: Box<[Target]>,
|
||||
},
|
||||
Arcanist {
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
Gravedigger {
|
||||
dead_players: Box<[Target]>,
|
||||
},
|
||||
Hunter {
|
||||
current_target: Option<Target>,
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
Militia {
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
MapleWolf {
|
||||
kill_or_die: bool,
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
Guardian {
|
||||
previous: Option<PreviousGuardianAction>,
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
WolfPackKill {
|
||||
living_villagers: Box<[Target]>,
|
||||
},
|
||||
Shapeshifter,
|
||||
AlphaWolf {
|
||||
living_villagers: Box<[Target]>,
|
||||
},
|
||||
DireWolf {
|
||||
living_players: Box<[Target]>,
|
||||
},
|
||||
}
|
||||
|
||||
impl PartialOrd for ActionPrompt {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
fn ordering_num(prompt: &ActionPrompt) -> u8 {
|
||||
match prompt {
|
||||
ActionPrompt::WolvesIntro { wolves: _ } => 0,
|
||||
ActionPrompt::Guardian {
|
||||
living_players: _,
|
||||
previous: _,
|
||||
}
|
||||
| ActionPrompt::Protector { targets: _ } => 1,
|
||||
ActionPrompt::WolfPackKill {
|
||||
living_villagers: _,
|
||||
} => 2,
|
||||
ActionPrompt::Shapeshifter => 3,
|
||||
ActionPrompt::AlphaWolf {
|
||||
living_villagers: _,
|
||||
} => 4,
|
||||
ActionPrompt::DireWolf { living_players: _ } => 5,
|
||||
ActionPrompt::Seer { living_players: _ }
|
||||
| ActionPrompt::Arcanist { living_players: _ }
|
||||
| ActionPrompt::Gravedigger { dead_players: _ }
|
||||
| ActionPrompt::Hunter {
|
||||
current_target: _,
|
||||
living_players: _,
|
||||
}
|
||||
| ActionPrompt::Militia { living_players: _ }
|
||||
| ActionPrompt::MapleWolf {
|
||||
kill_or_die: _,
|
||||
living_players: _,
|
||||
}
|
||||
| ActionPrompt::RoleChange { new_role: _ } => 0xFF,
|
||||
}
|
||||
}
|
||||
ordering_num(self).partial_cmp(&ordering_num(other))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Deserialize, ChecksAs)]
|
||||
pub enum ActionResponse {
|
||||
Seer(CharacterId),
|
||||
Arcanist(CharacterId, CharacterId),
|
||||
Gravedigger(CharacterId),
|
||||
Hunter(CharacterId),
|
||||
Militia(Option<CharacterId>),
|
||||
MapleWolf(Option<CharacterId>),
|
||||
Guardian(CharacterId),
|
||||
WolfPackKillVote(CharacterId),
|
||||
#[checks]
|
||||
Shapeshifter(bool),
|
||||
AlphaWolf(Option<CharacterId>),
|
||||
Direwolf(CharacterId),
|
||||
Protector(CharacterId),
|
||||
#[checks]
|
||||
RoleChangeAck,
|
||||
WolvesIntroAck,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ActionResult {
|
||||
RoleBlocked,
|
||||
Seer(Alignment),
|
||||
Arcanist { same: bool },
|
||||
GraveDigger(Option<RoleTitle>),
|
||||
WolvesMustBeUnanimous,
|
||||
WaitForOthersToVote,
|
||||
GoBackToSleep,
|
||||
RoleRevealDone,
|
||||
WolvesIntroDone,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum RoleChange {
|
||||
Elder(Role),
|
||||
Apprentice(Role),
|
||||
Shapeshift(Role),
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum Modifier {
|
||||
Drunk,
|
||||
Insane,
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[repr(transparent)]
|
||||
pub struct NonZeroSub100U8(u8);
|
||||
impl NonZeroSub100U8 {
|
||||
pub const fn new(val: u8) -> Option<Self> {
|
||||
if val == 0 || val > 99 {
|
||||
None
|
||||
} else {
|
||||
Some(Self(val))
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn get(&self) -> u8 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,301 @@
|
|||
use core::{fmt::Display, num::NonZeroU8};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{DateTime, Village},
|
||||
message::{Identification, PublicIdentity, Target, night::ActionPrompt},
|
||||
modifier::Modifier,
|
||||
role::{MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub struct PlayerId(uuid::Uuid);
|
||||
|
||||
impl PlayerId {
|
||||
pub fn new() -> Self {
|
||||
Self(uuid::Uuid::new_v4())
|
||||
}
|
||||
pub const fn from_u128(v: u128) -> Self {
|
||||
Self(uuid::Uuid::from_u128(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PlayerId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub struct CharacterId(uuid::Uuid);
|
||||
|
||||
impl CharacterId {
|
||||
pub fn new() -> Self {
|
||||
Self(uuid::Uuid::new_v4())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CharacterId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Player {
|
||||
id: PlayerId,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn new(name: String) -> Self {
|
||||
Self {
|
||||
id: PlayerId::new(),
|
||||
name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Protection {
|
||||
Guardian { source: CharacterId, guarding: bool },
|
||||
Protector { source: CharacterId },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum KillOutcome {
|
||||
Killed,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Character {
|
||||
player_id: PlayerId,
|
||||
character_id: CharacterId,
|
||||
public: PublicIdentity,
|
||||
role: Role,
|
||||
modifier: Option<Modifier>,
|
||||
died_to: Option<DiedTo>,
|
||||
role_changes: Vec<RoleChange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RoleChange {
|
||||
role: Role,
|
||||
new_role: RoleTitle,
|
||||
changed_on_night: u8,
|
||||
}
|
||||
|
||||
impl Character {
|
||||
pub fn new(Identification { player_id, public }: Identification, role: Role) -> Self {
|
||||
Self {
|
||||
role,
|
||||
public,
|
||||
player_id,
|
||||
character_id: CharacterId::new(),
|
||||
modifier: None,
|
||||
died_to: None,
|
||||
role_changes: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn target(&self) -> Target {
|
||||
Target {
|
||||
character_id: self.character_id.clone(),
|
||||
public: self.public.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn public_identity(&self) -> &PublicIdentity {
|
||||
&self.public
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.public.name
|
||||
}
|
||||
|
||||
pub const fn number(&self) -> NonZeroU8 {
|
||||
self.public.number
|
||||
}
|
||||
|
||||
pub const fn pronouns(&self) -> Option<&str> {
|
||||
match self.public.pronouns.as_ref() {
|
||||
Some(p) => Some(p.as_str()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn died_to(&self) -> Option<&DiedTo> {
|
||||
self.died_to.as_ref()
|
||||
}
|
||||
|
||||
pub fn kill(&mut self, died_to: DiedTo) {
|
||||
match &self.died_to {
|
||||
Some(_) => {}
|
||||
None => self.died_to = Some(died_to),
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn alive(&self) -> bool {
|
||||
self.died_to.is_none()
|
||||
}
|
||||
|
||||
pub fn execute(&mut self, day: NonZeroU8) -> Result<(), GameError> {
|
||||
if self.died_to.is_some() {
|
||||
return Err(GameError::CharacterAlreadyDead);
|
||||
}
|
||||
self.died_to = Some(DiedTo::Execution { day });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const fn character_id(&self) -> &CharacterId {
|
||||
&self.character_id
|
||||
}
|
||||
|
||||
pub const fn player_id(&self) -> &PlayerId {
|
||||
&self.player_id
|
||||
}
|
||||
|
||||
pub const fn role(&self) -> &Role {
|
||||
&self.role
|
||||
}
|
||||
|
||||
pub const fn role_mut(&mut self) -> &mut Role {
|
||||
&mut self.role
|
||||
}
|
||||
|
||||
pub fn role_change(&mut self, new_role: RoleTitle, at: DateTime) -> Result<(), GameError> {
|
||||
let mut role = new_role.title_to_role_excl_apprentice();
|
||||
core::mem::swap(&mut role, &mut self.role);
|
||||
self.role_changes.push(RoleChange {
|
||||
role,
|
||||
new_role,
|
||||
changed_on_night: match at {
|
||||
DateTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||
DateTime::Night { number } => number,
|
||||
},
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const fn is_wolf(&self) -> bool {
|
||||
self.role.wolf()
|
||||
}
|
||||
|
||||
pub const fn is_village(&self) -> bool {
|
||||
!self.is_wolf()
|
||||
}
|
||||
|
||||
pub fn night_action_prompt(
|
||||
&self,
|
||||
village: &Village,
|
||||
) -> Result<Option<ActionPrompt>, GameError> {
|
||||
if !self.alive() || !self.role.wakes(village) {
|
||||
return Ok(None);
|
||||
}
|
||||
let night = match village.date_time() {
|
||||
DateTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||
DateTime::Night { number } => number,
|
||||
};
|
||||
Ok(Some(match &self.role {
|
||||
Role::Shapeshifter {
|
||||
shifted_into: Some(_),
|
||||
}
|
||||
| Role::AlphaWolf { killed: Some(_) }
|
||||
| Role::Militia { targeted: Some(_) }
|
||||
| Role::Scapegoat
|
||||
| Role::Villager => return Ok(None),
|
||||
Role::Seer => ActionPrompt::Seer {
|
||||
living_players: village.living_players_excluding(&self.character_id),
|
||||
},
|
||||
Role::Arcanist => ActionPrompt::Arcanist {
|
||||
living_players: village.living_players_excluding(&self.character_id),
|
||||
},
|
||||
Role::Protector {
|
||||
last_protected: Some(last_protected),
|
||||
} => ActionPrompt::Protector {
|
||||
targets: village.living_players_excluding(last_protected),
|
||||
},
|
||||
Role::Protector {
|
||||
last_protected: None,
|
||||
} => ActionPrompt::Protector {
|
||||
targets: village.living_players_excluding(&self.character_id),
|
||||
},
|
||||
Role::Apprentice(role) => {
|
||||
let current_night = match village.date_time() {
|
||||
DateTime::Day { number: _ } => return Ok(None),
|
||||
DateTime::Night { number } => number,
|
||||
};
|
||||
return Ok(village
|
||||
.characters()
|
||||
.into_iter()
|
||||
.filter(|c| c.role().title() == role.title())
|
||||
.filter_map(|char| char.died_to)
|
||||
.any(|died_to| match died_to.date_time() {
|
||||
DateTime::Day { number } => number.get() + 1 >= current_night,
|
||||
DateTime::Night { number } => number + 1 >= current_night,
|
||||
})
|
||||
.then(|| ActionPrompt::RoleChange {
|
||||
new_role: role.title(),
|
||||
}));
|
||||
}
|
||||
Role::Elder { knows_on_night } => {
|
||||
let current_night = match village.date_time() {
|
||||
DateTime::Day { number: _ } => return Ok(None),
|
||||
DateTime::Night { number } => number,
|
||||
};
|
||||
return Ok((current_night == knows_on_night.get()).then_some({
|
||||
ActionPrompt::RoleChange {
|
||||
new_role: RoleTitle::Elder,
|
||||
}
|
||||
}));
|
||||
}
|
||||
Role::Militia { targeted: None } => ActionPrompt::Militia {
|
||||
living_players: village.living_players_excluding(&self.character_id),
|
||||
},
|
||||
Role::Werewolf => ActionPrompt::WolfPackKill {
|
||||
living_villagers: village.living_players(),
|
||||
},
|
||||
Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf {
|
||||
living_villagers: village.living_players_excluding(&self.character_id),
|
||||
},
|
||||
Role::DireWolf => ActionPrompt::DireWolf {
|
||||
living_players: village.living_players(),
|
||||
},
|
||||
Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter,
|
||||
Role::Gravedigger => ActionPrompt::Gravedigger {
|
||||
dead_players: village.dead_targets(),
|
||||
},
|
||||
Role::Hunter { target } => ActionPrompt::Hunter {
|
||||
current_target: target.as_ref().and_then(|t| village.target_by_id(t)),
|
||||
living_players: village.living_players_excluding(&self.character_id),
|
||||
},
|
||||
Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf {
|
||||
kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night,
|
||||
living_players: village.living_players_excluding(&self.character_id),
|
||||
},
|
||||
Role::Guardian {
|
||||
last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
|
||||
} => ActionPrompt::Guardian {
|
||||
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
|
||||
living_players: village.living_players_excluding(&prev_target.character_id),
|
||||
},
|
||||
Role::Guardian {
|
||||
last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
|
||||
} => ActionPrompt::Guardian {
|
||||
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
|
||||
living_players: village.living_players(),
|
||||
},
|
||||
Role::Guardian {
|
||||
last_protected: None,
|
||||
} => ActionPrompt::Guardian {
|
||||
previous: None,
|
||||
living_players: village.living_players(),
|
||||
},
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,183 @@
|
|||
use core::num::NonZeroU8;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use werewolves_macros::{ChecksAs, Titles};
|
||||
|
||||
use crate::{
|
||||
game::{DateTime, Village},
|
||||
message::Target,
|
||||
player::CharacterId,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ChecksAs, Titles)]
|
||||
pub enum Role {
|
||||
#[checks(Alignment::Village)]
|
||||
Villager,
|
||||
#[checks(Alignment::Wolves)]
|
||||
#[checks("killer")]
|
||||
#[checks("powerful")]
|
||||
Scapegoat,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks("powerful")]
|
||||
#[checks("is_mentor")]
|
||||
Seer,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks("powerful")]
|
||||
#[checks("is_mentor")]
|
||||
Arcanist,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks("powerful")]
|
||||
#[checks("is_mentor")]
|
||||
Gravedigger,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks("killer")]
|
||||
#[checks("powerful")]
|
||||
#[checks("is_mentor")]
|
||||
#[checks]
|
||||
Hunter { target: Option<CharacterId> },
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks("killer")]
|
||||
#[checks("powerful")]
|
||||
#[checks("is_mentor")]
|
||||
Militia { targeted: Option<CharacterId> },
|
||||
#[checks(Alignment::Wolves)]
|
||||
#[checks("killer")]
|
||||
#[checks("powerful")]
|
||||
#[checks("is_mentor")]
|
||||
MapleWolf { last_kill_on_night: u8 },
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks("powerful")]
|
||||
#[checks("killer")]
|
||||
#[checks("is_mentor")]
|
||||
Guardian {
|
||||
last_protected: Option<PreviousGuardianAction>,
|
||||
},
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks("powerful")]
|
||||
#[checks("is_mentor")]
|
||||
Protector { last_protected: Option<CharacterId> },
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks("powerful")]
|
||||
Apprentice(Box<Role>),
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks("powerful")]
|
||||
#[checks("is_mentor")]
|
||||
Elder { knows_on_night: NonZeroU8 },
|
||||
|
||||
#[checks(Alignment::Wolves)]
|
||||
#[checks("killer")]
|
||||
#[checks("powerful")]
|
||||
#[checks("wolf")]
|
||||
Werewolf,
|
||||
#[checks(Alignment::Wolves)]
|
||||
#[checks("killer")]
|
||||
#[checks("powerful")]
|
||||
#[checks("wolf")]
|
||||
AlphaWolf { killed: Option<CharacterId> },
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks("killer")]
|
||||
#[checks("powerful")]
|
||||
#[checks("wolf")]
|
||||
DireWolf,
|
||||
#[checks(Alignment::Wolves)]
|
||||
#[checks("killer")]
|
||||
#[checks("powerful")]
|
||||
#[checks("wolf")]
|
||||
Shapeshifter { shifted_into: Option<CharacterId> },
|
||||
}
|
||||
|
||||
impl Role {
|
||||
/// [RoleTitle] as shown to the player on role assignment
|
||||
pub const fn initial_shown_role(&self) -> RoleTitle {
|
||||
match self {
|
||||
Role::Apprentice(_) | Role::Elder { knows_on_night: _ } => RoleTitle::Villager,
|
||||
_ => self.title(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wakes(&self, village: &Village) -> bool {
|
||||
let night_zero = match village.date_time() {
|
||||
DateTime::Day { number: _ } => return false,
|
||||
DateTime::Night { number } => number == 0,
|
||||
};
|
||||
if night_zero {
|
||||
return match self {
|
||||
Role::DireWolf | Role::Arcanist | Role::Seer => true,
|
||||
|
||||
Role::Shapeshifter { shifted_into: _ }
|
||||
| Role::Werewolf
|
||||
| Role::AlphaWolf { killed: _ }
|
||||
| Role::Elder { knows_on_night: _ }
|
||||
| Role::Gravedigger
|
||||
| Role::Hunter { target: _ }
|
||||
| Role::Militia { targeted: _ }
|
||||
| Role::MapleWolf {
|
||||
last_kill_on_night: _,
|
||||
}
|
||||
| Role::Guardian { last_protected: _ }
|
||||
| Role::Apprentice(_)
|
||||
| Role::Villager
|
||||
| Role::Scapegoat
|
||||
| Role::Protector { last_protected: _ } => false,
|
||||
};
|
||||
}
|
||||
match self {
|
||||
Role::AlphaWolf { killed: Some(_) }
|
||||
| Role::Werewolf
|
||||
| Role::Scapegoat
|
||||
| Role::Militia { targeted: Some(_) }
|
||||
| Role::Villager => false,
|
||||
|
||||
Role::Shapeshifter { shifted_into: _ }
|
||||
| Role::DireWolf
|
||||
| Role::AlphaWolf { killed: None }
|
||||
| Role::Arcanist
|
||||
| Role::Protector { last_protected: _ }
|
||||
| Role::Gravedigger
|
||||
| Role::Hunter { target: _ }
|
||||
| Role::Militia { targeted: None }
|
||||
| Role::MapleWolf {
|
||||
last_kill_on_night: _,
|
||||
}
|
||||
| Role::Guardian { last_protected: _ }
|
||||
| Role::Seer => true,
|
||||
|
||||
Role::Apprentice(role) => village
|
||||
.characters()
|
||||
.iter()
|
||||
.any(|c| c.role().title() == role.title()),
|
||||
|
||||
Role::Elder { knows_on_night } => match village.date_time() {
|
||||
DateTime::Night { number } => number == knows_on_night.get(),
|
||||
_ => false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum Alignment {
|
||||
Village,
|
||||
Wolves,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ChecksAs)]
|
||||
pub enum ArcanistCheck {
|
||||
#[checks]
|
||||
Same,
|
||||
#[checks]
|
||||
Different,
|
||||
}
|
||||
|
||||
pub const MAPLE_WOLF_ABSTAIN_LIMIT: NonZeroU8 = NonZeroU8::new(3).unwrap();
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum RoleBlock {
|
||||
Direwolf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum PreviousGuardianAction {
|
||||
Protect(Target),
|
||||
Guard(Target),
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
[package]
|
||||
name = "werewolves-server"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.8", features = ["ws"] }
|
||||
tokio = { version = "1.44", features = ["full"] }
|
||||
log = { version = "0.4" }
|
||||
pretty_env_logger = { version = "0.5" }
|
||||
# env_logger = { version = "0.11" }
|
||||
futures = "0.3.31"
|
||||
anyhow = { version = "1" }
|
||||
werewolves-proto = { path = "../werewolves-proto" }
|
||||
werewolves-macros = { path = "../werewolves-macros" }
|
||||
mime-sniffer = { version = "0.1" }
|
||||
chrono = { version = "0.4" }
|
||||
atom_syndication = { version = "0.12" }
|
||||
axum-extra = { version = "0.10", features = ["typed-header"] }
|
||||
rand = { version = "0.9" }
|
||||
serde_json = { version = "1.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
thiserror = { version = "2" }
|
||||
ciborium = { version = "0.2", optional = true }
|
||||
colored = { version = "3.0" }
|
||||
|
||||
[features]
|
||||
# default = ["cbor"]
|
||||
cbor = ["dep:ciborium"]
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
[Unit]
|
||||
Description=blog
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
User=blog
|
||||
Group=blog
|
||||
|
||||
WorkingDirectory=/home/blog
|
||||
Environment=RUST_LOG=info
|
||||
Environment=PORT=3024
|
||||
ExecStart=/home/blog/blog-server
|
||||
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
use core::net::SocketAddr;
|
||||
|
||||
use crate::{
|
||||
AppState, XForwardedFor,
|
||||
connection::{ConnectionId, JoinedPlayer},
|
||||
runner::IdentifiedClientMessage,
|
||||
};
|
||||
use axum::{
|
||||
extract::{
|
||||
ConnectInfo, State, WebSocketUpgrade,
|
||||
ws::{self, Message, WebSocket},
|
||||
},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use axum_extra::{TypedHeader, headers};
|
||||
use colored::Colorize;
|
||||
use tokio::sync::broadcast::{Receiver, Sender};
|
||||
use werewolves_proto::message::{ClientMessage, Identification, ServerMessage, UpdateSelf};
|
||||
|
||||
pub async fn handler(
|
||||
ws: WebSocketUpgrade,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
x_forwarded_for: Option<TypedHeader<XForwardedFor>>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let who = x_forwarded_for
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| addr.to_string())
|
||||
.italic();
|
||||
log::info!(
|
||||
"{who}{} connected.",
|
||||
user_agent
|
||||
.map(|agent| format!(" (User-Agent: {})", agent.as_str()))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
let player_list = state.joined_players;
|
||||
|
||||
// finalize the upgrade process by returning upgrade callback.
|
||||
// we can customize the callback by sending additional info such as address.
|
||||
ws.on_upgrade(move |mut socket| async move {
|
||||
let ident = match get_identification(&mut socket, &who).await {
|
||||
Ok(ident) => ident,
|
||||
Err(err) => {
|
||||
log::warn!("identification failed for {who}: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
log::info!("connected {who} as {ident}");
|
||||
let connection_id = ConnectionId::new(ident.player_id.clone());
|
||||
let recv = {
|
||||
let (send, recv) = tokio::sync::broadcast::channel(100);
|
||||
player_list
|
||||
.insert_or_replace(
|
||||
ident.player_id.clone(),
|
||||
JoinedPlayer::new(
|
||||
send,
|
||||
recv,
|
||||
connection_id.clone(),
|
||||
ident.public.name.clone(),
|
||||
ident.public.number.clone(),
|
||||
ident.public.pronouns.clone(),
|
||||
),
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
Client::new(
|
||||
ident.clone(),
|
||||
connection_id.clone(),
|
||||
socket,
|
||||
who.to_string(),
|
||||
state.send,
|
||||
recv,
|
||||
)
|
||||
.run()
|
||||
.await;
|
||||
|
||||
log::info!("ending connection with {who}");
|
||||
player_list.disconnect(&connection_id).await;
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_identification(
|
||||
socket: &mut WebSocket,
|
||||
who: &str,
|
||||
) -> Result<Identification, anyhow::Error> {
|
||||
loop {
|
||||
let msg_bytes = &socket
|
||||
.recv()
|
||||
.await
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"connection from {who} closed before identification"
|
||||
))?
|
||||
.map_err(|err| anyhow::anyhow!("connection error from {who} at identification: {err}"))?
|
||||
.into_data();
|
||||
#[cfg(not(feature = "cbor"))]
|
||||
let res = serde_json::from_slice::<Identification>(msg_bytes);
|
||||
#[cfg(feature = "cbor")]
|
||||
let res = ciborium::from_reader::<Identification, &[u8]>(msg_bytes);
|
||||
match res {
|
||||
Ok(id) => return Ok(id),
|
||||
Err(err) => {
|
||||
log::error!("invalid identification message from {who}: {err}");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
enum MessagePayload {
|
||||
Bytes(axum::body::Bytes),
|
||||
Utf8(ws::Utf8Bytes),
|
||||
}
|
||||
|
||||
struct Client {
|
||||
ident: Identification,
|
||||
connection_id: ConnectionId,
|
||||
socket: WebSocket,
|
||||
who: String,
|
||||
sender: Sender<IdentifiedClientMessage>,
|
||||
receiver: Receiver<ServerMessage>,
|
||||
message_history: Vec<ServerMessage>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
fn new(
|
||||
ident: Identification,
|
||||
connection_id: ConnectionId,
|
||||
socket: WebSocket,
|
||||
who: String,
|
||||
sender: Sender<IdentifiedClientMessage>,
|
||||
receiver: Receiver<ServerMessage>,
|
||||
) -> Self {
|
||||
Self {
|
||||
ident,
|
||||
connection_id,
|
||||
socket,
|
||||
who,
|
||||
sender,
|
||||
receiver,
|
||||
message_history: Vec::new(),
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "cbor")]
|
||||
fn decode_message(msg: MessagePayload) -> Result<ClientMessage, anyhow::Error> {
|
||||
match msg {
|
||||
MessagePayload::Bytes(bytes) => Ok(ciborium::from_reader(bytes.iter().as_slice())?),
|
||||
MessagePayload::Utf8(_) => Err(anyhow::anyhow!(
|
||||
"cbor doesn't use utf8 strings for messages"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "cbor"))]
|
||||
fn decode_message(msg: MessagePayload) -> Result<ClientMessage, anyhow::Error> {
|
||||
match msg {
|
||||
MessagePayload::Bytes(bytes) => Ok(serde_json::from_slice(&bytes)?),
|
||||
MessagePayload::Utf8(text) => Ok(serde_json::from_str(&text)?),
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_recv(
|
||||
&mut self,
|
||||
msg: Result<Message, axum::Error>,
|
||||
conn_id: ConnectionId,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
use crate::LogError;
|
||||
|
||||
let msg = match msg {
|
||||
Ok(msg) => msg,
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
let message: ClientMessage = match msg {
|
||||
Message::Binary(bytes) => Self::decode_message(MessagePayload::Bytes(bytes))?,
|
||||
Message::Text(text) => Self::decode_message(MessagePayload::Utf8(text))?,
|
||||
Message::Ping(ping) => {
|
||||
self.socket.send(Message::Pong(ping)).await.log_debug();
|
||||
return Ok(());
|
||||
}
|
||||
Message::Pong(_) => return Ok(()),
|
||||
Message::Close(Some(close_frame)) => {
|
||||
log::debug!("sent close frame: {close_frame:?}");
|
||||
return Ok(());
|
||||
}
|
||||
Message::Close(None) => {
|
||||
log::debug!("host closed connection");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
if let ClientMessage::UpdateSelf(update) = &message {
|
||||
match update {
|
||||
UpdateSelf::Name(name) => self.ident.public.name = name.clone(),
|
||||
UpdateSelf::Number(num) => self.ident.public.number = *num,
|
||||
UpdateSelf::Pronouns(pronouns) => self.ident.public.pronouns = pronouns.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
self.sender.send(IdentifiedClientMessage {
|
||||
message,
|
||||
identity: self.ident.clone(),
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_message(&mut self, message: ServerMessage) -> Result<(), anyhow::Error> {
|
||||
self.socket
|
||||
.send({
|
||||
#[cfg(not(feature = "cbor"))]
|
||||
{
|
||||
ws::Message::Text(serde_json::to_string(&message)?.into())
|
||||
}
|
||||
#[cfg(feature = "cbor")]
|
||||
ws::Message::Binary({
|
||||
let mut v = Vec::new();
|
||||
ciborium::into_writer(&message, &mut v)?;
|
||||
v.into()
|
||||
})
|
||||
})
|
||||
.await?;
|
||||
self.message_history.push(message);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run(mut self) {
|
||||
loop {
|
||||
if let Err(err) = tokio::select! {
|
||||
msg = self.socket.recv() => {
|
||||
match msg {
|
||||
Some(msg) => self.on_recv(msg, self.connection_id.clone()).await,
|
||||
None => {
|
||||
return;
|
||||
},
|
||||
}
|
||||
},
|
||||
r = self.receiver.recv() => {
|
||||
match r {
|
||||
Ok(msg) => {
|
||||
self.handle_message(msg).await
|
||||
}
|
||||
Err(err) => {
|
||||
log::warn!("[{}] recv error: {err}", self.connection_id.player_id());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} {
|
||||
log::error!("[{}][{}] {err}", self.connection_id.player_id(), self.who);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
use tokio::sync::{broadcast::Sender, mpsc::Receiver};
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
message::host::{HostMessage, ServerToHostMessage},
|
||||
};
|
||||
|
||||
pub struct HostComms {
|
||||
send: Sender<ServerToHostMessage>,
|
||||
recv: Receiver<HostMessage>,
|
||||
}
|
||||
|
||||
impl HostComms {
|
||||
pub const fn new(
|
||||
host_send: Sender<ServerToHostMessage>,
|
||||
host_recv: Receiver<HostMessage>,
|
||||
) -> Self {
|
||||
Self {
|
||||
send: host_send,
|
||||
recv: host_recv,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub async fn recv(&mut self) -> Option<HostMessage> {
|
||||
match self.recv.recv().await {
|
||||
Some(msg) => Some(msg),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
pub async fn recv(&mut self) -> Option<HostMessage> {
|
||||
self.recv.recv().await
|
||||
}
|
||||
|
||||
pub fn send(&mut self, message: ServerToHostMessage) -> Result<(), GameError> {
|
||||
self.send
|
||||
.send(message)
|
||||
.map_err(|err| GameError::GenericError(err.to_string()))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
use tokio::sync::broadcast::Receiver;
|
||||
use werewolves_proto::{error::GameError, player::PlayerId};
|
||||
|
||||
use crate::{communication::Comms, runner::Message};
|
||||
|
||||
use super::{HostComms, player::PlayerIdComms};
|
||||
|
||||
pub struct LobbyComms {
|
||||
comms: Comms,
|
||||
connect_recv: Receiver<(PlayerId, bool)>,
|
||||
}
|
||||
|
||||
impl LobbyComms {
|
||||
pub fn new(comms: Comms, connect_recv: Receiver<(PlayerId, bool)>) -> Self {
|
||||
Self {
|
||||
comms,
|
||||
connect_recv,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> (Comms, Receiver<(PlayerId, bool)>) {
|
||||
(self.comms, self.connect_recv)
|
||||
}
|
||||
|
||||
pub const fn player(&mut self) -> &mut PlayerIdComms {
|
||||
self.comms.player()
|
||||
}
|
||||
|
||||
pub const fn host(&mut self) -> &mut HostComms {
|
||||
self.comms.host()
|
||||
}
|
||||
|
||||
pub async fn next_message(&mut self) -> Result<Message, GameError> {
|
||||
tokio::select! {
|
||||
r = self.comms.message() => {
|
||||
r
|
||||
}
|
||||
r = self.connect_recv.recv() => {
|
||||
match r {
|
||||
Ok((player_id, true)) => Ok(Message::Connect(player_id)),
|
||||
Ok((player_id, false)) => Ok(Message::Disconnect(player_id)),
|
||||
Err(err) => Err(GameError::GenericError(err.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
use werewolves_proto::error::GameError;
|
||||
|
||||
use crate::{
|
||||
communication::{host::HostComms, player::PlayerIdComms},
|
||||
runner::Message,
|
||||
};
|
||||
|
||||
pub mod host;
|
||||
pub mod lobby;
|
||||
pub mod player;
|
||||
|
||||
pub struct Comms {
|
||||
host: HostComms,
|
||||
player: PlayerIdComms,
|
||||
}
|
||||
|
||||
impl Comms {
|
||||
pub const fn new(host: HostComms, player: PlayerIdComms) -> Self {
|
||||
Self { host, player }
|
||||
}
|
||||
|
||||
pub const fn host(&mut self) -> &mut HostComms {
|
||||
&mut self.host
|
||||
}
|
||||
|
||||
pub const fn player(&mut self) -> &mut PlayerIdComms {
|
||||
&mut self.player
|
||||
}
|
||||
|
||||
pub async fn message(&mut self) -> Result<Message, GameError> {
|
||||
tokio::select! {
|
||||
msg = self.host.recv() => {
|
||||
match msg {
|
||||
Some(msg) => Ok(Message::Host(msg)),
|
||||
None => Err(GameError::HostChannelClosed),
|
||||
}
|
||||
|
||||
}
|
||||
Ok(msg) = self.player.recv() => {
|
||||
Ok(Message::Client(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
use core::time::Duration;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use colored::Colorize;
|
||||
use tokio::{
|
||||
sync::broadcast::{Receiver, Sender},
|
||||
time::Instant,
|
||||
};
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
message::{ClientMessage, ServerMessage, Target, night::ActionResponse},
|
||||
player::{Character, CharacterId, PlayerId},
|
||||
};
|
||||
|
||||
use crate::{connection::JoinedPlayers, runner::IdentifiedClientMessage};
|
||||
|
||||
pub struct PlayerIdComms {
|
||||
joined_players: JoinedPlayers,
|
||||
message_recv: Receiver<IdentifiedClientMessage>,
|
||||
connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>,
|
||||
}
|
||||
|
||||
impl PlayerIdComms {
|
||||
pub fn new(
|
||||
joined_players: JoinedPlayers,
|
||||
message_recv: Receiver<IdentifiedClientMessage>,
|
||||
connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
joined_players,
|
||||
message_recv,
|
||||
connect_recv,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn recv(&mut self) -> Result<IdentifiedClientMessage, GameError> {
|
||||
match self
|
||||
.message_recv
|
||||
.recv()
|
||||
.await
|
||||
.map_err(|err| GameError::GenericError(err.to_string()))
|
||||
{
|
||||
Ok(msg) => {
|
||||
log::debug!("got message: {}", format!("{msg:?}").dimmed());
|
||||
Ok(msg)
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
use core::num::NonZeroU8;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use colored::Colorize;
|
||||
use tokio::{
|
||||
sync::{
|
||||
Mutex,
|
||||
broadcast::{Receiver, Sender},
|
||||
},
|
||||
time::Instant,
|
||||
};
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
message::{PublicIdentity, ServerMessage},
|
||||
player::PlayerId,
|
||||
};
|
||||
|
||||
use crate::LogError;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ConnectionId(PlayerId, Instant);
|
||||
|
||||
impl ConnectionId {
|
||||
pub fn new(player_id: PlayerId) -> Self {
|
||||
Self(player_id, Instant::now())
|
||||
}
|
||||
|
||||
pub const fn player_id(&self) -> &PlayerId {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub const fn connect_time(&self) -> &Instant {
|
||||
&self.1
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct JoinedPlayer {
|
||||
sender: Sender<ServerMessage>,
|
||||
receiver: Receiver<ServerMessage>,
|
||||
active_connection: ConnectionId,
|
||||
in_game: bool,
|
||||
pub name: String,
|
||||
pub number: NonZeroU8,
|
||||
pub pronouns: Option<String>,
|
||||
}
|
||||
|
||||
impl JoinedPlayer {
|
||||
pub const fn new(
|
||||
sender: Sender<ServerMessage>,
|
||||
receiver: Receiver<ServerMessage>,
|
||||
active_connection: ConnectionId,
|
||||
name: String,
|
||||
number: NonZeroU8,
|
||||
pronouns: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
name,
|
||||
number,
|
||||
sender,
|
||||
pronouns,
|
||||
receiver,
|
||||
active_connection,
|
||||
in_game: false,
|
||||
}
|
||||
}
|
||||
pub fn resubscribe_reciever(&self) -> Receiver<ServerMessage> {
|
||||
self.receiver.resubscribe()
|
||||
}
|
||||
|
||||
pub fn sender(&self) -> Sender<ServerMessage> {
|
||||
self.sender.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct JoinedPlayers {
|
||||
players: Arc<Mutex<HashMap<PlayerId, JoinedPlayer>>>,
|
||||
connect_state_sender: Sender<(PlayerId, bool)>,
|
||||
}
|
||||
|
||||
impl JoinedPlayers {
|
||||
pub fn new(connect_state_sender: Sender<(PlayerId, bool)>) -> Self {
|
||||
Self {
|
||||
connect_state_sender,
|
||||
players: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_all_lobby(&self, in_lobby: Box<[PublicIdentity]>, in_lobby_ids: &[PlayerId]) {
|
||||
let players: tokio::sync::MutexGuard<'_, HashMap<PlayerId, JoinedPlayer>> =
|
||||
self.players.lock().await;
|
||||
let senders = players
|
||||
.iter()
|
||||
.map(|(pid, p)| (pid.clone(), p.sender.clone()))
|
||||
.collect::<Box<[_]>>();
|
||||
core::mem::drop(players);
|
||||
for (pid, send) in senders {
|
||||
send.send(ServerMessage::LobbyInfo {
|
||||
joined: in_lobby_ids.contains(&pid),
|
||||
players: in_lobby.clone(),
|
||||
})
|
||||
.log_debug();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn is_connected(&self, player_id: &PlayerId) -> bool {
|
||||
self.players.lock().await.contains_key(player_id)
|
||||
}
|
||||
|
||||
pub async fn update(&self, player_id: &PlayerId, f: impl FnOnce(&mut JoinedPlayer)) {
|
||||
if let Some(p) = self
|
||||
.players
|
||||
.lock()
|
||||
.await
|
||||
.iter_mut()
|
||||
.find_map(|(pid, p)| (player_id == pid).then_some(p))
|
||||
{
|
||||
f(p)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_name(&self, player_id: &PlayerId) -> Option<String> {
|
||||
self.players.lock().await.iter().find_map(|(pid, p)| {
|
||||
if pid == player_id {
|
||||
Some(p.name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Disconnect the player
|
||||
///
|
||||
/// Will not disconnect if the player is currently in a game, allowing them to reconnect
|
||||
pub async fn disconnect(&self, connection: &ConnectionId) -> Option<JoinedPlayer> {
|
||||
let mut map = self.players.lock().await;
|
||||
|
||||
self.connect_state_sender
|
||||
.send((connection.0.clone(), false))
|
||||
.log_warn();
|
||||
|
||||
if map
|
||||
.get(connection.player_id())
|
||||
.map(|p| p.active_connection == *connection && !p.in_game)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
return map.remove(connection.player_id());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn start_game_with(&self, players: &[PlayerId]) -> Result<InGameToken, GameError> {
|
||||
let mut map = self.players.lock().await;
|
||||
if !players.iter().all(|p| map.contains_key(p)) {
|
||||
return Err(GameError::NotAllPlayersConnected);
|
||||
}
|
||||
for player in players {
|
||||
unsafe { map.get_mut(player).unwrap_unchecked() }.in_game = true;
|
||||
}
|
||||
|
||||
Ok(InGameToken::new(
|
||||
self.clone(),
|
||||
players.iter().cloned().collect(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn release_from_game(&self, players: &[PlayerId]) {
|
||||
self.players
|
||||
.lock()
|
||||
.await
|
||||
.iter_mut()
|
||||
.filter(|(p, _)| players.contains(*p))
|
||||
.for_each(|(_, p)| p.in_game = false)
|
||||
}
|
||||
|
||||
pub async fn get_sender(&self, player_id: &PlayerId) -> Option<Sender<ServerMessage>> {
|
||||
self.players
|
||||
.lock()
|
||||
.await
|
||||
.get(player_id)
|
||||
.map(|c| c.sender.clone())
|
||||
}
|
||||
|
||||
pub async fn insert_or_replace(
|
||||
&self,
|
||||
player_id: PlayerId,
|
||||
player: JoinedPlayer,
|
||||
) -> Receiver<ServerMessage> {
|
||||
let mut map = self.players.lock().await;
|
||||
|
||||
if let Some(old) = map.insert(player_id.clone(), player) {
|
||||
let old_map_entry = unsafe { map.get_mut(&player_id).unwrap_unchecked() };
|
||||
old_map_entry.receiver = old.resubscribe_reciever();
|
||||
|
||||
old.receiver
|
||||
} else {
|
||||
self.connect_state_sender
|
||||
.send((player_id.clone(), true))
|
||||
.log_warn();
|
||||
unsafe { map.get(&player_id).unwrap_unchecked() }
|
||||
.receiver
|
||||
.resubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InGameToken {
|
||||
joined_players: JoinedPlayers,
|
||||
players_in_game: Option<Box<[PlayerId]>>,
|
||||
}
|
||||
impl InGameToken {
|
||||
const fn new(joined_players: JoinedPlayers, players_in_game: Box<[PlayerId]>) -> Self {
|
||||
Self {
|
||||
joined_players,
|
||||
players_in_game: Some(players_in_game),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Drop for InGameToken {
|
||||
fn drop(&mut self) {
|
||||
let joined_players = self.joined_players.clone();
|
||||
if let Some(players) = self.players_in_game.take() {
|
||||
tokio::spawn(async move {
|
||||
let players_in_game = players;
|
||||
joined_players.release_from_game(&players_in_game).await;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
use core::ops::Not;
|
||||
|
||||
use crate::{
|
||||
LogError,
|
||||
communication::{Comms, lobby::LobbyComms},
|
||||
connection::{InGameToken, JoinedPlayers},
|
||||
lobby::{Lobby, PlayerIdSender},
|
||||
runner::{IdentifiedClientMessage, Message},
|
||||
};
|
||||
use tokio::{sync::broadcast::Receiver, time::Instant};
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
game::{Game, GameOver, Village},
|
||||
message::{
|
||||
ClientMessage, Identification, ServerMessage,
|
||||
host::{HostGameMessage, HostMessage, HostNightMessage, ServerToHostMessage},
|
||||
},
|
||||
player::{Character, PlayerId},
|
||||
};
|
||||
|
||||
type Result<T> = core::result::Result<T, GameError>;
|
||||
|
||||
pub struct GameRunner {
|
||||
game: Game,
|
||||
comms: Comms,
|
||||
connect_recv: Receiver<(PlayerId, bool)>,
|
||||
player_sender: PlayerIdSender,
|
||||
roles_revealed: bool,
|
||||
joined_players: JoinedPlayers,
|
||||
_release_token: InGameToken,
|
||||
cover_of_darkness: bool,
|
||||
}
|
||||
|
||||
impl GameRunner {
|
||||
pub const fn new(
|
||||
game: Game,
|
||||
comms: Comms,
|
||||
player_sender: PlayerIdSender,
|
||||
connect_recv: Receiver<(PlayerId, bool)>,
|
||||
joined_players: JoinedPlayers,
|
||||
release_token: InGameToken,
|
||||
) -> Self {
|
||||
Self {
|
||||
game,
|
||||
comms,
|
||||
connect_recv,
|
||||
player_sender,
|
||||
joined_players,
|
||||
roles_revealed: false,
|
||||
_release_token: release_token,
|
||||
cover_of_darkness: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn comms(&mut self) -> &mut Comms {
|
||||
&mut self.comms
|
||||
}
|
||||
|
||||
pub fn into_lobby(self) -> Lobby {
|
||||
Lobby::new(
|
||||
self.joined_players,
|
||||
LobbyComms::new(self.comms, self.connect_recv),
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn proto_game(&self) -> &Game {
|
||||
&self.game
|
||||
}
|
||||
|
||||
pub async fn role_reveal(&mut self) {
|
||||
for char in self.game.village().characters() {
|
||||
if let Err(err) = self.player_sender.send_if_present(
|
||||
char.player_id(),
|
||||
ServerMessage::GameStart {
|
||||
role: char.role().initial_shown_role(),
|
||||
},
|
||||
) {
|
||||
log::warn!(
|
||||
"failed sending role info to [{}]({}): {err}",
|
||||
char.player_id(),
|
||||
char.name()
|
||||
)
|
||||
}
|
||||
}
|
||||
let mut acks = self
|
||||
.game
|
||||
.village()
|
||||
.characters()
|
||||
.into_iter()
|
||||
.map(|c| (c, false))
|
||||
.collect::<Box<[_]>>();
|
||||
|
||||
let update_host = |acks: &[(Character, bool)], comms: &mut Comms| {
|
||||
comms
|
||||
.host()
|
||||
.send(ServerToHostMessage::WaitingForRoleRevealAcks {
|
||||
ackd: acks
|
||||
.iter()
|
||||
.filter_map(|(a, ackd)| ackd.then_some(a.target()))
|
||||
.collect(),
|
||||
waiting: acks
|
||||
.iter()
|
||||
.filter_map(|(a, ackd)| ackd.not().then_some(a.target()))
|
||||
.collect(),
|
||||
})
|
||||
.log_err();
|
||||
};
|
||||
(update_host)(&acks, &mut self.comms);
|
||||
let notify_of_role = |player_id: &PlayerId, village: &Village, sender: &PlayerIdSender| {
|
||||
if let Some(char) = village.character_by_player_id(player_id) {
|
||||
sender
|
||||
.send_if_present(
|
||||
player_id,
|
||||
ServerMessage::GameStart {
|
||||
role: char.role().initial_shown_role(),
|
||||
},
|
||||
)
|
||||
.log_debug();
|
||||
}
|
||||
};
|
||||
|
||||
let mut last_err_log = tokio::time::Instant::now() - tokio::time::Duration::from_secs(60);
|
||||
while acks.iter().any(|(_, ackd)| !*ackd) {
|
||||
let msg = match self.comms.message().await {
|
||||
Ok(msg) => msg,
|
||||
Err(err) => {
|
||||
if (tokio::time::Instant::now() - last_err_log).as_secs() >= 30 {
|
||||
log::error!("recv during role_reveal: {err}");
|
||||
last_err_log = tokio::time::Instant::now();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
};
|
||||
match msg {
|
||||
Message::Host(HostMessage::ForceRoleAckFor(char_id)) => {
|
||||
if let Some((c, ackd)) =
|
||||
acks.iter_mut().find(|(c, _)| c.character_id() == &char_id)
|
||||
{
|
||||
*ackd = true;
|
||||
(notify_of_role)(c.player_id(), self.game.village(), &self.player_sender);
|
||||
}
|
||||
(update_host)(&acks, &mut self.comms);
|
||||
}
|
||||
Message::Host(_) => {
|
||||
(update_host)(&acks, &mut self.comms);
|
||||
}
|
||||
Message::Client(IdentifiedClientMessage {
|
||||
identity:
|
||||
Identification {
|
||||
player_id,
|
||||
public: _,
|
||||
},
|
||||
message: ClientMessage::RoleAck,
|
||||
}) => {
|
||||
if let Some((_, ackd)) =
|
||||
acks.iter_mut().find(|(t, _)| t.player_id() == &player_id)
|
||||
{
|
||||
*ackd = true;
|
||||
self.player_sender
|
||||
.send_if_present(&player_id, ServerMessage::Sleep)
|
||||
.log_debug();
|
||||
}
|
||||
(update_host)(&acks, &mut self.comms);
|
||||
}
|
||||
Message::Client(IdentifiedClientMessage {
|
||||
identity:
|
||||
Identification {
|
||||
player_id,
|
||||
public: _,
|
||||
},
|
||||
message: _,
|
||||
})
|
||||
| Message::Connect(player_id) => {
|
||||
(notify_of_role)(&player_id, self.game.village(), &self.player_sender)
|
||||
}
|
||||
Message::Disconnect(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.roles_revealed = true;
|
||||
}
|
||||
|
||||
pub async fn next(&mut self) -> Option<GameOver> {
|
||||
let msg = self.comms.host().recv().await.expect("host channel closed");
|
||||
match self.host_message(msg) {
|
||||
Ok(resp) => {
|
||||
self.comms.host().send(resp).log_warn();
|
||||
}
|
||||
Err(err) => {
|
||||
self.comms
|
||||
.host()
|
||||
.send(ServerToHostMessage::Error(err))
|
||||
.log_warn();
|
||||
}
|
||||
}
|
||||
self.game.game_over()
|
||||
}
|
||||
|
||||
pub fn host_message(&mut self, message: HostMessage) -> Result<ServerToHostMessage> {
|
||||
if !self.roles_revealed {
|
||||
return Err(GameError::NeedRoleReveal);
|
||||
}
|
||||
if self.cover_of_darkness {
|
||||
match &message {
|
||||
HostMessage::GetState | HostMessage::InGame(HostGameMessage::GetState) => {
|
||||
return Ok(ServerToHostMessage::CoverOfDarkness);
|
||||
}
|
||||
HostMessage::InGame(HostGameMessage::Night(HostNightMessage::Next)) => {
|
||||
self.cover_of_darkness = false;
|
||||
return self.host_message(HostMessage::GetState);
|
||||
}
|
||||
_ => return Err(GameError::InvalidMessageForGameState),
|
||||
};
|
||||
}
|
||||
match message {
|
||||
HostMessage::GetState => self.game.process(HostGameMessage::GetState),
|
||||
HostMessage::InGame(msg) => self.game.process(msg),
|
||||
HostMessage::Lobby(_) | HostMessage::NewLobby | HostMessage::ForceRoleAckFor(_) => {
|
||||
Err(GameError::InvalidMessageForGameState)
|
||||
}
|
||||
HostMessage::Echo(echo) => Ok(echo),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GameEnd {
|
||||
game: Option<GameRunner>,
|
||||
result: GameOver,
|
||||
last_error_log: Instant,
|
||||
}
|
||||
|
||||
impl GameEnd {
|
||||
pub fn new(game: GameRunner, result: GameOver) -> Self {
|
||||
Self {
|
||||
result,
|
||||
game: Some(game),
|
||||
last_error_log: Instant::now() - core::time::Duration::from_secs(60),
|
||||
}
|
||||
}
|
||||
|
||||
const fn game(&mut self) -> Result<&mut GameRunner> {
|
||||
match self.game.as_mut() {
|
||||
Some(game) => Ok(game),
|
||||
None => Err(GameError::InactiveGameObject),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end_screen(&mut self) -> Result<()> {
|
||||
let result = self.result;
|
||||
for char in self.game()?.game.village().characters() {
|
||||
self.game()?
|
||||
.player_sender
|
||||
.send_if_present(char.player_id(), ServerMessage::GameOver(result))
|
||||
.log_debug();
|
||||
}
|
||||
self.game()?
|
||||
.comms
|
||||
.host()
|
||||
.send(ServerToHostMessage::GameOver(result))
|
||||
.log_warn();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn next(&mut self) -> Option<Lobby> {
|
||||
let msg = match self.game().unwrap().comms.message().await {
|
||||
Ok(msg) => msg,
|
||||
Err(err) => {
|
||||
if (Instant::now() - self.last_error_log).as_secs() >= 30 {
|
||||
log::error!("getting message: {err}");
|
||||
self.last_error_log = Instant::now();
|
||||
}
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match msg {
|
||||
Message::Host(HostMessage::Echo(msg)) => {
|
||||
self.game().unwrap().comms.host().send(msg).log_debug();
|
||||
}
|
||||
Message::Host(HostMessage::GetState) => {
|
||||
let result = self.result;
|
||||
self.game()
|
||||
.unwrap()
|
||||
.comms
|
||||
.host()
|
||||
.send(ServerToHostMessage::GameOver(result))
|
||||
.log_debug()
|
||||
}
|
||||
Message::Host(HostMessage::NewLobby) => {
|
||||
self.game()
|
||||
.unwrap()
|
||||
.comms
|
||||
.host()
|
||||
.send(ServerToHostMessage::Lobby(Box::new([])))
|
||||
.log_debug();
|
||||
let lobby = self.game.take().unwrap().into_lobby();
|
||||
return Some(lobby);
|
||||
}
|
||||
Message::Host(_) => self
|
||||
.game()
|
||||
.unwrap()
|
||||
.comms
|
||||
.host()
|
||||
.send(ServerToHostMessage::Error(
|
||||
GameError::InvalidMessageForGameState,
|
||||
))
|
||||
.log_debug(),
|
||||
Message::Client(IdentifiedClientMessage {
|
||||
identity,
|
||||
message: _,
|
||||
}) => {
|
||||
let result = self.result;
|
||||
self.game()
|
||||
.unwrap()
|
||||
.player_sender
|
||||
.send_if_present(&identity.player_id, ServerMessage::GameOver(result))
|
||||
.log_debug();
|
||||
}
|
||||
Message::Connect(_) | Message::Disconnect(_) => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
use core::net::SocketAddr;
|
||||
|
||||
use axum::{
|
||||
extract::{
|
||||
ConnectInfo, State, WebSocketUpgrade,
|
||||
ws::{self, Message, WebSocket},
|
||||
},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use axum_extra::{TypedHeader, headers};
|
||||
use colored::Colorize;
|
||||
use tokio::sync::{broadcast::Receiver, mpsc::Sender};
|
||||
use werewolves_proto::message::host::{HostMessage, ServerToHostMessage};
|
||||
|
||||
use crate::{AppState, LogError, XForwardedFor};
|
||||
|
||||
pub async fn handler(
|
||||
ws: WebSocketUpgrade,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
x_forwarded_for: Option<TypedHeader<XForwardedFor>>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse {
|
||||
let who = x_forwarded_for
|
||||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| addr.to_string());
|
||||
log::info!(
|
||||
"{who}{} connected.",
|
||||
user_agent
|
||||
.map(|agent| format!(" (User-Agent: {})", agent.as_str()))
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
// finalize the upgrade process by returning upgrade callback.
|
||||
// we can customize the callback by sending additional info such as address.
|
||||
ws.on_upgrade(move |socket| async move {
|
||||
Host::new(
|
||||
socket,
|
||||
state.host_send.clone(),
|
||||
state.host_recv.resubscribe(),
|
||||
)
|
||||
.run()
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
struct Host {
|
||||
socket: WebSocket,
|
||||
host_send: Sender<HostMessage>,
|
||||
server_recv: Receiver<ServerToHostMessage>,
|
||||
}
|
||||
|
||||
impl Host {
|
||||
pub fn new(
|
||||
socket: WebSocket,
|
||||
host_send: Sender<HostMessage>,
|
||||
server_recv: Receiver<ServerToHostMessage>,
|
||||
) -> Self {
|
||||
Self {
|
||||
host_send,
|
||||
server_recv,
|
||||
socket,
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_recv(
|
||||
&mut self,
|
||||
msg: Option<Result<Message, axum::Error>>,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
#[cfg(not(feature = "cbor"))]
|
||||
let msg: HostMessage = serde_json::from_slice(
|
||||
&match msg {
|
||||
Some(Ok(msg)) => msg,
|
||||
Some(Err(err)) => return Err(err.into()),
|
||||
None => {
|
||||
log::warn!("[host] no message");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
.into_data(),
|
||||
)?;
|
||||
#[cfg(feature = "cbor")]
|
||||
let msg: HostMessage = {
|
||||
let bytes = match msg {
|
||||
Some(Ok(msg)) => msg.into_data(),
|
||||
Some(Err(err)) => return Err(err.into()),
|
||||
None => {
|
||||
log::warn!("[host] no message");
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let slice: &[u8] = &bytes;
|
||||
ciborium::from_reader(slice)?
|
||||
};
|
||||
if let HostMessage::Echo(echo) = &msg {
|
||||
self.send_message(echo).await.log_warn();
|
||||
return Ok(());
|
||||
}
|
||||
log::debug!(
|
||||
"{} {}",
|
||||
"[host::incoming::message]".bold(),
|
||||
format!("{msg:?}").dimmed()
|
||||
);
|
||||
Ok(self.host_send.send(msg).await?)
|
||||
}
|
||||
|
||||
async fn send_message(&mut self, msg: &ServerToHostMessage) -> Result<(), anyhow::Error> {
|
||||
Ok(self
|
||||
.socket
|
||||
.send(
|
||||
#[cfg(not(feature = "cbor"))]
|
||||
ws::Message::Text(serde_json::to_string(msg)?.into()),
|
||||
#[cfg(feature = "cbor")]
|
||||
ws::Message::Binary({
|
||||
let mut bytes = Vec::new();
|
||||
ciborium::into_writer(msg, &mut bytes)?;
|
||||
bytes.into()
|
||||
}),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn run(mut self) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
msg = self.socket.recv() => {
|
||||
if let Err(err) = self.on_recv(msg).await {
|
||||
log::error!("{} {err}", "[host::incoming]".bold());
|
||||
return;
|
||||
}
|
||||
},
|
||||
msg = self.server_recv.recv() => {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
log::debug!("sending message to host: {}", format!("{msg:?}").dimmed());
|
||||
if let Err(err) = self.send_message(&msg).await {
|
||||
log::error!("{} {err}", "[host::outgoing]".bold())
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("{} {err}", "[host::mpsc]".bold());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
use core::{
|
||||
num::NonZeroU8,
|
||||
ops::{Deref, DerefMut},
|
||||
time::Duration,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::broadcast::Sender;
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
game::{Game, GameSettings, Village},
|
||||
message::{
|
||||
CharacterState, ClientMessage, DayCharacter, Identification, PlayerState, ServerMessage,
|
||||
UpdateSelf,
|
||||
host::{HostLobbyMessage, HostMessage, ServerToHostMessage},
|
||||
},
|
||||
player::PlayerId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
LogError,
|
||||
communication::lobby::LobbyComms,
|
||||
connection::JoinedPlayers,
|
||||
game::GameRunner,
|
||||
runner::{IdentifiedClientMessage, Message},
|
||||
};
|
||||
|
||||
pub struct Lobby {
|
||||
players_in_lobby: PlayerIdSender,
|
||||
settings: GameSettings,
|
||||
joined_players: JoinedPlayers,
|
||||
comms: Option<LobbyComms>,
|
||||
}
|
||||
|
||||
impl Lobby {
|
||||
pub fn new(joined_players: JoinedPlayers, comms: LobbyComms) -> Self {
|
||||
Self {
|
||||
joined_players,
|
||||
comms: Some(comms),
|
||||
settings: GameSettings::default(),
|
||||
players_in_lobby: PlayerIdSender(Vec::new()),
|
||||
}
|
||||
}
|
||||
|
||||
const fn comms(&mut self) -> Result<&mut LobbyComms, GameError> {
|
||||
match self.comms.as_mut() {
|
||||
Some(comms) => Ok(comms),
|
||||
None => Err(GameError::InactiveGameObject),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_lobby_info_to_clients(&mut self) {
|
||||
let players = self
|
||||
.players_in_lobby
|
||||
.iter()
|
||||
.map(|(id, _)| id.public.clone())
|
||||
.collect::<Box<[_]>>();
|
||||
self.joined_players
|
||||
.send_all_lobby(
|
||||
players,
|
||||
&self
|
||||
.players_in_lobby
|
||||
.iter()
|
||||
.map(|(id, _)| id.player_id.clone())
|
||||
.collect::<Box<[_]>>(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn get_lobby_player_list(&self) -> Box<[PlayerState]> {
|
||||
let mut players = Vec::new();
|
||||
for (player, _) in self.players_in_lobby.iter() {
|
||||
players.push(PlayerState {
|
||||
identification: player.clone(),
|
||||
connected: self.joined_players.is_connected(&player.player_id).await,
|
||||
});
|
||||
}
|
||||
|
||||
players.into_boxed_slice()
|
||||
}
|
||||
|
||||
async fn send_lobby_info_to_host(&mut self) -> Result<(), GameError> {
|
||||
let players = self.get_lobby_player_list().await;
|
||||
self.comms()?
|
||||
.host()
|
||||
.send(ServerToHostMessage::Lobby(players))
|
||||
.map_err(|err| GameError::GenericError(err.to_string()))
|
||||
}
|
||||
|
||||
pub async fn next(&mut self) -> Option<GameRunner> {
|
||||
let msg = self
|
||||
.comms()
|
||||
.unwrap()
|
||||
.next_message()
|
||||
.await
|
||||
.expect("get next message");
|
||||
|
||||
match self.next_inner(msg.clone()).await.map_err(|err| (msg, err)) {
|
||||
Ok(None) => {}
|
||||
Ok(Some(mut game)) => {
|
||||
game.role_reveal().await;
|
||||
match game.host_message(HostMessage::GetState) {
|
||||
Ok(msg) => {
|
||||
log::info!("initial message after night reveal: {msg:?}");
|
||||
game.comms().host().send(msg).log_warn();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("processing get_state after role reveal to host: {err}")
|
||||
}
|
||||
}
|
||||
|
||||
return Some(game);
|
||||
}
|
||||
Err((Message::Host(_), err)) => self
|
||||
.comms()
|
||||
.unwrap()
|
||||
.host()
|
||||
.send(ServerToHostMessage::Error(err))
|
||||
.log_warn(),
|
||||
Err((
|
||||
Message::Client(IdentifiedClientMessage {
|
||||
identity:
|
||||
Identification {
|
||||
player_id,
|
||||
public: _,
|
||||
},
|
||||
message: _,
|
||||
}),
|
||||
GameError::InvalidMessageForGameState,
|
||||
)) => {
|
||||
let _ = self
|
||||
.players_in_lobby
|
||||
.send_if_present(&player_id, ServerMessage::InvalidMessageForGameState);
|
||||
}
|
||||
Err((
|
||||
Message::Client(IdentifiedClientMessage {
|
||||
identity: Identification { player_id, public },
|
||||
message: _,
|
||||
}),
|
||||
err,
|
||||
)) => {
|
||||
log::error!("processing message from {public} [{player_id}]: {err}");
|
||||
let _ = self
|
||||
.players_in_lobby
|
||||
.send_if_present(&player_id, ServerMessage::Reset);
|
||||
}
|
||||
Err((Message::Connect(_), _)) | Err((Message::Disconnect(_), _)) => {}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
async fn next_inner(&mut self, msg: Message) -> Result<Option<GameRunner>, GameError> {
|
||||
match msg {
|
||||
Message::Host(HostMessage::InGame(_))
|
||||
| Message::Host(HostMessage::ForceRoleAckFor(_)) => {
|
||||
return Err(GameError::InvalidMessageForGameState);
|
||||
}
|
||||
Message::Host(HostMessage::NewLobby) => self
|
||||
.comms()
|
||||
.unwrap()
|
||||
.host()
|
||||
.send(ServerToHostMessage::Error(
|
||||
GameError::InvalidMessageForGameState,
|
||||
))
|
||||
.log_warn(),
|
||||
Message::Host(HostMessage::Lobby(HostLobbyMessage::GetState))
|
||||
| Message::Host(HostMessage::GetState) => self.send_lobby_info_to_host().await?,
|
||||
Message::Host(HostMessage::Lobby(HostLobbyMessage::GetGameSettings)) => {
|
||||
let msg = ServerToHostMessage::GameSettings(self.settings.clone());
|
||||
let _ = self.comms().unwrap().host().send(msg);
|
||||
}
|
||||
Message::Host(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(settings))) => {
|
||||
settings.check()?;
|
||||
self.settings = settings;
|
||||
}
|
||||
Message::Host(HostMessage::Lobby(HostLobbyMessage::Start)) => {
|
||||
if self.players_in_lobby.len() < self.settings.min_players_needed() {
|
||||
return Err(GameError::TooFewPlayers {
|
||||
got: self.players_in_lobby.len() as _,
|
||||
need: self.settings.min_players_needed() as _,
|
||||
});
|
||||
}
|
||||
let playing_players = self
|
||||
.players_in_lobby
|
||||
.iter()
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect::<Box<[_]>>();
|
||||
let release_token = self
|
||||
.joined_players
|
||||
.start_game_with(
|
||||
&playing_players
|
||||
.iter()
|
||||
.map(|id| id.player_id.clone())
|
||||
.collect::<Box<[_]>>(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let game = Game::new(&playing_players, self.settings.clone())?;
|
||||
assert_eq!(game.village().characters().len(), playing_players.len());
|
||||
|
||||
let (comms, recv) = self.comms.take().unwrap().into_inner();
|
||||
return Ok(Some(GameRunner::new(
|
||||
game,
|
||||
comms,
|
||||
self.players_in_lobby.drain(),
|
||||
recv,
|
||||
self.joined_players.clone(),
|
||||
release_token,
|
||||
)));
|
||||
}
|
||||
Message::Client(IdentifiedClientMessage {
|
||||
identity,
|
||||
message: ClientMessage::Hello,
|
||||
}) => {
|
||||
if self
|
||||
.players_in_lobby
|
||||
.iter_mut()
|
||||
.any(|p| p.0.player_id == identity.player_id)
|
||||
{
|
||||
// Already have the player
|
||||
return Ok(None);
|
||||
}
|
||||
if let Some(sender) = self.joined_players.get_sender(&identity.player_id).await {
|
||||
self.players_in_lobby.push((identity, sender.clone()));
|
||||
self.send_lobby_info_to_clients().await;
|
||||
self.send_lobby_info_to_host().await?;
|
||||
}
|
||||
}
|
||||
Message::Host(HostMessage::Lobby(HostLobbyMessage::Kick(player_id)))
|
||||
| Message::Client(IdentifiedClientMessage {
|
||||
identity:
|
||||
Identification {
|
||||
player_id,
|
||||
public: _,
|
||||
},
|
||||
message: ClientMessage::Goodbye,
|
||||
}) => {
|
||||
log::error!("we are in there");
|
||||
if let Some(remove_idx) = self
|
||||
.players_in_lobby
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(idx, p)| (p.0.player_id == player_id).then_some(idx))
|
||||
{
|
||||
log::error!("removing player {player_id} at idx {remove_idx}");
|
||||
self.players_in_lobby.swap_remove(remove_idx);
|
||||
self.send_lobby_info_to_host().await?;
|
||||
self.send_lobby_info_to_clients().await;
|
||||
}
|
||||
}
|
||||
Message::Client(IdentifiedClientMessage {
|
||||
identity:
|
||||
Identification {
|
||||
player_id,
|
||||
public: _,
|
||||
},
|
||||
message: ClientMessage::GetState,
|
||||
}) => {
|
||||
let msg = ServerMessage::LobbyInfo {
|
||||
joined: self
|
||||
.players_in_lobby
|
||||
.iter()
|
||||
.any(|(p, _)| p.player_id == player_id),
|
||||
players: self
|
||||
.players_in_lobby
|
||||
.iter()
|
||||
.map(|(id, _)| id.public.clone())
|
||||
.collect(),
|
||||
};
|
||||
if let Some(sender) = self.joined_players.get_sender(&player_id).await {
|
||||
sender.send(msg).log_debug();
|
||||
}
|
||||
}
|
||||
Message::Client(IdentifiedClientMessage {
|
||||
identity: _,
|
||||
message: ClientMessage::RoleAck,
|
||||
}) => return Err(GameError::InvalidMessageForGameState),
|
||||
Message::Client(IdentifiedClientMessage {
|
||||
identity: Identification { player_id, public },
|
||||
message: ClientMessage::UpdateSelf(_),
|
||||
}) => {
|
||||
self.joined_players
|
||||
.update(&player_id, move |p| {
|
||||
p.name = public.name;
|
||||
p.number = public.number;
|
||||
p.pronouns = public.pronouns;
|
||||
})
|
||||
.await;
|
||||
self.send_lobby_info_to_clients().await;
|
||||
self.send_lobby_info_to_host().await.log_debug();
|
||||
}
|
||||
Message::Connect(_) | Message::Disconnect(_) => self.send_lobby_info_to_host().await?,
|
||||
Message::Host(HostMessage::Echo(msg)) => {
|
||||
self.comms()?.host().send(msg).log_warn();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PlayerIdSender(Vec<(Identification, Sender<ServerMessage>)>);
|
||||
|
||||
impl Deref for PlayerIdSender {
|
||||
type Target = Vec<(Identification, Sender<ServerMessage>)>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for PlayerIdSender {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PlayerIdSender {
|
||||
pub fn find(&self, player_id: &PlayerId) -> Option<&Sender<ServerMessage>> {
|
||||
self.iter()
|
||||
.find_map(|(id, s)| (&id.player_id == player_id).then_some(s))
|
||||
}
|
||||
|
||||
pub fn send_if_present(
|
||||
&self,
|
||||
player_id: &PlayerId,
|
||||
message: ServerMessage,
|
||||
) -> Result<(), GameError> {
|
||||
if let Some(sender) = self.find(player_id) {
|
||||
sender
|
||||
.send(message)
|
||||
.map(|_| ())
|
||||
.map_err(|err| GameError::GenericError(err.to_string()))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drain(&mut self) -> Self {
|
||||
let mut swapped = Self(vec![]);
|
||||
core::mem::swap(&mut swapped, self);
|
||||
swapped
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,267 @@
|
|||
mod client;
|
||||
mod communication;
|
||||
mod connection;
|
||||
mod game;
|
||||
mod host;
|
||||
mod lobby;
|
||||
mod runner;
|
||||
mod saver;
|
||||
|
||||
use axum::{
|
||||
Router,
|
||||
http::{Request, header},
|
||||
response::IntoResponse,
|
||||
routing::{any, get},
|
||||
};
|
||||
use axum_extra::headers;
|
||||
use communication::lobby::LobbyComms;
|
||||
use connection::JoinedPlayers;
|
||||
use core::{fmt::Display, net::SocketAddr, str::FromStr};
|
||||
use log::Record;
|
||||
use runner::IdentifiedClientMessage;
|
||||
use std::{env, io::Write, path::Path};
|
||||
use tokio::{
|
||||
sync::{broadcast, mpsc},
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
communication::{Comms, host::HostComms, player::PlayerIdComms},
|
||||
saver::FileSaver,
|
||||
};
|
||||
|
||||
const DEFAULT_PORT: u16 = 8080;
|
||||
const DEFAULT_HOST: &str = "127.0.0.1";
|
||||
const DEFAULT_SAVE_DIR: &str = "werewolves-saves/";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// pretty_env_logger::init();
|
||||
use colored::Colorize;
|
||||
pretty_env_logger::formatted_builder()
|
||||
.parse_default_env()
|
||||
.format(|f, record| {
|
||||
let time = chrono::Local::now().time().to_string().dimmed();
|
||||
|
||||
match record.file() {
|
||||
Some(file) => {
|
||||
let file = format!(
|
||||
"[{file}{}]",
|
||||
record
|
||||
.line()
|
||||
.map(|l| format!(":{l}"))
|
||||
.unwrap_or_else(String::new),
|
||||
)
|
||||
.dimmed();
|
||||
let level = match record.level() {
|
||||
log::Level::Error => "[err]".red().bold(),
|
||||
log::Level::Warn => "[warn]".yellow().bold(),
|
||||
log::Level::Info => "[info]".white().bold(),
|
||||
log::Level::Debug => "[debug]".dimmed().bold(),
|
||||
log::Level::Trace => "[trace]".dimmed(),
|
||||
};
|
||||
let args = record.args();
|
||||
|
||||
let arrow = "➢".bold().magenta();
|
||||
writeln!(
|
||||
f,
|
||||
"{time} {file}\n{level} {arrow} {args}",
|
||||
// "⇗⇘⇗⇘⇗⇘".bold().dimmed(),
|
||||
)
|
||||
}
|
||||
_ => writeln!(f, "{time} [{}] {}", record.level(), record.args()),
|
||||
}
|
||||
})
|
||||
.try_init()
|
||||
.unwrap();
|
||||
|
||||
let default_panic = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
default_panic(info);
|
||||
std::process::exit(1);
|
||||
}));
|
||||
|
||||
let host = env::var("HOST").unwrap_or(DEFAULT_HOST.to_string());
|
||||
let port = env::var("PORT")
|
||||
.map_err(|err| anyhow::anyhow!("{err}"))
|
||||
.map(|port_str| {
|
||||
port_str
|
||||
.parse::<u16>()
|
||||
.unwrap_or_else(|err| panic!("parse PORT={port_str} failed: {err}"))
|
||||
})
|
||||
.unwrap_or(DEFAULT_PORT);
|
||||
let listen_addr =
|
||||
SocketAddr::from_str(format!("{host}:{port}").as_str()).expect("invalid host/port");
|
||||
|
||||
let (send, recv) = broadcast::channel(100);
|
||||
let (server_send, host_recv) = broadcast::channel(100);
|
||||
let (host_send, server_recv) = mpsc::channel(100);
|
||||
let (connect_send, connect_recv) = broadcast::channel(100);
|
||||
let joined_players = JoinedPlayers::new(connect_send);
|
||||
let lobby_comms = LobbyComms::new(
|
||||
Comms::new(
|
||||
HostComms::new(server_send, server_recv),
|
||||
PlayerIdComms::new(joined_players.clone(), recv, connect_recv.resubscribe()),
|
||||
),
|
||||
connect_recv,
|
||||
);
|
||||
|
||||
let jp_clone = joined_players.clone();
|
||||
|
||||
let path = Path::new(option_env!("SAVE_PATH").unwrap_or(DEFAULT_SAVE_DIR))
|
||||
.canonicalize()
|
||||
.expect("canonicalizing path");
|
||||
if let Err(err) = std::fs::create_dir(&path)
|
||||
&& !matches!(err.kind(), std::io::ErrorKind::AlreadyExists)
|
||||
{
|
||||
panic!("creating save dir at [{path:?}]: {err}")
|
||||
}
|
||||
// Check if we can write to the path
|
||||
{
|
||||
let test_file_path = path.join(".test");
|
||||
if let Err(err) = std::fs::File::create(&test_file_path) {
|
||||
panic!("can't create files in {path:?}: {err}")
|
||||
}
|
||||
std::fs::remove_file(&test_file_path).log_err();
|
||||
}
|
||||
|
||||
let saver = FileSaver::new(path);
|
||||
tokio::spawn(async move {
|
||||
crate::runner::run_game(jp_clone, lobby_comms, saver).await;
|
||||
panic!("game over");
|
||||
});
|
||||
let state = AppState {
|
||||
joined_players,
|
||||
host_recv,
|
||||
host_send,
|
||||
send,
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/connect/client", any(client::handler))
|
||||
.route("/connect/host", any(host::handler))
|
||||
.with_state(state)
|
||||
.fallback(get(handle_http_static));
|
||||
let listener = tokio::net::TcpListener::bind(listen_addr).await.unwrap();
|
||||
log::info!("listening on {}", listener.local_addr().unwrap());
|
||||
axum::serve(
|
||||
listener,
|
||||
app.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
joined_players: JoinedPlayers,
|
||||
send: broadcast::Sender<IdentifiedClientMessage>,
|
||||
host_send: tokio::sync::mpsc::Sender<werewolves_proto::message::host::HostMessage>,
|
||||
host_recv: broadcast::Receiver<werewolves_proto::message::host::ServerToHostMessage>,
|
||||
}
|
||||
impl Clone for AppState {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
joined_players: self.joined_players.clone(),
|
||||
send: self.send.clone(),
|
||||
host_send: self.host_send.clone(),
|
||||
host_recv: self.host_recv.resubscribe(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_http_static(req: Request<axum::body::Body>) -> impl IntoResponse {
|
||||
use mime_sniffer::MimeTypeSniffer;
|
||||
const INDEX_FILE: &[u8] = include_bytes!("../../werewolves/dist/index.html");
|
||||
let path = req.uri().path();
|
||||
|
||||
werewolves_macros::include_dist!(DIST_FILES, "werewolves/dist");
|
||||
|
||||
let file = if let Some(file) = DIST_FILES.iter().find_map(|(file_path, file)| {
|
||||
if *file_path == path {
|
||||
Some(*file)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
file
|
||||
} else {
|
||||
return (
|
||||
[(header::CONTENT_TYPE, "text/html".to_string())],
|
||||
INDEX_FILE,
|
||||
);
|
||||
};
|
||||
|
||||
let mime = if path.ends_with(".js") {
|
||||
"text/javascript".to_string()
|
||||
} else if path.ends_with(".css") {
|
||||
"text/css".to_string()
|
||||
} else if path.ends_with(".wasm") {
|
||||
"application/wasm".to_string()
|
||||
} else {
|
||||
file.sniff_mime_type()
|
||||
.unwrap_or("application/octet-stream")
|
||||
.to_string()
|
||||
};
|
||||
|
||||
([(header::CONTENT_TYPE, mime)], file)
|
||||
}
|
||||
|
||||
struct XForwardedFor(String);
|
||||
|
||||
impl Display for XForwardedFor {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl headers::Header for XForwardedFor {
|
||||
fn name() -> &'static header::HeaderName {
|
||||
static NAME: header::HeaderName = header::HeaderName::from_static("x-forwarded-for");
|
||||
&NAME
|
||||
}
|
||||
|
||||
fn decode<'i, I>(values: &mut I) -> Result<Self, headers::Error>
|
||||
where
|
||||
Self: Sized,
|
||||
I: Iterator<Item = &'i header::HeaderValue>,
|
||||
{
|
||||
Ok(Self(
|
||||
values
|
||||
.next()
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.ok_or(headers::Error::invalid())?
|
||||
.to_string(),
|
||||
))
|
||||
}
|
||||
|
||||
fn encode<E: Extend<header::HeaderValue>>(&self, _: &mut E) {}
|
||||
}
|
||||
|
||||
pub trait LogError {
|
||||
fn log_warn(self);
|
||||
fn log_err(self);
|
||||
fn log_debug(self);
|
||||
}
|
||||
|
||||
impl<T, E> LogError for Result<T, E>
|
||||
where
|
||||
E: Display,
|
||||
{
|
||||
fn log_warn(self) {
|
||||
if let Err(err) = self {
|
||||
log::warn!("{err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn log_err(self) {
|
||||
if let Err(err) = self {
|
||||
log::error!("{err}");
|
||||
}
|
||||
}
|
||||
|
||||
fn log_debug(self) {
|
||||
if let Err(err) = self {
|
||||
log::debug!("{err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
use core::time::Duration;
|
||||
|
||||
use thiserror::Error;
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
message::{ClientMessage, Identification, host::HostMessage},
|
||||
player::PlayerId,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
communication::lobby::LobbyComms,
|
||||
connection::JoinedPlayers,
|
||||
game::{GameEnd, GameRunner},
|
||||
lobby::Lobby,
|
||||
saver::Saver,
|
||||
};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum GameOrSendError<T> {
|
||||
#[error("game error: {0}")]
|
||||
GameError(#[from] GameError),
|
||||
#[error("send error: {0}")]
|
||||
SendError(#[from] tokio::sync::mpsc::error::SendError<T>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct IdentifiedClientMessage {
|
||||
pub identity: Identification,
|
||||
pub message: ClientMessage,
|
||||
}
|
||||
|
||||
pub async fn run_game(joined_players: JoinedPlayers, comms: LobbyComms, mut saver: impl Saver) {
|
||||
let mut state = RunningState::Lobby(Lobby::new(joined_players, comms));
|
||||
loop {
|
||||
match &mut state {
|
||||
RunningState::Lobby(lobby) => {
|
||||
if let Some(game) = lobby.next().await {
|
||||
state = RunningState::Game(game)
|
||||
}
|
||||
}
|
||||
RunningState::Game(game) => {
|
||||
if let Some(result) = game.next().await {
|
||||
match saver.save(game.proto_game()) {
|
||||
Ok(path) => {
|
||||
log::info!("saved game to {path}");
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("saving game: {err}");
|
||||
let game_clone = game.proto_game().clone();
|
||||
let mut saver_clone = saver.clone();
|
||||
tokio::spawn(async move {
|
||||
let started = chrono::Utc::now();
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
match saver_clone.save(&game_clone) {
|
||||
Ok(path) => {
|
||||
log::info!("saved game from {started} to {path}");
|
||||
return;
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("saving game from {started}: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
state = match state {
|
||||
RunningState::Game(game) => {
|
||||
RunningState::GameOver(GameEnd::new(game, result))
|
||||
}
|
||||
_ => unsafe { core::hint::unreachable_unchecked() },
|
||||
};
|
||||
}
|
||||
}
|
||||
RunningState::GameOver(end) => {
|
||||
if let Some(mut new_lobby) = end.next().await {
|
||||
new_lobby.send_lobby_info_to_clients().await;
|
||||
state = RunningState::Lobby(new_lobby)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Message {
|
||||
Host(HostMessage),
|
||||
Client(IdentifiedClientMessage),
|
||||
Connect(PlayerId),
|
||||
Disconnect(PlayerId),
|
||||
}
|
||||
|
||||
pub enum RunningState {
|
||||
Lobby(Lobby),
|
||||
Game(GameRunner),
|
||||
GameOver(GameEnd),
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
use core::fmt::Display;
|
||||
use std::{io::Write, path::PathBuf};
|
||||
|
||||
use thiserror::Error;
|
||||
use werewolves_proto::game::Game;
|
||||
|
||||
pub trait Saver: Clone + Send + 'static {
|
||||
type Error: Display;
|
||||
|
||||
fn save(&mut self, game: &Game) -> Result<String, Self::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FileSaverError {
|
||||
#[error("io error: {0}")]
|
||||
IoError(std::io::Error),
|
||||
#[error("serialization error")]
|
||||
SerializationError(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for FileSaverError {
|
||||
fn from(value: std::io::Error) -> Self {
|
||||
Self::IoError(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileSaver {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl FileSaver {
|
||||
pub const fn new(path: PathBuf) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
}
|
||||
|
||||
impl Saver for FileSaver {
|
||||
type Error = FileSaverError;
|
||||
|
||||
fn save(&mut self, game: &Game) -> Result<String, Self::Error> {
|
||||
let name = format!("werewolves_{}.json", chrono::Utc::now().timestamp());
|
||||
let path = self.path.join(name.clone());
|
||||
let mut file = std::fs::File::create_new(path.clone())?;
|
||||
serde_json::to_writer_pretty(&mut file, &game)?;
|
||||
file.flush()?;
|
||||
Ok(path.to_str().map(|s| s.to_string()).unwrap_or(name))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
[target.wasm32-unknown-unknown]
|
||||
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,42 @@
|
|||
[package]
|
||||
name = "werewolves"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
[dependencies]
|
||||
web-sys = { version = "0.3", features = [
|
||||
"HtmlTableCellElement",
|
||||
"Event",
|
||||
"EventTarget",
|
||||
"HtmlImageElement",
|
||||
"HtmlDivElement",
|
||||
"HtmlSelectElement",
|
||||
] }
|
||||
log = "0.4"
|
||||
rand = { version = "0.9", features = ["small_rng"] }
|
||||
getrandom = { version = "0.3", features = ["wasm_js"] }
|
||||
uuid = { version = "*", features = ["js"] }
|
||||
yew = { version = "0.21", features = ["csr"] }
|
||||
yew-router = "0.18.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
gloo = "0.11"
|
||||
wasm-logger = "0.2"
|
||||
instant = { version = "0.1", features = ["wasm-bindgen"] }
|
||||
once_cell = "1"
|
||||
chrono = { version = "0.4" }
|
||||
werewolves-macros = { path = "../werewolves-macros" }
|
||||
werewolves-proto = { path = "../werewolves-proto" }
|
||||
futures = "0.3.31"
|
||||
wasm-bindgen-futures = "0.4.50"
|
||||
thiserror = { version = "2" }
|
||||
convert_case = { version = "0.8" }
|
||||
ciborium = { version = "0.2", optional = true }
|
||||
|
||||
[features]
|
||||
# default = ["cbor"]
|
||||
default = ["json"]
|
||||
cbor = ["dep:ciborium"]
|
||||
json = ["dep:serde_json"]
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
[build]
|
||||
target = "index.html" # The index HTML file to drive the bundling process.
|
||||
html_output = "index.html" # The name of the output HTML file.
|
||||
release = true # Build in release mode.
|
||||
dist = "dist" # The output dir for all final assets.
|
||||
public_url = "/" # The public URL from which assets are to be served.
|
||||
filehash = true # Whether to include hash values in the output file names.
|
||||
inject_scripts = true # Whether to inject scripts (and module preloads) into the finalized output.
|
||||
offline = false # Run without network access
|
||||
frozen = false # Require Cargo.lock and cache are up to date
|
||||
locked = false # Require Cargo.lock is up to date
|
||||
minify = "always" # Control minification: can be one of: never, on_release, always
|
||||
no_sri = false # Allow disabling sub-resource integrity (SRI)
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -0,0 +1,27 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>website</title>
|
||||
<link rel="icon" href="/img/puffidle.webp" />
|
||||
<link data-trunk rel="sass" href="index.scss" />
|
||||
<link data-trunk rel="copy-dir" href="img">
|
||||
<link data-trunk rel="copy-dir" href="assets">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app></app>
|
||||
<clients>
|
||||
<dupe1></dupe1>
|
||||
<dupe2></dupe2>
|
||||
<dupe3></dupe3>
|
||||
<dupe4></dupe4>
|
||||
<dupe5></dupe5>
|
||||
<dupe6></dupe6>
|
||||
</clients>
|
||||
<error></error>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,19 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use yew::prelude::*;
|
||||
use yew_router::{
|
||||
BrowserRouter, Router, Switch,
|
||||
history::{AnyHistory, History, MemoryHistory},
|
||||
};
|
||||
|
||||
use crate::pages::*;
|
||||
|
||||
#[function_component]
|
||||
pub fn App() -> Html {
|
||||
html! {
|
||||
<main>
|
||||
// <BrowserRouter>
|
||||
// </BrowserRouter>
|
||||
</main>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
werewolves_macros::static_links!("werewolves/assets" relative to "werewolves/");
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
use core::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures::SinkExt;
|
||||
use yew::{html::Scope, prelude::*};
|
||||
|
||||
pub fn send_message<T: Clone + Debug + 'static, P>(
|
||||
msg: T,
|
||||
send: futures::channel::mpsc::Sender<T>,
|
||||
) -> Callback<P> {
|
||||
Callback::from(move |_| {
|
||||
let mut send = send.clone();
|
||||
let msg = msg.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
if let Err(err) = send.send(msg.clone()).await {
|
||||
log::error!("sending message <({msg:?})> in callback: {err}")
|
||||
};
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
pub fn mouse_event<F>(inner: F) -> Callback<MouseEvent>
|
||||
where
|
||||
F: Fn() + 'static,
|
||||
{
|
||||
Callback::from(move |_| (inner)())
|
||||
}
|
||||
|
||||
pub fn send_fn<T, P, F>(msg_fn: F, send: futures::channel::mpsc::Sender<T>) -> Callback<P>
|
||||
where
|
||||
T: Clone + Debug + 'static,
|
||||
F: Fn(P) -> T + 'static,
|
||||
P: 'static,
|
||||
{
|
||||
let msg_fn = Arc::new(msg_fn);
|
||||
Callback::from(move |param| {
|
||||
let mut send = send.clone();
|
||||
let msg_fn = msg_fn.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
if let Err(err) = send.send((msg_fn)(param)).await {
|
||||
log::error!("sending message in callback: {err}")
|
||||
};
|
||||
});
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,109 @@
|
|||
use core::ops::Not;
|
||||
|
||||
use werewolves_proto::{
|
||||
message::{
|
||||
PublicIdentity, Target,
|
||||
host::{HostGameMessage, HostMessage, HostNightMessage},
|
||||
night::{ActionPrompt, ActionResponse},
|
||||
},
|
||||
player::CharacterId,
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{
|
||||
Identity,
|
||||
action::{SingleTarget, WolvesIntro},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct ActionPromptProps {
|
||||
pub prompt: ActionPrompt,
|
||||
pub ident: PublicIdentity,
|
||||
#[prop_or_default]
|
||||
pub big_screen: bool,
|
||||
pub on_complete: Callback<HostMessage>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||
let ident = props
|
||||
.big_screen
|
||||
.not()
|
||||
.then(|| html! {<Identity ident={props.ident.clone()}/>});
|
||||
match &props.prompt {
|
||||
ActionPrompt::WolvesIntro { wolves } => {
|
||||
let on_complete = props.on_complete.clone();
|
||||
let on_complete = Callback::from(move |_| {
|
||||
on_complete.emit(HostMessage::InGame(
|
||||
werewolves_proto::message::host::HostGameMessage::Night(
|
||||
werewolves_proto::message::host::HostNightMessage::ActionResponse(
|
||||
werewolves_proto::message::night::ActionResponse::WolvesIntroAck,
|
||||
),
|
||||
),
|
||||
))
|
||||
});
|
||||
html! {
|
||||
<WolvesIntro big_screen={props.big_screen} on_complete={on_complete} wolves={wolves.clone()}/>
|
||||
}
|
||||
}
|
||||
ActionPrompt::Seer { living_players } => {
|
||||
let on_complete = props.on_complete.clone();
|
||||
let on_select = Callback::from(move |target: CharacterId| {
|
||||
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||
HostNightMessage::ActionResponse(ActionResponse::Seer(target)),
|
||||
)));
|
||||
});
|
||||
html! {
|
||||
<div>
|
||||
{ident}
|
||||
<SingleTarget
|
||||
targets={living_players.clone()}
|
||||
on_select={on_select}
|
||||
headline={"check alignment"}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionPrompt::RoleChange { new_role } => {
|
||||
let on_complete = props.on_complete.clone();
|
||||
let on_click = Callback::from(move |_| {
|
||||
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||
HostNightMessage::ActionResponse(ActionResponse::RoleChangeAck),
|
||||
)))
|
||||
});
|
||||
let cont = props.big_screen.not().then(|| {
|
||||
html! {
|
||||
<button onclick={on_click}>{"continue"}</button>
|
||||
}
|
||||
});
|
||||
html! {
|
||||
<div>
|
||||
{ident}
|
||||
<h2>{"your role has changed"}</h2>
|
||||
<p>{new_role.to_string()}</p>
|
||||
{cont}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionPrompt::Protector { targets } => todo!(),
|
||||
ActionPrompt::Arcanist { living_players } => todo!(),
|
||||
ActionPrompt::Gravedigger { dead_players } => todo!(),
|
||||
ActionPrompt::Hunter {
|
||||
current_target,
|
||||
living_players,
|
||||
} => todo!(),
|
||||
ActionPrompt::Militia { living_players } => todo!(),
|
||||
ActionPrompt::MapleWolf {
|
||||
kill_or_die,
|
||||
living_players,
|
||||
} => todo!(),
|
||||
ActionPrompt::Guardian {
|
||||
previous,
|
||||
living_players,
|
||||
} => todo!(),
|
||||
ActionPrompt::WolfPackKill { living_villagers } => todo!(),
|
||||
ActionPrompt::Shapeshifter => todo!(),
|
||||
ActionPrompt::AlphaWolf { living_villagers } => todo!(),
|
||||
ActionPrompt::DireWolf { living_players } => todo!(),
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
use core::ops::Not;
|
||||
|
||||
use werewolves_proto::{
|
||||
message::{
|
||||
PublicIdentity,
|
||||
host::{HostGameMessage, HostMessage, HostNightMessage},
|
||||
night::{ActionPrompt, ActionResponse, ActionResult},
|
||||
},
|
||||
role::Alignment,
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{CoverOfDarkness, Identity};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct ActionResultProps {
|
||||
pub result: ActionResult,
|
||||
pub ident: PublicIdentity,
|
||||
#[prop_or_default]
|
||||
pub big_screen: bool,
|
||||
pub on_complete: Callback<HostMessage>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
||||
let ident = props
|
||||
.big_screen
|
||||
.not()
|
||||
.then(|| html! {<Identity ident={props.ident.clone()}/>});
|
||||
let on_complete = props.on_complete.clone();
|
||||
let on_complete = Callback::from(move |_| {
|
||||
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||
HostNightMessage::Next,
|
||||
)))
|
||||
});
|
||||
let cont = props
|
||||
.big_screen
|
||||
.not()
|
||||
.then(|| html! {<button onclick={on_complete}>{"continue"}</button>});
|
||||
match &props.result {
|
||||
ActionResult::RoleBlocked => {
|
||||
html! {
|
||||
<div class="result">
|
||||
{ident}
|
||||
<h2>{"you were role blocked"}</h2>
|
||||
{cont}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionResult::Seer(alignment) => html! {
|
||||
<div class="result">
|
||||
{ident}
|
||||
<h2>{"the alignment was"}</h2>
|
||||
<p>{match alignment {
|
||||
Alignment::Village => "village",
|
||||
Alignment::Wolves => "wolfpack",
|
||||
}}</p>
|
||||
{cont}
|
||||
</div>
|
||||
},
|
||||
ActionResult::Arcanist { same } => todo!(),
|
||||
ActionResult::GraveDigger(role_title) => todo!(),
|
||||
ActionResult::WolvesMustBeUnanimous => todo!(),
|
||||
ActionResult::WaitForOthersToVote => todo!(),
|
||||
ActionResult::GoBackToSleep => {
|
||||
let next = props.big_screen.not().then(|| {
|
||||
let on_complete = props.on_complete.clone();
|
||||
move |_| {
|
||||
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||
HostNightMessage::Next,
|
||||
)))
|
||||
}
|
||||
});
|
||||
html! {
|
||||
<CoverOfDarkness message={"go to sleep"} next={next}>
|
||||
{"continue"}
|
||||
</CoverOfDarkness>
|
||||
}
|
||||
}
|
||||
ActionResult::RoleRevealDone => todo!(),
|
||||
ActionResult::WolvesIntroDone => {
|
||||
let on_complete = props.on_complete.clone();
|
||||
let next = props.big_screen.not().then(|| {
|
||||
Callback::from(move |_| {
|
||||
on_complete.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||
HostNightMessage::Next,
|
||||
)))
|
||||
})
|
||||
});
|
||||
|
||||
html! {
|
||||
<CoverOfDarkness message={"wolves go to sleep"} next={next}/>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
use core::ops::Not;
|
||||
|
||||
use werewolves_proto::{message::Target, player::CharacterId};
|
||||
use yew::{html::Scope, prelude::*};
|
||||
|
||||
use crate::components::Identity;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct SingleTargetProps {
|
||||
pub targets: Box<[Target]>,
|
||||
#[prop_or_default]
|
||||
pub headline: &'static str,
|
||||
#[prop_or_default]
|
||||
pub read_only: bool,
|
||||
pub on_select: Callback<CharacterId>,
|
||||
}
|
||||
|
||||
pub struct SingleTarget {
|
||||
selected: Option<CharacterId>,
|
||||
}
|
||||
|
||||
impl Component for SingleTarget {
|
||||
type Message = CharacterId;
|
||||
|
||||
type Properties = SingleTargetProps;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self { selected: None }
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let SingleTargetProps { read_only, headline, targets, on_select } = ctx.props();
|
||||
let scope = ctx.link().clone();
|
||||
let card_select = Callback::from(move |target| {
|
||||
scope.send_message(target);
|
||||
});
|
||||
let targets = targets.iter().map(|t| {
|
||||
html!{
|
||||
<TargetCard
|
||||
target={t.clone()}
|
||||
selected={self.selected.as_ref().map(|sel| sel == &t.character_id).unwrap_or_default()}
|
||||
on_select={card_select.clone()}
|
||||
/>
|
||||
}
|
||||
}).collect::<Html>();
|
||||
let headline = if headline.trim().is_empty() {
|
||||
html!()
|
||||
} else {
|
||||
html!(<h2>{headline}</h2>)
|
||||
};
|
||||
|
||||
let on_select = on_select.clone();
|
||||
let on_click = if let Some(target) = self.selected.clone() {
|
||||
Callback::from(move |_| on_select.emit(target.clone()))
|
||||
} else {
|
||||
Callback::from(|_| ())
|
||||
};
|
||||
|
||||
let submit = read_only.not().then(|| html!{
|
||||
<div class="button-container sp-ace">
|
||||
<button disabled={self.selected.is_none()} onclick={on_click}>{"submit"}</button>
|
||||
</div>
|
||||
});
|
||||
|
||||
html!{
|
||||
<div class="column-list">
|
||||
{headline}
|
||||
<div class="row-list">
|
||||
{targets}
|
||||
</div>
|
||||
{submit}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
if let Some(selected) = self.selected.as_ref() {
|
||||
if selected == &msg {
|
||||
self.selected = None;
|
||||
} else {
|
||||
self.selected = Some(msg);
|
||||
}
|
||||
} else {
|
||||
self.selected = Some(msg);
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct TargetCardProps {
|
||||
pub target: Target,
|
||||
pub selected: bool,
|
||||
pub on_select: Callback<CharacterId>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn TargetCard(props: &TargetCardProps) -> Html {
|
||||
let on_select = props.on_select.clone();
|
||||
let target = props.target.character_id.clone();
|
||||
let on_click = Callback::from(move |_| {
|
||||
on_select.emit(target.clone());
|
||||
});
|
||||
html! {
|
||||
<div
|
||||
class={classes!("target-card", "character", props.selected.then_some("selected"))}
|
||||
onclick={on_click}
|
||||
>
|
||||
<Identity ident={props.target.public.clone()} />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
use werewolves_proto::{message::Target, role::RoleTitle};
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct WolvesIntroProps {
|
||||
pub wolves: Box<[(Target, RoleTitle)]>,
|
||||
pub big_screen: bool,
|
||||
pub on_complete: Callback<()>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn WolvesIntro(props: &WolvesIntroProps) -> Html {
|
||||
let on_complete = props.on_complete.clone();
|
||||
let on_complete = Callback::from(move |_| on_complete.emit(()));
|
||||
html! {
|
||||
<div class="wolves-intro">
|
||||
<h2>{"these are the wolves:"}</h2>
|
||||
{
|
||||
if props.big_screen {
|
||||
html!()
|
||||
} else {
|
||||
html!{
|
||||
<button onclick={on_complete}>{"continue"}</button>
|
||||
}
|
||||
}
|
||||
}
|
||||
{
|
||||
props.wolves.iter().map(|w| html!{
|
||||
<div class={"character wolves"}>
|
||||
<h2 class="role">{w.1.to_string()}</h2>
|
||||
<p class="name">{w.0.public.name.clone()}</p>
|
||||
{
|
||||
w.0.public.pronouns.as_ref().map(|p| html!{
|
||||
<p class="pronouns">{"("}{p.as_str()}{")"}</p>
|
||||
}).unwrap_or(html!())
|
||||
}
|
||||
</div>
|
||||
}).collect::<Html>()
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct ButtonProperties {
|
||||
pub on_click: Callback<()>,
|
||||
#[prop_or_default]
|
||||
pub disabled_reason: Option<String>,
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Button(props: &ButtonProperties) -> Html {
|
||||
let on_click = props.on_click.clone();
|
||||
let on_click = Callback::from(move |_| on_click.emit(()));
|
||||
html! {
|
||||
<div class="button-container">
|
||||
<button
|
||||
class="default-button"
|
||||
disabled={props.disabled_reason.is_some()}
|
||||
reason={props.disabled_reason.clone()}
|
||||
onclick={on_click}
|
||||
>
|
||||
{props.children.clone()}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
use crate::components::Button;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct CoverOfDarknessProps {
|
||||
#[prop_or_default]
|
||||
pub next: Option<Callback<()>>,
|
||||
#[prop_or_else(|| String::from("night falls"))]
|
||||
pub message: String,
|
||||
#[prop_or_else(|| html!("begin"))]
|
||||
pub children: Html,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn CoverOfDarkness(
|
||||
CoverOfDarknessProps {
|
||||
message,
|
||||
next,
|
||||
children,
|
||||
}: &CoverOfDarknessProps,
|
||||
) -> Html {
|
||||
let next = next.as_ref().map(|next| {
|
||||
html! {
|
||||
<Button on_click={next}>{children.clone()}</Button>
|
||||
}
|
||||
});
|
||||
html! {
|
||||
<div class="cover-of-darkness">
|
||||
<p>{message}</p>
|
||||
{next}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
use core::ops::Not;
|
||||
|
||||
use werewolves_proto::{message::CharacterState, player::CharacterId};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Button, Identity};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct DaytimePlayerListProps {
|
||||
pub characters: Box<[CharacterState]>,
|
||||
pub marked: Box<[CharacterId]>,
|
||||
pub on_execute: Callback<()>,
|
||||
pub on_mark: Callback<CharacterId>,
|
||||
pub big_screen: bool,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn DaytimePlayerList(
|
||||
DaytimePlayerListProps {
|
||||
characters,
|
||||
on_execute,
|
||||
on_mark,
|
||||
marked,
|
||||
big_screen,
|
||||
}: &DaytimePlayerListProps,
|
||||
) -> Html {
|
||||
let on_select = big_screen.not().then(|| on_mark.clone());
|
||||
let chars = characters
|
||||
.iter()
|
||||
.map(|c| {
|
||||
html! {
|
||||
<DaytimePlayer
|
||||
character={c.clone()}
|
||||
on_the_block={marked.contains(&c.character_id)}
|
||||
on_select={on_select.clone()}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let button = big_screen.not().then(|| {
|
||||
html! {
|
||||
<Button
|
||||
on_click={on_execute}
|
||||
disabled_reason={
|
||||
marked.is_empty()
|
||||
.then_some(String::from("no one is on the block"))
|
||||
}
|
||||
>
|
||||
{"execute"}
|
||||
</Button>
|
||||
}
|
||||
});
|
||||
html! {
|
||||
<div class="column-list">
|
||||
<div class="row-list small baseline player-list gap">
|
||||
{chars}
|
||||
</div>
|
||||
{button}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct DaytimePlayerProps {
|
||||
pub character: CharacterState,
|
||||
pub on_the_block: bool,
|
||||
pub on_select: Option<Callback<CharacterId>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn DaytimePlayer(
|
||||
DaytimePlayerProps {
|
||||
on_select,
|
||||
on_the_block,
|
||||
character:
|
||||
CharacterState {
|
||||
player_id: _,
|
||||
character_id,
|
||||
public_identity,
|
||||
role: _,
|
||||
died_to,
|
||||
},
|
||||
}: &DaytimePlayerProps,
|
||||
) -> Html {
|
||||
let dead = died_to.is_some().then_some("dead");
|
||||
let button_text = if *on_the_block { "unmark" } else { "mark" };
|
||||
let on_the_block = on_the_block.then_some("on-the-block");
|
||||
let submenu = died_to.is_none().then_some(()).and_then(|_| {
|
||||
on_select.as_ref().map(|on_select| {
|
||||
let character_id = character_id.clone();
|
||||
let on_select = on_select.clone();
|
||||
let on_click = Callback::from(move |_| on_select.emit(character_id.clone()));
|
||||
html! {
|
||||
<nav class="submenu">
|
||||
<Button on_click={on_click}>{button_text}</Button>
|
||||
</nav>
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
html! {
|
||||
<div class={classes!("player", dead, on_the_block, "column-list", "ident")}>
|
||||
<Identity ident={public_identity.clone()}/>
|
||||
{submenu}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
use werewolves_proto::message::PublicIdentity;
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct IdentityProps {
|
||||
pub ident: PublicIdentity,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Identity(props: &IdentityProps) -> Html {
|
||||
let IdentityProps {
|
||||
ident:
|
||||
PublicIdentity {
|
||||
name,
|
||||
pronouns,
|
||||
number,
|
||||
},
|
||||
} = props;
|
||||
let pronouns = pronouns.as_ref().map(|p| {
|
||||
html! {
|
||||
<p class="pronouns">{"("}{p}{")"}</p>
|
||||
}
|
||||
});
|
||||
html! {
|
||||
<div class="identity">
|
||||
<p class="number"><b>{number.get()}</b></p>
|
||||
<p>{name}</p>
|
||||
{pronouns}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
use core::num::NonZeroU8;
|
||||
|
||||
use web_sys::{HtmlInputElement, HtmlSelectElement, wasm_bindgen::JsCast};
|
||||
use werewolves_proto::message::PublicIdentity;
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct InputProps {
|
||||
#[prop_or_default]
|
||||
pub initial_value: PublicIdentity,
|
||||
pub callback: Callback<PublicIdentity, ()>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn InputName(props: &InputProps) -> Html {
|
||||
let callback = props.callback.clone();
|
||||
let on_click = Callback::from(move |_| {
|
||||
let name = gloo::utils::document()
|
||||
.query_selector(".identity-input #name")
|
||||
.expect("cannot find name input")
|
||||
.and_then(|e| e.dyn_into::<HtmlInputElement>().ok())
|
||||
.expect("name input element not HtmlInputElement")
|
||||
.value()
|
||||
.trim()
|
||||
.to_string();
|
||||
let pronouns = match gloo::utils::document()
|
||||
.query_selector(".identity-input #pronouns")
|
||||
.expect("cannot find pronouns input")
|
||||
.and_then(|e| e.dyn_into::<HtmlInputElement>().ok())
|
||||
.expect("pronouns input element not HtmlInputElement")
|
||||
.value()
|
||||
.trim()
|
||||
{
|
||||
"" => None,
|
||||
p => Some(p.to_string()),
|
||||
};
|
||||
let number = gloo::utils::document()
|
||||
.query_selector(".identity-input #number")
|
||||
.expect("cannot find number input")
|
||||
.and_then(|e| e.dyn_into::<HtmlSelectElement>().ok())
|
||||
.expect("number input element not HtmlSelectElement")
|
||||
.value()
|
||||
.trim()
|
||||
.parse::<NonZeroU8>()
|
||||
.expect("parse number");
|
||||
if name.is_empty() {
|
||||
return;
|
||||
}
|
||||
callback.emit(PublicIdentity {
|
||||
name,
|
||||
pronouns,
|
||||
number,
|
||||
});
|
||||
});
|
||||
html! {
|
||||
<div class="identity-input">
|
||||
<label for="name">{"Name"}</label>
|
||||
<input name="name" id="name" type="text" value={props.initial_value.name.clone()}/>
|
||||
<label for="pronouns">{"Pronouns"}</label>
|
||||
<input name="pronouns" id="pronouns" type="text"
|
||||
value={props.initial_value.pronouns.clone().unwrap_or_default()}/>
|
||||
<label for="number">{"Number"}</label>
|
||||
<select name="number" id="number">
|
||||
{
|
||||
(1..=0xFFu8).into_iter().map(|i| html!{
|
||||
<option value={i.to_string()}>{i}</option>
|
||||
}).collect::<Html>()
|
||||
}
|
||||
</select>
|
||||
<button onclick={on_click}>{"Submit"}</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
use werewolves_proto::{message::PlayerState, player::PlayerId};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{LobbyPlayer, LobbyPlayerAction};
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct LobbyProps {
|
||||
pub players: Box<[PlayerState]>,
|
||||
#[prop_or_default]
|
||||
pub on_action: Option<Callback<(PlayerId, LobbyPlayerAction)>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Lobby(LobbyProps { players, on_action }: &LobbyProps) -> Html {
|
||||
let mut players = players.clone();
|
||||
players.sort_by_key(|f| f.identification.public.number.get());
|
||||
html! {
|
||||
<div class="column-list">
|
||||
<p style="text-align: center;">{format!("Players in lobby: {}", players.len())}</p>
|
||||
<div class="row-list small baseline player-list gap">
|
||||
{
|
||||
players
|
||||
.into_iter()
|
||||
.map(|p| html! {<LobbyPlayer on_action={on_action} player={p} />})
|
||||
.collect::<Html>()
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
use web_sys::{HtmlDivElement, HtmlElement};
|
||||
use werewolves_proto::{message::PlayerState, player::PlayerId};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Button, Identity};
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct LobbyPlayerProps {
|
||||
pub player: PlayerState,
|
||||
#[prop_or_default]
|
||||
pub on_action: Option<Callback<(PlayerId, LobbyPlayerAction)>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum LobbyPlayerAction {
|
||||
Kick,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn LobbyPlayer(LobbyPlayerProps { player, on_action }: &LobbyPlayerProps) -> Html {
|
||||
let class = if player.connected {
|
||||
"connected"
|
||||
} else {
|
||||
"disconnected"
|
||||
};
|
||||
let pid = player.identification.player_id.clone();
|
||||
let action = |action: LobbyPlayerAction| {
|
||||
let pid = pid.clone();
|
||||
if let Some(on_action) = on_action.as_ref() {
|
||||
let on_action = on_action.clone();
|
||||
Callback::from(move |_| on_action.emit((pid.clone(), action)))
|
||||
} else {
|
||||
Callback::noop()
|
||||
}
|
||||
};
|
||||
let submenu = on_action.is_some().then(|| {
|
||||
html! {
|
||||
<nav class="submenu">
|
||||
<Button on_click={(action)(LobbyPlayerAction::Kick)}>{"Kick"}</Button>
|
||||
</nav>
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<div class={classes!("player", class, "column-list", "ident")}>
|
||||
<Identity ident={player.identification.public.clone()}/>
|
||||
{submenu}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct NotificationProps {
|
||||
pub text: String,
|
||||
pub callback: Callback<()>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Notification(props: &NotificationProps) -> Html {
|
||||
let cb = props.callback.clone();
|
||||
let on_click = Callback::from(move |_| cb.clone().emit(()));
|
||||
html! {
|
||||
<stack>
|
||||
<h2>{props.text.clone()}</h2>
|
||||
<button class="confirm" onclick={on_click}>{"Ok"}</button>
|
||||
</stack>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
use werewolves_proto::message::Target;
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct RoleRevealProps {
|
||||
pub ackd: Box<[Target]>,
|
||||
pub waiting: Box<[Target]>,
|
||||
pub on_force_ready: Callback<Target>,
|
||||
}
|
||||
|
||||
pub struct RoleReveal {}
|
||||
|
||||
impl Component for RoleReveal {
|
||||
type Message = Target;
|
||||
|
||||
type Properties = RoleRevealProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let RoleRevealProps {
|
||||
ackd,
|
||||
waiting,
|
||||
on_force_ready,
|
||||
} = ctx.props();
|
||||
let on_force_ready = on_force_ready.clone();
|
||||
let cards = ackd
|
||||
.iter()
|
||||
.map(|t| (t, true))
|
||||
.chain(waiting.iter().map(|t| (t, false)))
|
||||
.map(|(t, ready)| {
|
||||
html! {
|
||||
<RoleRevealCard
|
||||
target={t.clone()}
|
||||
is_ready={ready}
|
||||
on_force_ready={on_force_ready.clone()}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
|
||||
let nack = waiting.clone();
|
||||
let on_force_all = Callback::from(move |_| {
|
||||
for t in nack.clone() {
|
||||
on_force_ready.emit(t);
|
||||
}
|
||||
});
|
||||
html! {
|
||||
<div class={"role-reveal-cards"}>
|
||||
<button onclick={on_force_all}>{"force ready all"}</button>
|
||||
{cards}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct RoleRevealCardProps {
|
||||
pub target: Target,
|
||||
pub is_ready: bool,
|
||||
pub on_force_ready: Callback<Target>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn RoleRevealCard(props: &RoleRevealCardProps) -> Html {
|
||||
let class = if props.is_ready { "ready" } else { "not-ready" };
|
||||
let target = props.target.clone();
|
||||
let on_force_ready = props.on_force_ready.clone();
|
||||
let on_click = Callback::from(move |_| {
|
||||
on_force_ready.emit(target.clone());
|
||||
});
|
||||
html! {
|
||||
<div class={classes!(class, "role-reveal-card")}>
|
||||
<p>{props.target.public.name.as_str()}</p>
|
||||
{
|
||||
if !props.is_ready {
|
||||
html! {<button onclick={on_click}>{"force ready"}</button>}
|
||||
} else {
|
||||
html!{}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
use convert_case::{Case, Casing};
|
||||
use web_sys::HtmlInputElement;
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
game::GameSettings,
|
||||
role::{Alignment, RoleTitle},
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
const ALIGN_VILLAGE: &str = "village";
|
||||
const ALIGN_WOLVES: &str = "wolves";
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
pub struct SettingsProps {
|
||||
pub settings: GameSettings,
|
||||
pub players_in_lobby: usize,
|
||||
pub on_update: Callback<GameSettings>,
|
||||
pub on_start: Callback<()>,
|
||||
#[prop_or_default]
|
||||
pub on_error: Option<Callback<GameError>>,
|
||||
}
|
||||
|
||||
fn get_role_count(settings: &GameSettings, role: RoleTitle) -> u8 {
|
||||
match role {
|
||||
RoleTitle::Villager => {
|
||||
panic!("villager should not be in the settings page")
|
||||
}
|
||||
_ => settings
|
||||
.roles()
|
||||
.into_iter()
|
||||
.find_map(|(r, cnt)| (r == role).then_some(cnt.get()))
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
enum AmountChange {
|
||||
Increment(RoleTitle),
|
||||
Decrement(RoleTitle),
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Settings(props: &SettingsProps) -> Html {
|
||||
let on_update = props.on_update.clone();
|
||||
let (start_game_disabled, reason) = match props.settings.check() {
|
||||
Ok(_) => {
|
||||
if props.players_in_lobby < props.settings.min_players_needed() {
|
||||
(true, String::from("too few players for role setup"))
|
||||
} else {
|
||||
(false, String::new())
|
||||
}
|
||||
}
|
||||
Err(err) => (true, err.to_string()),
|
||||
};
|
||||
let settings = props.settings.clone();
|
||||
let on_error = props.on_error.clone();
|
||||
let on_changed = Callback::from(move |change: AmountChange| {
|
||||
let mut s = settings.clone();
|
||||
match change {
|
||||
AmountChange::Increment(role) => {
|
||||
if let Err(err) = s.add(role)
|
||||
&& let Some(on_error) = on_error.as_ref()
|
||||
{
|
||||
on_error.emit(err);
|
||||
}
|
||||
}
|
||||
AmountChange::Decrement(role) => s.sub(role),
|
||||
}
|
||||
if s != settings {
|
||||
on_update.emit(s)
|
||||
}
|
||||
});
|
||||
|
||||
let on_start = props.on_start.clone();
|
||||
let on_start_game = Callback::from(move |_| on_start.emit(()));
|
||||
|
||||
let roles = RoleTitle::ALL
|
||||
.into_iter()
|
||||
.filter(|r| !matches!(r, RoleTitle::Scapegoat | RoleTitle::Villager))
|
||||
.map(|r| html! {<RoleCard role={r} amount={get_role_count(&props.settings, r)} on_changed={on_changed.clone()}/>})
|
||||
.collect::<Html>();
|
||||
html! {
|
||||
<div class="settings">
|
||||
<h2>{format!("Min players for settings: {}", props.settings.min_players_needed())}</h2>
|
||||
<div class="role-list">
|
||||
// <BoolRoleCard role={RoleTitle::Scapegoat} enabled={props.settings.scapegoat} on_changed={on_bool_changed}/>
|
||||
{roles}
|
||||
</div>
|
||||
<button reason={reason} disabled={start_game_disabled} class="start-game" onclick={on_start_game}>{"Start Game"}</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
enum BoolRoleSet {
|
||||
Scapegoat { enabled: bool },
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
struct BoolRoleProps {
|
||||
pub role: RoleTitle,
|
||||
pub enabled: bool,
|
||||
pub on_changed: Callback<BoolRoleSet>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn BoolRoleCard(props: &BoolRoleProps) -> Html {
|
||||
let align_class = if props.role.wolf() {
|
||||
ALIGN_WOLVES
|
||||
} else {
|
||||
ALIGN_VILLAGE
|
||||
};
|
||||
|
||||
let set_role = match props.role {
|
||||
RoleTitle::Scapegoat => |enabled| BoolRoleSet::Scapegoat { enabled },
|
||||
_ => panic!("invalid role for bool card: {}", props.role),
|
||||
};
|
||||
log::warn!("Role: {} | {};", props.role, props.enabled);
|
||||
let enabled = props.enabled;
|
||||
let role = props.role;
|
||||
|
||||
let cb = props.on_changed.clone();
|
||||
let on_click = Callback::from(move |ev: MouseEvent| {
|
||||
let input = ev
|
||||
.target_dyn_into::<HtmlInputElement>()
|
||||
.expect("input callback not on input");
|
||||
cb.emit(set_role(input.checked()))
|
||||
});
|
||||
html! {
|
||||
<div class={classes!("role-card", align_class)}>
|
||||
// <div>
|
||||
<bool_spacer/>
|
||||
<bool_role>
|
||||
<role>{role.to_string()}</role>
|
||||
<input onclick={on_click} type="checkbox" checked={enabled}/>
|
||||
</bool_role>
|
||||
// </div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
struct RoleProps {
|
||||
pub role: RoleTitle,
|
||||
pub amount: u8,
|
||||
pub on_changed: Callback<AmountChange>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn RoleCard(props: &RoleProps) -> Html {
|
||||
let align_class = if props.role.wolf() {
|
||||
ALIGN_WOLVES
|
||||
} else {
|
||||
ALIGN_VILLAGE
|
||||
};
|
||||
let amount = props.amount;
|
||||
let role = props.role;
|
||||
let role_name = role.to_string().to_case(Case::Title);
|
||||
|
||||
let cb = props.on_changed.clone();
|
||||
let decrease = Callback::from(move |_| cb.emit(AmountChange::Decrement(role)));
|
||||
let cb = props.on_changed.clone();
|
||||
let increase = Callback::from(move |_| cb.emit(AmountChange::Increment(role)));
|
||||
html! {
|
||||
<div class={classes!("role-card", align_class)}>
|
||||
<button onclick={decrease}>{"-"}</button>
|
||||
<rolecard><role>{role_name}</role><count>{amount.to_string()}</count></rolecard>
|
||||
<button onclick={increase}>{"+"}</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
mod assets;
|
||||
mod storage;
|
||||
mod components {
|
||||
werewolves_macros::include_path!("werewolves/src/components");
|
||||
pub mod host;
|
||||
pub mod action {
|
||||
werewolves_macros::include_path!("werewolves/src/components/action");
|
||||
}
|
||||
}
|
||||
mod pages {
|
||||
werewolves_macros::include_path!("werewolves/src/pages");
|
||||
}
|
||||
mod callback;
|
||||
use core::num::NonZeroU8;
|
||||
|
||||
use pages::{Client, ErrorComponent, Host, WerewolfError};
|
||||
use web_sys::{Element, HtmlElement, Url, wasm_bindgen::JsCast};
|
||||
use werewolves_proto::{
|
||||
message::{Identification, PublicIdentity},
|
||||
player::PlayerId,
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::pages::ClientProps;
|
||||
|
||||
fn main() {
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||
let document = gloo::utils::document();
|
||||
let url = document.document_uri().expect("get uri");
|
||||
let url_obj = Url::new(&url).unwrap();
|
||||
let path = url_obj.pathname();
|
||||
log::warn!("path: {path}");
|
||||
let app_element = document.query_selector("app").unwrap().unwrap();
|
||||
let error_element = document.query_selector("error").unwrap().unwrap();
|
||||
let ec = yew::Renderer::<ErrorComponent>::with_root(error_element).render();
|
||||
let cb_clone = ec.clone();
|
||||
let error_callback =
|
||||
Callback::from(move |err: Option<WerewolfError>| cb_clone.send_message(err));
|
||||
|
||||
if path.starts_with("/host") {
|
||||
let host = yew::Renderer::<Host>::with_root(app_element).render();
|
||||
host.send_message(pages::HostEvent::SetErrorCallback(error_callback));
|
||||
if path.starts_with("/host/big") {
|
||||
host.send_message(pages::HostEvent::SetBigScreenState(true));
|
||||
}
|
||||
} else if path.starts_with("/many-client") {
|
||||
let mut number = 1..=0xFFu8;
|
||||
for (player_id, name, dupe) in [
|
||||
(
|
||||
PlayerId::from_u128(1),
|
||||
"player 1",
|
||||
document.query_selector("app").unwrap().unwrap(),
|
||||
),
|
||||
(
|
||||
PlayerId::from_u128(2),
|
||||
"player 2",
|
||||
document.query_selector("dupe1").unwrap().unwrap(),
|
||||
),
|
||||
(
|
||||
PlayerId::from_u128(3),
|
||||
"player 3",
|
||||
document.query_selector("dupe2").unwrap().unwrap(),
|
||||
),
|
||||
(
|
||||
PlayerId::from_u128(4),
|
||||
"player 4",
|
||||
document.query_selector("dupe3").unwrap().unwrap(),
|
||||
),
|
||||
(
|
||||
PlayerId::from_u128(5),
|
||||
"player 5",
|
||||
document.query_selector("dupe4").unwrap().unwrap(),
|
||||
),
|
||||
(
|
||||
PlayerId::from_u128(6),
|
||||
"player 6",
|
||||
document.query_selector("dupe5").unwrap().unwrap(),
|
||||
),
|
||||
(
|
||||
PlayerId::from_u128(7),
|
||||
"player 7",
|
||||
document.query_selector("dupe6").unwrap().unwrap(),
|
||||
),
|
||||
] {
|
||||
let client =
|
||||
yew::Renderer::<Client>::with_root_and_props(dupe, ClientProps { auto_join: true })
|
||||
.render();
|
||||
client.send_message(pages::Message::ForceIdentity(Identification {
|
||||
player_id,
|
||||
public: PublicIdentity {
|
||||
name: name.to_string(),
|
||||
pronouns: Some(String::from("he/him")),
|
||||
number: NonZeroU8::new(number.next().unwrap()).unwrap(),
|
||||
},
|
||||
}));
|
||||
client.send_message(pages::Message::SetErrorCallback(error_callback.clone()));
|
||||
}
|
||||
} else {
|
||||
let client = yew::Renderer::<Client>::with_root_and_props(
|
||||
app_element,
|
||||
ClientProps { auto_join: false },
|
||||
)
|
||||
.render();
|
||||
client.send_message(pages::Message::SetErrorCallback(error_callback));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,534 @@
|
|||
use core::{sync::atomic::AtomicBool, time::Duration};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use futures::{
|
||||
SinkExt, StreamExt,
|
||||
channel::mpsc::{Receiver, Sender},
|
||||
};
|
||||
use gloo::{
|
||||
net::websocket::{self, futures::WebSocket},
|
||||
storage::{LocalStorage, Storage, errors::StorageError},
|
||||
};
|
||||
use serde::Serialize;
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
game::GameOver,
|
||||
message::{
|
||||
ClientMessage, DayCharacter, Identification, PlayerUpdate, PublicIdentity, ServerMessage,
|
||||
Target,
|
||||
night::{ActionPrompt, ActionResponse, ActionResult},
|
||||
},
|
||||
player::{Character, CharacterId, PlayerId},
|
||||
role::RoleTitle,
|
||||
};
|
||||
use yew::{html::Scope, prelude::*};
|
||||
|
||||
use crate::{
|
||||
components::{InputName, Notification},
|
||||
storage::StorageKey,
|
||||
};
|
||||
|
||||
use super::WerewolfError;
|
||||
|
||||
const DEBUG_URL: &str = "ws://192.168.1.162:8080/connect/client";
|
||||
const LIVE_URL: &str = "wss://wolf.emilis.dev/connect/client";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
SetErrorCallback(Callback<Option<WerewolfError>>),
|
||||
SetPublicIdentity(PublicIdentity),
|
||||
RecvServerMessage(ServerMessage),
|
||||
Connect,
|
||||
ForceIdentity(Identification),
|
||||
}
|
||||
|
||||
pub struct Connection {
|
||||
scope: Scope<Client>,
|
||||
ident: Identification,
|
||||
recv: Receiver<ClientMessage>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
async fn connect_ws() -> WebSocket {
|
||||
let url = option_env!("DEBUG").map(|_| DEBUG_URL).unwrap_or(LIVE_URL);
|
||||
loop {
|
||||
match WebSocket::open(url) {
|
||||
Ok(ws) => break ws,
|
||||
Err(err) => {
|
||||
log::error!("connect: {err}");
|
||||
yew::platform::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_message(msg: &impl Serialize) -> websocket::Message {
|
||||
#[cfg(feature = "json")]
|
||||
{
|
||||
websocket::Message::Text(serde_json::to_string(msg).expect("message serialization"))
|
||||
}
|
||||
#[cfg(feature = "cbor")]
|
||||
{
|
||||
websocket::Message::Bytes({
|
||||
let mut v = Vec::new();
|
||||
ciborium::into_writer(msg, &mut v).expect("serializing message");
|
||||
v
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(mut self) {
|
||||
let url = option_env!("DEBUG").map(|_| DEBUG_URL).unwrap_or(LIVE_URL);
|
||||
'outer: loop {
|
||||
log::info!("connecting to {url}");
|
||||
let mut ws = Self::connect_ws().await.fuse();
|
||||
log::info!("connected to {url}");
|
||||
|
||||
if let Err(err) = ws.send(Self::encode_message(&self.ident)).await {
|
||||
log::error!("websocket identification send: {err}");
|
||||
continue 'outer;
|
||||
};
|
||||
|
||||
if let Err(err) = ws
|
||||
.send(Self::encode_message(&ClientMessage::GetState))
|
||||
.await
|
||||
{
|
||||
log::error!("websocket identification send: {err}");
|
||||
continue 'outer;
|
||||
};
|
||||
|
||||
loop {
|
||||
let msg = futures::select! {
|
||||
r = ws.next() => {
|
||||
match r {
|
||||
Some(Ok(msg)) => msg,
|
||||
Some(Err(err)) => {
|
||||
log::error!("websocket recv: {err}");
|
||||
continue 'outer;
|
||||
},
|
||||
None => {
|
||||
log::warn!("websocket closed");
|
||||
continue 'outer;
|
||||
},
|
||||
}
|
||||
}
|
||||
r = self.recv.next() => {
|
||||
match r {
|
||||
Some(msg) => {
|
||||
log::info!("sending message: {msg:?}");
|
||||
if let Err(err) = ws.send(
|
||||
Self::encode_message(&msg)
|
||||
).await {
|
||||
log::error!("websocket send error: {err}");
|
||||
continue 'outer;
|
||||
}
|
||||
continue;
|
||||
},
|
||||
None => {
|
||||
log::info!("recv channel closed");
|
||||
return;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
let parse = {
|
||||
#[cfg(feature = "json")]
|
||||
{
|
||||
match msg {
|
||||
websocket::Message::Text(text) => {
|
||||
serde_json::from_str::<ServerMessage>(&text)
|
||||
}
|
||||
websocket::Message::Bytes(items) => serde_json::from_slice(&items),
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "cbor")]
|
||||
{
|
||||
match msg {
|
||||
websocket::Message::Text(_) => {
|
||||
log::error!("text messages not supported in cbor mode; discarding");
|
||||
continue;
|
||||
}
|
||||
websocket::Message::Bytes(bytes) => {
|
||||
ciborium::from_reader::<ServerMessage, _>(bytes.as_slice())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
match parse {
|
||||
Ok(msg) => self.scope.send_message(Message::RecvServerMessage(msg)),
|
||||
Err(err) => {
|
||||
log::error!("parsing server message: {err}; ignoring.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum ClientEvent {
|
||||
Disconnected,
|
||||
Notification(String),
|
||||
Waiting,
|
||||
ShowRole(RoleTitle),
|
||||
NotInLobby(Box<[PublicIdentity]>),
|
||||
InLobby(Box<[PublicIdentity]>),
|
||||
GameInProgress,
|
||||
GameOver(GameOver),
|
||||
}
|
||||
|
||||
impl TryFrom<ServerMessage> for ClientEvent {
|
||||
type Error = ServerMessage;
|
||||
|
||||
fn try_from(msg: ServerMessage) -> Result<Self, Self::Error> {
|
||||
Ok(match msg {
|
||||
ServerMessage::Disconnect => Self::Disconnected,
|
||||
ServerMessage::LobbyInfo {
|
||||
joined: false,
|
||||
players,
|
||||
} => Self::NotInLobby(players),
|
||||
ServerMessage::LobbyInfo {
|
||||
joined: true,
|
||||
players,
|
||||
} => Self::InLobby(players),
|
||||
ServerMessage::GameInProgress => Self::GameInProgress,
|
||||
_ => return Err(msg),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
player: Option<Identification>,
|
||||
send: Sender<ClientMessage>,
|
||||
recv: Option<Receiver<ClientMessage>>,
|
||||
current_event: Option<ClientEvent>,
|
||||
auto_join: bool,
|
||||
|
||||
error_callback: Callback<Option<WerewolfError>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
fn error(&self, err: WerewolfError) {
|
||||
self.error_callback.emit(Some(err))
|
||||
}
|
||||
|
||||
fn clear_error(&self) {
|
||||
self.error_callback.emit(None)
|
||||
}
|
||||
|
||||
fn bug(&self, msg: &str) {
|
||||
log::warn!("BUG: {msg}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Copy, Properties)]
|
||||
pub struct ClientProps {
|
||||
#[prop_or_default]
|
||||
pub auto_join: bool,
|
||||
}
|
||||
|
||||
impl Component for Client {
|
||||
type Message = Message;
|
||||
|
||||
type Properties = ClientProps;
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
gloo::utils::document().set_title("Werewolves Player");
|
||||
let player = StorageKey::PlayerId
|
||||
.get()
|
||||
.ok()
|
||||
.and_then(|p| StorageKey::PublicIdentity.get().ok().map(|n| (p, n)))
|
||||
.map(|(player_id, public)| Identification { player_id, public });
|
||||
|
||||
let (send, recv) = futures::channel::mpsc::channel::<ClientMessage>(100);
|
||||
|
||||
Self {
|
||||
player,
|
||||
send,
|
||||
recv: Some(recv),
|
||||
auto_join: ctx.props().auto_join,
|
||||
error_callback: Callback::from(|err| {
|
||||
if let Some(err) = err {
|
||||
log::error!("{err}")
|
||||
}
|
||||
}),
|
||||
current_event: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
if self.player.is_none() {
|
||||
let scope = ctx.link().clone();
|
||||
let callback = Callback::from(move |public: PublicIdentity| {
|
||||
scope.send_message(Message::SetPublicIdentity(public));
|
||||
});
|
||||
return html! {
|
||||
<InputName callback={callback}/>
|
||||
};
|
||||
} else if self.recv.is_some() {
|
||||
// Player info loaded, but connection isn't started
|
||||
ctx.link().send_message(Message::Connect);
|
||||
return html! {};
|
||||
}
|
||||
|
||||
let msg = match self.current_event.as_ref() {
|
||||
Some(msg) => msg,
|
||||
None => {
|
||||
return html! {
|
||||
<div class="connecting">
|
||||
<p>{"Connecting..."}</p>
|
||||
</div>
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let content = match msg {
|
||||
ClientEvent::Disconnected => html! {
|
||||
<div class="disconnected">
|
||||
<p>{"You were disconnected"}</p>
|
||||
</div>
|
||||
},
|
||||
ClientEvent::Waiting => {
|
||||
html! {
|
||||
<div class="waiting-lobby">
|
||||
<p>{"Waiting"}</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ClientEvent::ShowRole(role_title) => {
|
||||
let send = self.send.clone();
|
||||
let on_click =
|
||||
Callback::from(
|
||||
move |_| {
|
||||
while send.clone().try_send(ClientMessage::RoleAck).is_err() {}
|
||||
},
|
||||
);
|
||||
html! {
|
||||
<div class="game-start-role">
|
||||
<p>{format!("Your role: {role_title}")}</p>
|
||||
<button onclick={on_click}>{"got it"}</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ClientEvent::NotInLobby(players) => {
|
||||
let send = self.send.clone();
|
||||
let on_click =
|
||||
Callback::from(
|
||||
move |_| {
|
||||
while send.clone().try_send(ClientMessage::Hello).is_err() {}
|
||||
},
|
||||
);
|
||||
html! {
|
||||
<div class="lobby">
|
||||
<p>{format!("Players in lobby: {}", players.len())}</p>
|
||||
<ul class="players">
|
||||
{players.iter().map(|p| html!{<p>{p.to_string()}</p>}).collect::<Html>()}
|
||||
</ul>
|
||||
<button onclick={on_click}>{"Join"}</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ClientEvent::InLobby(players) => {
|
||||
let send = self.send.clone();
|
||||
let on_click =
|
||||
Callback::from(
|
||||
move |_| {
|
||||
while send.clone().try_send(ClientMessage::Goodbye).is_err() {}
|
||||
},
|
||||
);
|
||||
html! {
|
||||
<div class="lobby">
|
||||
<p>{format!("Players in lobby: {}", players.len())}</p>
|
||||
<ul class="players">
|
||||
{players.iter().map(|p| html!{<p>{p.to_string()}</p>}).collect::<Html>()}
|
||||
</ul>
|
||||
<button onclick={on_click}>{"Leave"}</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ClientEvent::GameInProgress => html! {
|
||||
<div class="waiting-lobby">
|
||||
<p>{"game in progress"}</p>
|
||||
</div>
|
||||
},
|
||||
ClientEvent::GameOver(result) => html! {
|
||||
<div>
|
||||
<h2>{"game over"}</h2>
|
||||
<p>{
|
||||
match result {
|
||||
GameOver::VillageWins => "village wins",
|
||||
GameOver::WolvesWin => "wolves win",
|
||||
}}</p>
|
||||
</div>
|
||||
},
|
||||
ClientEvent::Notification(notification) => {
|
||||
let scope = ctx.link().clone();
|
||||
let next_event =
|
||||
// Callback::from(move |_| scope.clone().send_message(Message::));
|
||||
Callback::from(move |_| log::info!("nothing"));
|
||||
html! {
|
||||
<Notification text={notification.clone()} callback={next_event}/>
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let player = self
|
||||
.player
|
||||
.as_ref()
|
||||
.map(|player| {
|
||||
let pronouns = if let Some(pronouns) = player.public.pronouns.as_ref() {
|
||||
html! {
|
||||
<p class={"pronouns"}>{"("}{pronouns.as_str()}{")"}</p>
|
||||
}
|
||||
} else {
|
||||
html!()
|
||||
};
|
||||
html! {
|
||||
<player>
|
||||
<p>{player.public.number.get()}</p>
|
||||
<name>{player.public.name.clone()}</name>
|
||||
{pronouns}
|
||||
</player>
|
||||
}
|
||||
})
|
||||
.unwrap_or(html!());
|
||||
|
||||
html! {
|
||||
<client>
|
||||
{player}
|
||||
{content}
|
||||
</client>
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
log::info!("update: {msg:?}");
|
||||
match msg {
|
||||
Message::ForceIdentity(id) => {
|
||||
self.player.replace(id);
|
||||
true
|
||||
}
|
||||
Message::SetErrorCallback(cb) => {
|
||||
self.error_callback = cb;
|
||||
false
|
||||
}
|
||||
Message::SetPublicIdentity(public) => {
|
||||
match self.player.as_mut() {
|
||||
Some(p) => p.public = public,
|
||||
None => {
|
||||
let res =
|
||||
StorageKey::PlayerId
|
||||
.get_or_set(PlayerId::new)
|
||||
.and_then(|player_id| {
|
||||
StorageKey::PublicIdentity
|
||||
.set(public.clone())
|
||||
.map(|_| Identification { player_id, public })
|
||||
});
|
||||
match res {
|
||||
Ok(ident) => {
|
||||
self.player = Some(ident.clone());
|
||||
if let Some(recv) = self.recv.take() {
|
||||
yew::platform::spawn_local(
|
||||
Connection {
|
||||
scope: ctx.link().clone(),
|
||||
ident,
|
||||
recv,
|
||||
}
|
||||
.run(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
self.error(err.into());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
Message::RecvServerMessage(msg) => {
|
||||
if let ServerMessage::LobbyInfo {
|
||||
joined: false,
|
||||
players: _,
|
||||
} = &msg
|
||||
{
|
||||
if self.auto_join {
|
||||
let mut send = self.send.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
if let Err(err) = send.send(ClientMessage::Hello).await {
|
||||
log::error!("send: {err}");
|
||||
}
|
||||
});
|
||||
self.auto_join = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let msg = match msg.try_into() {
|
||||
Ok(event) => {
|
||||
self.current_event.replace(event);
|
||||
return true;
|
||||
}
|
||||
Err(msg) => msg,
|
||||
};
|
||||
match msg {
|
||||
ServerMessage::GameStart { role } => {
|
||||
self.current_event.replace(ClientEvent::ShowRole(role));
|
||||
}
|
||||
ServerMessage::InvalidMessageForGameState => self.error(
|
||||
WerewolfError::GameError(GameError::InvalidMessageForGameState),
|
||||
),
|
||||
ServerMessage::NoSuchTarget => {
|
||||
self.error(WerewolfError::InvalidTarget);
|
||||
}
|
||||
ServerMessage::GameInProgress
|
||||
| ServerMessage::LobbyInfo {
|
||||
joined: _,
|
||||
players: _,
|
||||
}
|
||||
| ServerMessage::Disconnect => return false,
|
||||
ServerMessage::GameOver(game_over) => {
|
||||
self.current_event = Some(ClientEvent::GameOver(game_over));
|
||||
}
|
||||
ServerMessage::Reset => {
|
||||
let mut send = self.send.clone();
|
||||
self.current_event = Some(ClientEvent::Disconnected);
|
||||
yew::platform::spawn_local(async move {
|
||||
if let Err(err) = send.send(ClientMessage::GetState).await {
|
||||
log::error!("send: {err}");
|
||||
}
|
||||
});
|
||||
}
|
||||
ServerMessage::Sleep => self.current_event = Some(ClientEvent::Waiting),
|
||||
ServerMessage::Update(update) => match (update, self.player.as_mut()) {
|
||||
(PlayerUpdate::Number(num), Some(player)) => {
|
||||
player.public.number = num;
|
||||
return true;
|
||||
}
|
||||
(_, None) => return false,
|
||||
},
|
||||
};
|
||||
true
|
||||
}
|
||||
Message::Connect => {
|
||||
if let Some(player) = self.player.as_ref() {
|
||||
if let Some(recv) = self.recv.take() {
|
||||
yew::platform::spawn_local(
|
||||
Connection {
|
||||
scope: ctx.link().clone(),
|
||||
ident: player.clone(),
|
||||
recv,
|
||||
}
|
||||
.run(),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
while let Err(err) = self.send.try_send(ClientMessage::GetState) {
|
||||
log::error!("send IsThereALobby: {err}")
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
use gloo::storage::errors::StorageError;
|
||||
use thiserror::Error;
|
||||
use werewolves_proto::error::GameError;
|
||||
use yew::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Error)]
|
||||
pub enum WerewolfError {
|
||||
#[error("{0}")]
|
||||
GameError(#[from] GameError),
|
||||
#[error("local storage error: {0}")]
|
||||
LocalStorageError(String),
|
||||
#[error("invalid target")]
|
||||
InvalidTarget,
|
||||
#[error("send error: {0}")]
|
||||
SendError(#[from] futures::channel::mpsc::SendError),
|
||||
}
|
||||
|
||||
impl From<StorageError> for WerewolfError {
|
||||
fn from(storage_error: StorageError) -> Self {
|
||||
Self::LocalStorageError(storage_error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ErrorComponent {
|
||||
error: Option<WerewolfError>,
|
||||
}
|
||||
|
||||
impl Component for ErrorComponent {
|
||||
type Message = Option<WerewolfError>;
|
||||
|
||||
type Properties = ();
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
Self { error: None }
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let scope = ctx.link().clone();
|
||||
let clear = Callback::from(move |_| scope.send_message(None));
|
||||
match &self.error {
|
||||
Some(err) => html! {
|
||||
<div class="error-container">
|
||||
<div class="error-message">
|
||||
<p>{err.to_string()}</p>
|
||||
<button onclick={clear}>{"✖"}</button>
|
||||
</div>
|
||||
</div>
|
||||
},
|
||||
None => html!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
// if msg == self.error {
|
||||
// return false;
|
||||
// }
|
||||
self.error = msg;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,566 @@
|
|||
use core::{num::NonZeroU8, ops::Not, time::Duration};
|
||||
|
||||
use futures::{
|
||||
SinkExt, StreamExt,
|
||||
channel::mpsc::{Receiver, Sender},
|
||||
};
|
||||
use gloo::net::websocket::{self, futures::WebSocket};
|
||||
use serde::Serialize;
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
game::{GameOver, GameSettings},
|
||||
message::{
|
||||
CharacterState, Identification, PlayerState, PublicIdentity, Target,
|
||||
host::{
|
||||
HostDayMessage, HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage,
|
||||
ServerToHostMessage,
|
||||
},
|
||||
night::{ActionPrompt, ActionResult},
|
||||
},
|
||||
player::{CharacterId, PlayerId},
|
||||
};
|
||||
use yew::{html::Scope, prelude::*};
|
||||
|
||||
use crate::{
|
||||
callback,
|
||||
components::{
|
||||
Button, CoverOfDarkness, Lobby, LobbyPlayerAction, RoleReveal, Settings,
|
||||
action::{ActionResultView, Prompt},
|
||||
host::DaytimePlayerList,
|
||||
},
|
||||
};
|
||||
|
||||
use super::WerewolfError;
|
||||
|
||||
const DEBUG_URL: &str = "ws://192.168.1.162:8080/connect/host";
|
||||
const LIVE_URL: &str = "wss://wolf.emilis.dev/connect/host";
|
||||
|
||||
async fn connect_ws() -> WebSocket {
|
||||
let url = option_env!("DEBUG").map(|_| DEBUG_URL).unwrap_or(LIVE_URL);
|
||||
loop {
|
||||
match WebSocket::open(url) {
|
||||
Ok(ws) => break ws,
|
||||
Err(err) => {
|
||||
log::error!("connect: {err}");
|
||||
yew::platform::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_message(msg: &impl Serialize) -> websocket::Message {
|
||||
#[cfg(feature = "json")]
|
||||
{
|
||||
websocket::Message::Text(serde_json::to_string(msg).expect("message serialization"))
|
||||
}
|
||||
#[cfg(feature = "cbor")]
|
||||
{
|
||||
websocket::Message::Bytes({
|
||||
let mut v = Vec::new();
|
||||
ciborium::into_writer(msg, &mut v).expect("serializing message");
|
||||
v
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn worker(mut recv: Receiver<HostMessage>, scope: Scope<Host>) {
|
||||
let url = option_env!("DEBUG").map(|_| DEBUG_URL).unwrap_or(LIVE_URL);
|
||||
'outer: loop {
|
||||
log::info!("connecting to {url}");
|
||||
let mut ws = connect_ws().await.fuse();
|
||||
log::info!("connected to {url}");
|
||||
|
||||
if let Err(err) = ws.send(encode_message(&HostMessage::GetState)).await {
|
||||
log::error!("sending request for player list: {err}");
|
||||
continue 'outer;
|
||||
}
|
||||
|
||||
let mut last_msg = chrono::Local::now();
|
||||
loop {
|
||||
let msg = futures::select! {
|
||||
r = ws.next() => {
|
||||
match r {
|
||||
Some(Ok(msg)) => {
|
||||
last_msg = chrono::Local::now();
|
||||
msg
|
||||
},
|
||||
Some(Err(err)) => {
|
||||
log::error!("websocket recv: {err}");
|
||||
continue 'outer;
|
||||
},
|
||||
None => {
|
||||
log::warn!("websocket closed");
|
||||
continue 'outer;
|
||||
},
|
||||
}
|
||||
}
|
||||
r = recv.next() => {
|
||||
match r {
|
||||
Some(msg) => {
|
||||
log::info!("sending message: {msg:?}");
|
||||
if let Err(err) = ws.send(
|
||||
encode_message(&msg)
|
||||
).await {
|
||||
log::error!("websocket send error: {err}");
|
||||
continue 'outer;
|
||||
}
|
||||
continue;
|
||||
},
|
||||
None => {
|
||||
log::info!("recv channel closed");
|
||||
return;
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let parse = {
|
||||
#[cfg(feature = "json")]
|
||||
{
|
||||
match msg {
|
||||
websocket::Message::Text(text) => {
|
||||
serde_json::from_str::<ServerToHostMessage>(&text)
|
||||
}
|
||||
websocket::Message::Bytes(items) => serde_json::from_slice(&items),
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "cbor")]
|
||||
{
|
||||
match msg {
|
||||
websocket::Message::Text(_) => {
|
||||
log::error!("text messages not supported in cbor mode; discarding");
|
||||
continue;
|
||||
}
|
||||
websocket::Message::Bytes(bytes) => {
|
||||
ciborium::from_reader::<ServerToHostMessage, _>(bytes.as_slice())
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
let took = chrono::Local::now() - last_msg;
|
||||
if took.num_milliseconds() >= 100 {
|
||||
log::warn!("took {took}")
|
||||
}
|
||||
match parse {
|
||||
Ok(msg) => scope.send_message::<HostEvent>(msg.into()),
|
||||
Err(err) => {
|
||||
log::error!("parsing server message: {err}; ignoring.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum HostEvent {
|
||||
SetErrorCallback(Callback<Option<WerewolfError>>),
|
||||
SetBigScreenState(bool),
|
||||
SetState(HostState),
|
||||
PlayerList(Box<[PlayerState]>),
|
||||
Settings(GameSettings),
|
||||
Error(GameError),
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum HostState {
|
||||
Disconnected,
|
||||
Lobby {
|
||||
players: Box<[PlayerState]>,
|
||||
settings: GameSettings,
|
||||
},
|
||||
Day {
|
||||
characters: Box<[CharacterState]>,
|
||||
marked_for_execution: Box<[CharacterId]>,
|
||||
day: NonZeroU8,
|
||||
},
|
||||
GameOver {
|
||||
result: GameOver,
|
||||
},
|
||||
RoleReveal {
|
||||
ackd: Box<[Target]>,
|
||||
waiting: Box<[Target]>,
|
||||
},
|
||||
Prompt(PublicIdentity, ActionPrompt),
|
||||
Result(PublicIdentity, ActionResult),
|
||||
CoverOfDarkness,
|
||||
}
|
||||
|
||||
impl From<ServerToHostMessage> for HostEvent {
|
||||
fn from(msg: ServerToHostMessage) -> Self {
|
||||
match msg {
|
||||
ServerToHostMessage::Disconnect => HostEvent::SetState(HostState::Disconnected),
|
||||
ServerToHostMessage::Daytime {
|
||||
characters,
|
||||
day,
|
||||
marked: marked_for_execution,
|
||||
} => HostEvent::SetState(HostState::Day {
|
||||
characters,
|
||||
day,
|
||||
marked_for_execution,
|
||||
}),
|
||||
ServerToHostMessage::Lobby(players) => HostEvent::PlayerList(players),
|
||||
ServerToHostMessage::GameSettings(settings) => HostEvent::Settings(settings),
|
||||
ServerToHostMessage::Error(err) => HostEvent::Error(err),
|
||||
ServerToHostMessage::GameOver(game_over) => {
|
||||
HostEvent::SetState(HostState::GameOver { result: game_over })
|
||||
}
|
||||
ServerToHostMessage::ActionPrompt(ident, prompt) => {
|
||||
HostEvent::SetState(HostState::Prompt(ident, prompt))
|
||||
}
|
||||
ServerToHostMessage::ActionResult(ident, result) => {
|
||||
HostEvent::SetState(HostState::Result(ident, result))
|
||||
}
|
||||
ServerToHostMessage::WaitingForRoleRevealAcks { ackd, waiting } => {
|
||||
HostEvent::SetState(HostState::RoleReveal { ackd, waiting })
|
||||
}
|
||||
ServerToHostMessage::CoverOfDarkness => HostEvent::SetState(HostState::CoverOfDarkness),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Host {
|
||||
send: Sender<HostMessage>,
|
||||
state: HostState,
|
||||
error_callback: Callback<Option<WerewolfError>>,
|
||||
big_screen: bool,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
impl Component for Host {
|
||||
type Message = HostEvent;
|
||||
|
||||
type Properties = ();
|
||||
|
||||
fn create(ctx: &Context<Self>) -> Self {
|
||||
gloo::utils::document().set_title("Werewolves Host");
|
||||
if let Some(clients) = gloo::utils::document()
|
||||
.query_selector("clients")
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
clients.remove();
|
||||
}
|
||||
let (send, recv) = futures::channel::mpsc::channel(100);
|
||||
let scope = ctx.link().clone();
|
||||
yew::platform::spawn_local(async move { worker(recv, scope).await });
|
||||
Self {
|
||||
send,
|
||||
state: HostState::Lobby {
|
||||
players: Box::new([]),
|
||||
settings: GameSettings::default(),
|
||||
},
|
||||
debug: option_env!("DEBUG").is_some(),
|
||||
big_screen: false,
|
||||
error_callback: Callback::from(|err| {
|
||||
if let Some(err) = err {
|
||||
log::error!("{err}")
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
log::info!("state: {:?}", self.state);
|
||||
let content = match self.state.clone() {
|
||||
HostState::GameOver { result } => {
|
||||
let send = self.send.clone();
|
||||
let new_lobby = Callback::from(move |_| {
|
||||
let send = send.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
if let Err(err) = send.clone().send(HostMessage::NewLobby).await {
|
||||
log::error!("send new lobby: {err}");
|
||||
}
|
||||
});
|
||||
});
|
||||
html! {
|
||||
<div>
|
||||
<p>{format!("game over: {result:?}")}</p>
|
||||
<div class="button-container">
|
||||
<button onclick={new_lobby}>{"New Lobby"}</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
HostState::Disconnected => html! {
|
||||
<div class="disconnected">
|
||||
<h2>{"disconnected"}</h2>
|
||||
</div>
|
||||
},
|
||||
HostState::Lobby { players, settings } => {
|
||||
let on_error = self.error_callback.clone();
|
||||
|
||||
let settings = self.big_screen.not().then(|| {
|
||||
let send = self.send.clone();
|
||||
let on_changed = Callback::from(move |s| {
|
||||
let send = send.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
let mut send = send.clone();
|
||||
if let Err(err) = send
|
||||
.send(HostMessage::Lobby(HostLobbyMessage::SetGameSettings(s)))
|
||||
.await
|
||||
{
|
||||
log::error!("sending game settings update: {err}");
|
||||
}
|
||||
if let Err(err) = send
|
||||
.send(HostMessage::Lobby(HostLobbyMessage::GetGameSettings))
|
||||
.await
|
||||
{
|
||||
log::error!("sending game settings get: {err}");
|
||||
}
|
||||
});
|
||||
});
|
||||
let send = self.send.clone();
|
||||
let on_start = Callback::from(move |_| {
|
||||
let send = send.clone();
|
||||
let on_error = on_error.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
let mut send = send.clone();
|
||||
if let Err(err) =
|
||||
send.send(HostMessage::Lobby(HostLobbyMessage::Start)).await
|
||||
{
|
||||
on_error.emit(Some(err.into()))
|
||||
}
|
||||
});
|
||||
});
|
||||
html! {
|
||||
<Settings
|
||||
settings={settings}
|
||||
on_start={on_start}
|
||||
on_update={on_changed}
|
||||
players_in_lobby={players.len()}
|
||||
/>
|
||||
}
|
||||
});
|
||||
let on_action = self.big_screen.not().then(|| {
|
||||
let on_error = self.error_callback.clone();
|
||||
let send = self.send.clone();
|
||||
Callback::from(move |(player_id, act): (PlayerId, LobbyPlayerAction)| {
|
||||
let msg = match act {
|
||||
LobbyPlayerAction::Kick => {
|
||||
HostMessage::Lobby(HostLobbyMessage::Kick(player_id))
|
||||
}
|
||||
};
|
||||
let mut send = send.clone();
|
||||
let on_error = on_error.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
if let Err(err) = send.send(msg).await {
|
||||
on_error.emit(Some(err.into()))
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
html! {
|
||||
<div class="column-list">
|
||||
{settings}
|
||||
<Lobby players={players} on_action={on_action}/>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
HostState::Day {
|
||||
characters,
|
||||
day,
|
||||
marked_for_execution,
|
||||
} => {
|
||||
let on_mark = crate::callback::send_fn(
|
||||
|target| {
|
||||
HostMessage::InGame(HostGameMessage::Day(HostDayMessage::MarkForExecution(
|
||||
target,
|
||||
)))
|
||||
},
|
||||
self.send.clone(),
|
||||
);
|
||||
let on_execute = crate::callback::send_message(
|
||||
HostMessage::InGame(HostGameMessage::Day(HostDayMessage::Execute)),
|
||||
self.send.clone(),
|
||||
);
|
||||
html! {
|
||||
<>
|
||||
<h2>{format!("Day {}", day.get())}</h2>
|
||||
<DaytimePlayerList
|
||||
marked={marked_for_execution}
|
||||
big_screen={self.big_screen}
|
||||
characters={
|
||||
characters.into_iter().map(|c| c.into()).collect::<Box<[_]>>()
|
||||
}
|
||||
on_execute={on_execute}
|
||||
on_mark={on_mark}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
HostState::RoleReveal { ackd, waiting } => {
|
||||
let send = self.send.clone();
|
||||
let on_force_ready = Callback::from(move |target: Target| {
|
||||
let send = send.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
if let Err(err) = send
|
||||
.clone()
|
||||
.send(HostMessage::ForceRoleAckFor(target.character_id.clone()))
|
||||
.await
|
||||
{
|
||||
log::error!("force role ack for [{target}]: {err}");
|
||||
}
|
||||
});
|
||||
});
|
||||
html! {
|
||||
<RoleReveal ackd={ackd} waiting={waiting} on_force_ready={on_force_ready}/>
|
||||
}
|
||||
}
|
||||
HostState::Prompt(ident, prompt) => {
|
||||
let send = self.send.clone();
|
||||
let on_complete = Callback::from(move |msg| {
|
||||
let mut send = send.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
if let Err(err) = send.send(msg).await {
|
||||
log::error!("sending prompt response: {err}")
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
html! {
|
||||
<Prompt
|
||||
prompt={prompt}
|
||||
big_screen={self.big_screen}
|
||||
on_complete={on_complete}
|
||||
ident={ident}
|
||||
/>
|
||||
}
|
||||
}
|
||||
HostState::Result(ident, result) => {
|
||||
let send = self.send.clone();
|
||||
let on_complete = Callback::from(move |msg| {
|
||||
let mut send = send.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
if let Err(err) = send.send(msg).await {
|
||||
log::error!("sending action result response: {err}")
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
html! {
|
||||
<ActionResultView
|
||||
result={result}
|
||||
big_screen={self.big_screen}
|
||||
on_complete={on_complete}
|
||||
ident={ident}
|
||||
/>
|
||||
}
|
||||
}
|
||||
HostState::CoverOfDarkness => {
|
||||
let next = self.big_screen.not().then(|| {
|
||||
let send = self.send.clone();
|
||||
Callback::from(move |_| {
|
||||
let mut send = send.clone();
|
||||
yew::platform::spawn_local(async move {
|
||||
if let Err(err) = send
|
||||
.send(HostMessage::InGame(HostGameMessage::Night(
|
||||
HostNightMessage::Next,
|
||||
)))
|
||||
.await
|
||||
{
|
||||
log::error!("sending action result response: {err}")
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
return html! {
|
||||
<CoverOfDarkness next={next} />
|
||||
};
|
||||
}
|
||||
};
|
||||
let debug_nav = self.debug.then(|| {
|
||||
let on_error_click = callback::send_message(
|
||||
HostMessage::Echo(ServerToHostMessage::Error(GameError::NoApprenticeMentor)),
|
||||
self.send.clone(),
|
||||
);
|
||||
html! {
|
||||
<nav class="debug-nav">
|
||||
<div class="row-list">
|
||||
<Button on_click={on_error_click}>{"error"}</Button>
|
||||
</div>
|
||||
</nav>
|
||||
}
|
||||
});
|
||||
html! {
|
||||
<>
|
||||
{debug_nav}
|
||||
<div class="content">
|
||||
{content}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match msg {
|
||||
HostEvent::PlayerList(players) => {
|
||||
match &mut self.state {
|
||||
HostState::Lobby {
|
||||
players: p,
|
||||
settings: _,
|
||||
} => *p = players,
|
||||
HostState::CoverOfDarkness
|
||||
| HostState::Prompt(_, _)
|
||||
| HostState::Result(_, _)
|
||||
| HostState::Disconnected
|
||||
| HostState::RoleReveal {
|
||||
ackd: _,
|
||||
waiting: _,
|
||||
}
|
||||
| HostState::GameOver { result: _ }
|
||||
| HostState::Day {
|
||||
characters: _,
|
||||
day: _,
|
||||
marked_for_execution: _,
|
||||
} => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
HostEvent::SetErrorCallback(callback) => {
|
||||
self.error_callback = callback;
|
||||
false
|
||||
}
|
||||
HostEvent::SetState(state) => {
|
||||
self.state = state;
|
||||
true
|
||||
}
|
||||
HostEvent::Settings(settings) => match &mut self.state {
|
||||
HostState::Lobby {
|
||||
players: _,
|
||||
settings: s,
|
||||
} => {
|
||||
*s = settings;
|
||||
true
|
||||
}
|
||||
HostState::CoverOfDarkness
|
||||
| HostState::Prompt(_, _)
|
||||
| HostState::Result(_, _)
|
||||
| HostState::Disconnected
|
||||
| HostState::RoleReveal {
|
||||
ackd: _,
|
||||
waiting: _,
|
||||
}
|
||||
| HostState::GameOver { result: _ }
|
||||
| HostState::Day {
|
||||
characters: _,
|
||||
day: _,
|
||||
marked_for_execution: _,
|
||||
} => {
|
||||
log::info!("ignoring settings get");
|
||||
false
|
||||
}
|
||||
},
|
||||
HostEvent::Error(err) => {
|
||||
self.error_callback
|
||||
.emit(Some(WerewolfError::GameError(err)));
|
||||
false
|
||||
}
|
||||
HostEvent::SetBigScreenState(state) => {
|
||||
self.big_screen = state;
|
||||
self.debug = false;
|
||||
self.error_callback = Callback::noop();
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
use gloo::storage::{LocalStorage, Storage, errors::StorageError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub enum StorageKey {
|
||||
PlayerId,
|
||||
PublicIdentity,
|
||||
}
|
||||
|
||||
impl StorageKey {
|
||||
const fn key(&self) -> &'static str {
|
||||
match self {
|
||||
StorageKey::PlayerId => "player_id",
|
||||
StorageKey::PublicIdentity => "player_public",
|
||||
}
|
||||
}
|
||||
pub fn get<T>(&self) -> Result<T, StorageError>
|
||||
where
|
||||
T: for<'de> Deserialize<'de>,
|
||||
{
|
||||
LocalStorage::get(self.key())
|
||||
}
|
||||
|
||||
pub fn set<T>(&self, value: T) -> Result<(), StorageError>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
LocalStorage::set(self.key(), value)
|
||||
}
|
||||
|
||||
pub fn get_or_set<T>(&self, value_fn: impl FnOnce() -> T) -> Result<T, StorageError>
|
||||
where
|
||||
T: Serialize + for<'de> Deserialize<'de>,
|
||||
{
|
||||
match LocalStorage::get(self.key()) {
|
||||
Ok(v) => Ok(v),
|
||||
Err(StorageError::KeyNotFound(_)) => {
|
||||
LocalStorage::set(self.key(), (value_fn)())?;
|
||||
self.get()
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue