1use std::time::Duration;
6
7use anyhow::{Context, Result, bail};
8use clap::Args;
9
10#[derive(Debug, Clone, Args)]
11#[allow(clippy::struct_excessive_bools)] pub struct Config {
13 #[arg(long, env = "DATABASE_URL", hide_env_values = true)]
17 pub database_url: String,
18
19 #[arg(long, env = "CHESS_LISTEN_DATABASE_URL", hide_env_values = true)]
21 pub listen_database_url: Option<String>,
22
23 #[arg(long, env = "CHESS_SITE_SCHEMA", default_value = "esports")]
28 pub site_schema: String,
29
30 #[arg(long, env = "CHESS_SCHEMA", default_value = "chess")]
32 pub schema: String,
33
34 #[arg(long, env = "CHESS_POOL_SIZE", default_value_t = 8)]
35 pub pool_size: usize,
36
37 #[arg(long, env = "CHESS_PUBLIC_URL", default_value = "http://localhost:8090")]
41 pub public_url: String,
42
43 #[arg(long, env = "ESPORTS_PUBLIC_URL", default_value = "http://localhost:8080")]
45 pub site_url: String,
46
47 #[arg(
52 long,
53 env = "ESPORTS_LOGIN",
54 default_value_t = false,
55 num_args = 0..=1,
56 default_missing_value = "true",
57 action = clap::ArgAction::Set,
58 value_parser = clap::builder::BoolishValueParser::new()
59 )]
60 pub esports_login: bool,
61
62 #[arg(long, env = "ESPORTS_SECRET", hide_env_values = true)]
65 pub esports_secret: Option<String>,
66
67 #[arg(
71 long,
72 env = "CHESS_SIGNUP",
73 default_value_t = true,
74 num_args = 0..=1,
75 default_missing_value = "true",
76 action = clap::ArgAction::Set,
77 value_parser = clap::builder::BoolishValueParser::new()
78 )]
79 pub signup: bool,
80
81 #[arg(long, env = "CHESS_SITE_NAME", default_value = "open-chess")]
84 pub site_name: String,
85
86 #[arg(long, env = "CHESS_BIND", default_value = "0.0.0.0:8090")]
87 pub bind: String,
88
89 #[arg(long, env = "CHESS_METRICS_BIND", default_value = "0.0.0.0:9465")]
91 pub metrics_bind: String,
92
93 #[arg(long, env = "CHESS_LAG_COMP_MS", default_value_t = 100)]
95 pub lag_comp_ms: i64,
96
97 #[arg(long, env = "CHESS_CHALLENGE_TTL_SECS", default_value_t = 20)]
100 pub challenge_ttl_secs: u64,
101
102 #[arg(long, env = "CHESS_FIRST_MOVE_SECS", default_value_t = 30)]
105 pub first_move_secs: u64,
106
107 #[arg(long, env = "CHESS_GONE_AFTER_SECS", default_value_t = 20)]
109 pub gone_after_secs: u64,
110
111 #[arg(long, env = "CHESS_CLAIM_WIN_SECS", default_value_t = 10)]
113 pub claim_win_secs: u64,
114
115 #[arg(long, env = "CHESS_PUBLIC_DELAY_PLIES", default_value_t = 3)]
118 pub public_delay_plies: usize,
119
120 #[arg(long, env = "CHESS_FAIRPLAY", default_value_t = true, action = clap::ArgAction::Set)]
122 pub fairplay: bool,
123
124 #[arg(long, env = "CHESS_BLOCK_MARKED", default_value_t = true, action = clap::ArgAction::Set)]
126 pub block_marked: bool,
127
128 #[arg(long, env = "CHESS_SWEEP_MS", default_value_t = 500)]
130 pub sweep_ms: u64,
131
132 #[arg(long, env = "CHESS_VISION_ENDPOINT", default_value = "")]
138 pub vision_endpoint: String,
139
140 #[arg(long, env = "CHESS_VISION_MODEL", default_value = "qwen3-vl:8b")]
142 pub vision_model: String,
143
144 #[arg(long, env = "CHESS_VISION_API_KEY", hide_env_values = true)]
146 pub vision_api_key: Option<String>,
147
148 #[arg(long, env = "CHESS_VISION_LOCAL_MODEL", default_value = "")]
153 pub vision_local_model: String,
154
155 #[arg(long, env = "CHESS_VISION_MIN_CONFIDENCE", default_value_t = 0.5)]
159 pub vision_min_confidence: f32,
160
161 #[arg(long, env = "CHESS_VISION_REVIEW", default_value_t = true, action = clap::ArgAction::Set)]
165 pub vision_review: bool,
166
167 #[arg(long, env = "CHESS_VISION_REVIEW_MAX", default_value_t = 5000)]
170 pub vision_review_max: i64,
171
172 #[arg(long, env = "CHESS_TABLEBASE_URL", default_value = "https://tablebase.lichess.ovh")]
176 pub tablebase_url: String,
177
178 #[arg(long, env = "CHESS_APP_DIR")]
181 pub app_dir: Option<String>,
182
183 #[arg(long, env = "CHESS_APP_THEME")]
188 pub app_theme: Option<String>,
189}
190
191impl Config {
192 pub fn base_path(&self) -> Result<String> {
194 let u = url::Url::parse(&self.public_url).context("CHESS_PUBLIC_URL isn't a URL")?;
195 let p = u.path().trim_end_matches('/');
196 Ok(p.to_owned())
197 }
198
199 pub fn public_url(&self) -> &str {
200 self.public_url.trim_end_matches('/')
201 }
202
203 pub fn esports(&self) -> Option<&str> {
205 self.esports_secret.as_deref().filter(|s| self.esports_login && s.len() >= 32)
206 }
207
208 pub fn challenge_ttl(&self) -> Duration {
209 Duration::from_secs(self.challenge_ttl_secs.max(5))
210 }
211
212 pub fn validate(&self) -> Result<()> {
213 for s in [&self.schema, &self.site_schema] {
214 if s.is_empty() || !s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
215 bail!("schema names are letters, digits and _ (got {s:?})");
216 }
217 }
218 if self.schema == self.site_schema {
219 bail!("CHESS_SCHEMA must differ from CHESS_SITE_SCHEMA");
220 }
221 self.base_path()?;
222 Ok(())
223 }
224}
225
226#[derive(Debug, Clone, Args)]
228pub struct AnalyseConfig {
229 #[arg(long, env = "CHESS_STOCKFISH")]
231 pub stockfish: Option<String>,
232
233 #[arg(long, env = "CHESS_ANALYSIS_DEPTH", default_value_t = 14)]
235 pub depth: u32,
236
237 #[arg(long, env = "CHESS_ANALYSIS_THREADS", default_value_t = 1)]
239 pub threads: u32,
240
241 #[arg(long, env = "CHESS_ANALYSIS_HASH_MB", default_value_t = 64)]
243 pub hash_mb: u32,
244
245 #[arg(long, env = "CHESS_FAIRPLAY_SKIP_PLIES", default_value_t = 16)]
247 pub skip_plies: usize,
248
249 #[arg(long, env = "CHESS_FAIRPLAY_MODEL", hide_env_values = true)]
255 pub model: Option<String>,
256
257 #[arg(long, env = "CHESS_FAIRPLAY_FLAG_SCORE")]
259 pub flag_score: Option<f64>,
260
261 #[arg(long, env = "CHESS_FAIRPLAY_MIN_MOVES")]
264 pub min_moves: Option<usize>,
265
266 #[arg(long, env = "CHESS_FAIRPLAY_APP_DEPTH", default_value_t = 6)]
269 pub app_depth: u8,
270
271 #[arg(long, env = "CHESS_FAIRPLAY_APP_NODES", default_value_t = 400_000)]
272 pub app_nodes: u64,
273
274 #[arg(long, env = "CHESS_FAIRPLAY_APP_NET")]
277 pub app_net: Option<String>,
278
279 #[arg(long)]
281 pub once: bool,
282}
283
284impl AnalyseConfig {
285 pub fn model(&self) -> Result<(chess_fairplay::Model, bool)> {
289 let (mut model, configured) = match self.model.as_deref().map(str::trim) {
290 None | Some("") => (chess_fairplay::Model::default(), false),
291 Some(json) if json.starts_with('{') => {
292 (serde_json::from_str(json).context("CHESS_FAIRPLAY_MODEL isn't a valid model")?, true)
293 }
294 Some(path) => {
295 let text = std::fs::read_to_string(path)
296 .with_context(|| format!("reading the fair-play model {path}"))?;
297 (serde_json::from_str(&text).with_context(|| format!("{path} isn't a valid model"))?, true)
298 }
299 };
300 if let Some(s) = self.flag_score {
301 model.flag_score = s;
302 }
303 if let Some(n) = self.min_moves {
304 model.min_moves = n;
305 }
306 model.validate().map_err(|e| anyhow::anyhow!("fair-play model: {e}"))?;
307 Ok((model, configured))
308 }
309}