1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
7 use nix::sys::time::TimeValLike as TVL;
9 // ========== definitions ==========
13 type Time = i32; // make humantime serde
15 // ==================== state ====================
17 #[derive(Debug,Clone,Serialize,Deserialize)]
20 #[serde(default)] per_move: Time,
23 #[derive(Debug,Serialize,Deserialize)]
24 struct Clock { // PieceTrait
28 #[derive(Debug,Serialize,Deserialize)]
31 current: Option<Current>,
32 #[serde(skip)] notify: Option<mpsc::Sender<()>>,
33 #[serde(skip)] running: Option<Running>,
37 #[derive(Debug,Copy,Clone,Serialize,Deserialize)]
40 #[serde(with="timespec_serde")] remaining: TimeSpec, // -ve means flag
43 #[derive(Debug,Copy,Clone,Eq,PartialEq,Serialize,Deserialize)]
48 #[derive(Debug,Clone,Copy)]
54 fn initial_time(&self) -> TimeSpec { TVL::seconds(self.time.into()) }
55 fn per_move(&self) -> TimeSpec { TVL::seconds(self.per_move.into()) }
56 fn initial(&self) -> [TimeSpec; N] {
57 // White is player Y, and they will ge to go first, so the clock
58 // will go from stopped to Y, and then later when it's X's turn
59 // X will get an extra per_move. Y therefore needs per_move too.
61 self.initial_time() + self.per_move(),
68 fn new(spec: &Spec) -> Self {
69 let mut state = State::dummy();
74 fn reset(&mut self, spec: &Spec) {
75 for (ust, t) in izip!(&mut self.users, spec.initial().iter().copied()) {
80 fn any_expired(&self) -> bool {
81 self.users.iter().any(|ust| ust.remaining < TVL::zero())
84 fn implies_running(&self, held: Option<PlayerId>) -> Option<User> {
86 if let Some(Current { user }) = self.current;
88 if ! self.any_expired();
95 #[typetag::serde(name="ChessClock")]
96 impl PieceXData for State {
99 users: [UState { player: default(), remaining: TVL::zero() }; N],
108 // ==================== users ====================
114 #[derive(Copy,Clone,Serialize,Deserialize)]
115 #[derive(Eq,Ord,PartialEq,PartialOrd,Hash)]
116 #[serde(try_from="u8", into="u8")]
119 impl<T> Index<User> for [T;2] {
121 fn index(&self, index: User) -> &T { &self[index.0 as usize] }
123 impl<T> IndexMut<User> for [T;2] {
124 fn index_mut(&mut self, index: User) -> &mut T { &mut self[index.0 as usize] }
127 const USERINFOS: [UserInfo; N] = [
128 UserInfo { idchar: 'x' },
129 UserInfo { idchar: 'y' },
132 const USERS: [User; N] = [ User(false), User(true) ];
134 impl fmt::Display for User {
135 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
136 f.write_char(USERINFOS[*self].idchar)
139 impl fmt::Debug for User {
140 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141 write!(f, "User({})", self)
144 hformat_as_display!{User}
146 #[derive(Debug,Clone,Copy,Error,Serialize,Deserialize)]
147 struct BadClockUserError;
148 display_as_debug!{BadClockUserError}
150 impl TryFrom<u8> for User {
151 type Error = BadClockUserError;
152 #[throws(BadClockUserError)]
153 fn try_from(u: u8) -> User { User(match u {
156 _ => throw!(BadClockUserError),
160 impl TryFrom<char> for User {
161 type Error = BadClockUserError;
162 #[throws(BadClockUserError)]
163 fn try_from(c: char) -> User { User(match c {
166 _ => throw!(BadClockUserError),
170 impl From<User> for u8 {
171 fn from(user: User) -> u8 { user.0 as u8 }
174 impl std::ops::Not for User {
176 fn not(self) -> User { User(! self.0) }
179 // ==================== rendering, abstract ====================
184 remaining: TimeSpec, // always >=0
185 nick: Option<&'r str>,
188 #[derive(Debug,Copy,Clone)]
189 #[derive(Eq,Ord,PartialEq,PartialOrd,Hash)]
199 use URenderState as URS;
202 fn urender<'r>(&self, state: &State, held: Option<PlayerId>,
203 gplayers: &'r GPlayers) -> [URender<'r>; N]
205 let mut r: [URender;N] = izip!(
208 self.spec.initial().iter().copied(),
209 ).map(|(&user, ustate, initial)| {
210 let nick = gplayers.get(ustate.player)
211 .map(|gpl| gpl.nick.as_str());
212 let (st, remaining) =
213 if ustate.remaining < TVL::zero() {
214 (URS::Flag, TVL::zero())
217 if let Some(current) = &state.current {
218 if current.user != user {
220 } else if state.any_expired() {
222 } else if held.is_some() {
227 } else if ustate.remaining == initial {
237 URender { st, remaining, nick }
239 .collect::<ArrayVec<_,2>>()
240 .into_inner().unwrap();
242 if r.iter().filter(|ur| ur.st == URS::Reset).count() == 1 {
244 if ur.st == URS::Reset { ur.st = URS::Stopped }
252 // ==================== running ====================
256 fn do_start_or_stop(&mut self, piece: PieceId,
257 was_current: Option<Current>,
258 was_implied_running: Option<User>,
259 held: Option<PlayerId>,
265 if let Some(was_current) = was_current;
266 if let Some(now_current) = state.current;
267 if now_current != was_current;
268 if ! state.any_expired();
270 let remaining = &mut state.users[now_current.user].remaining;
271 *remaining = *remaining + spec.per_move();
275 if state.implies_running(held) == was_implied_running { return }
280 if let Some(was_running_user) = was_implied_running;
281 if let Some(Running { expires }) = state.running;
283 state.users[was_running_user].remaining = expires - now;
288 if let Some(now_running_user) = state.implies_running(held);
290 let expires = now + state.users[now_running_user].remaining;
291 state.running = Some(Running { expires });
295 state.notify.get_or_insert_with(||{
296 let (tx,rx) = mpsc::channel();
297 let ts = ThreadState {
298 ig: ig.downgrade_to_weak(),
301 next_wakeup: Some(now),
303 thread::spawn(move || {
305 .unwrap_or_else(|e| error!("clock thread failed: {:?}", e));
310 .unwrap_or_else(|e| error!("clock send notify failed: {:?}", e));
315 fn now() -> TimeSpec {
316 clock_gettime(CLOCK_MONOTONIC).context("clock_gettime")?
323 notify: mpsc::Receiver<()>,
324 next_wakeup: Option<TimeSpec>,
331 match self.next_wakeup {
333 let timeout = wakeup - now()?;
334 if timeout > TVL::zero() {
336 Duration::from_nanos(timeout.tv_nsec() as u64) +
337 Duration::from_secs(timeout.tv_sec() as u64);
339 use mpsc::RecvTimeoutError::*;
340 match self.notify.recv_timeout(timeout) {
341 Err(Disconnected) => break,
348 match self.notify.recv() {
349 Err(mpsc::RecvError) => break,
355 let ig = match self.ig.upgrade() {
359 let mut ig = ig.lock().context("relocking game in clock")?;
361 let gpc = ig.gs.pieces.get_mut(self.piece);
362 let gpc = if let Some(gpc) = gpc { gpc } else { break };
364 let state: &mut State = gpc.xdata_mut(|| State::dummy())?;
367 if let Some(user) = state.implies_running(held) {
369 let remaining = state.running.ok_or_else(
370 || internal_error_bydebug(&state)
372 state.users[user].remaining = remaining;
373 let pause: TimeSpec = libc::timespec {
375 tv_nsec: remaining.tv_nsec(),
382 PrepareUpdatesBuffer::spontaneous_image(&mut ig, self.piece, None)?;
387 // ==================== rendering ====================
391 const OUTLINE: RectOutline = RectOutline { xy: PosC::new(W as f64, H as f64) };
394 // ==================== piece management, loading, etc. ====================
396 fn unprepared_update(piece: PieceId) -> UnpreparedUpdates {
397 vec![Box::new(move |buf: &mut PrepareUpdatesBuffer| {
398 buf.piece_update_image(piece, &None)
399 .unwrap_or_else(|e| error!("failed to prep clock: {:?}", e));
403 #[typetag::serde(name="ChessClock")]
404 impl PieceSpec for Spec {
406 fn load(&self, PLA { gpc,.. }: PLA) -> SpecLoaded {
407 if self.time <= 0 { throw!(SpecError::NegativeTimeout) }
413 gpc.xdata_init(State::new(self))?;
424 impl OutlineTrait for Clock {
427 fn outline_path(&self, scale: f64) -> Result<Html, IE>;
428 fn thresh_dragraise(&self) -> Result<Option<Coord>, IE>;
429 fn bbox_approx(&self) -> Result<Rect, IE>;
430 fn shape(&self) -> Option<Shape>;
436 impl PieceBaseTrait for Clock {
437 fn nfaces(&self) -> RawFaceId { 1 }
439 fn itemname(&self) -> &str { "chess-clock" }
442 #[typetag::serde(name="ChessClock")]
443 impl PieceTrait for Clock {
445 fn svg_piece(&self, f: &mut Html, gpc: &GPiece, gs: &GameState,
446 vpid: VisiblePieceId) {
447 let state = gpc.xdata()?
448 .ok_or_else(|| internal_logic_error("missing/wrong xdata"))?;
449 let urenders = self.urender(state, gpc.held, &gs.players);
451 // player missing, nick is red and pink
453 const Y: &[f32] = &[ 7., 0. ];
457 background: &'static str,
462 fn show(self) -> Show {
464 let (text, background, sigil) = match self {
465 Running => ("black", "yellow", "▶" /* > */ ),
466 ActiveHeld => ("black", "yellow", "‖" /* || */ ),
467 OtherFlag => ("black", "yellow", ":" ),
468 Inactive => ("black", "white", ":" ),
469 Stopped => ("black", "lightblue", "□" /* [] */ ),
470 Reset => ("black", "lightgreen", "○" /* O */ ),
471 Flag => ("white", "red", "⚑" /* F */ ),
473 Show { text, background, sigil }
478 <g transform="translate(-20,-7)">"##,
480 for (y, u) in izip!(Y.iter(), urenders.iter()) {
482 <rect y="{}" fill="{}" width="40" height="7"/>"##,
484 Html::lit(u.st.show().background),
488 <rect fill="none" stroke="black" width="40" height="14"></rect>
489 <clipPath id="def.{}.cl"><rect width="40" height="14"></rect></clipPath>"##,
492 for (user, y, u) in izip!(USERS.iter(), Y.iter(), urenders.iter()) {
494 let show = u.st.show();
495 let mins = u.remaining.tv_sec() / 60;
496 let secs = u.remaining.tv_sec() % 60;
497 let mins = mins.to_string();
498 let mins_pad = Html::from_html_string(" ".repeat(3 - mins.len()));
500 let font = monospace_font(6);
502 <{} x="1" y="{}" {} fill="{}" >{}{}{}</text>"##,
503 HTML_TEXT_LABEL_ELEM_START,
504 y, font, Html::lit(show.text),
505 mins_pad, HtmlStr::from_html_str(&mins), Html::lit(show.sigil)
507 // We write this separately because, empirically, not all the
508 // sigil characters have the same width, even in a mono font.
510 <{} x="14" y="{}" {} fill="{}" >{:02}</text>"##,
511 HTML_TEXT_LABEL_ELEM_START,
512 y, font, Html::lit(show.text),
515 let nick_y = y - 0.5;
516 if let Some(nick) = u.nick {
518 <{} x="21" y="{}" fill="{}" clip-path="url(#def.{}.cl)"
519 font-size="4">{}</text>
521 HTML_TEXT_LABEL_ELEM_START,
528 <{} x="27" y="{}" fill="pink" stroke="red"
529 stroke-width="0.1" font-size="4">({})</text>"##,
530 HTML_TEXT_LABEL_ELEM_START,
540 fn describe_html(&self, _gpc: &GPiece, _goccults: &GOccults) -> Html {
541 Html::lit("the chess clock").into()
544 #[throws(InternalError)]
545 fn add_ui_operations(&self, _: ShowUnocculted, upd: &mut Vec<UoDescription>,
546 gs: &GameState, gpc: &GPiece) {
547 let state: &State = gpc.xdata_exp()?;
549 let for_users = || izip!(&USERS, &USERINFOS, &state.users).map(
550 |(&user, userinfo, ust)| {
551 let upchar = userinfo.idchar.to_ascii_uppercase();
552 let upchar = IsHtmlFormatted(upchar);
553 (user, userinfo, ust, upchar)
557 for (user, userinfo, _ust, upchar) in for_users() {
558 if state.current.as_ref().map(|c| c.user) != Some(user) {
559 upd.push(UoDescription {
561 def_key: userinfo.idchar,
562 opname: format!("start-{}", userinfo.idchar),
563 desc: if state.current.is_none() {
564 hformat!("Start, with player {}", &upchar)
566 hformat!("Make player {} current", &upchar)
568 wrc: WRC::Predictable,
573 if state.current.is_some() {
574 upd.push(UoDescription {
577 opname: "stop".to_string(),
578 desc: Html::lit("Stop").into(),
579 wrc: WRC::Predictable,
582 if state.current.is_none() {
583 upd.push(UoDescription {
586 opname: "reset".to_string(),
587 desc: Html::lit("Reset").into(),
588 wrc: WRC::Unpredictable,
592 for (_user, userinfo, ust, upchar) in for_users() {
593 if let Some(_gpl) = gs.players.get(ust.player) {
594 upd.push(UoDescription {
597 opname: format!("unclaim-{}", userinfo.idchar),
598 desc: hformat!("Clear player {}", &upchar),
599 wrc: WRC::Unpredictable,
602 upd.push(UoDescription {
605 opname: format!("claim-{}", userinfo.idchar),
606 desc: hformat!("Become player {}", &upchar),
607 wrc: WRC::Unpredictable,
613 #[throws(ApiPieceOpError)]
614 fn ui_operation(&self, _: ShowUnocculted, args: ApiPieceOpArgs<'_>,
615 opname: &str, _wrc: WhatResponseToClientOp)
617 let ApiPieceOpArgs { gs,piece,player,ioccults,ipc,ig,.. } = args;
618 let gpc = gs.pieces.byid_mut(piece)?;
620 let gpl = gs.players.byid(player)?;
621 let state: &mut State = gpc.xdata_mut_exp()?;
622 let get_user = || opname.chars().next_back().unwrap().try_into().unwrap();
630 let was_current = state.current;
631 let was_implied_running = state.implies_running(held);
633 let (howish,did) = match opname {
634 "start-x" | "start-y" => {
635 let user = get_user();
636 state.current = Some(Current { user });
637 (UniversalImage, format!("activated player {} at the", user))
640 state.current = None;
641 (UniversalImage, format!("stopped"))
644 if state.current.is_some() {
645 throw!(Ia::BadPieceStateForOperation);
647 state.reset(&self.spec);
648 (Unpredictable, format!("reset"))
650 "claim-x" | "claim-y" => {
651 let user = get_user();
652 if let Some(_gpl) = gs.players.get(state.users[user].player) {
653 throw!(Ia::BadPieceStateForOperation);
655 state.users[user].player = player;
656 if state.users[! user].player == player {
657 // OK, you want to swap
658 state.users[! user].player = default();
660 (Unpredictable, format!("became player {} at the", user))
662 "unclaim-x" | "unclaim-y" => {
663 let user = get_user();
664 state.users[user].player = default();
665 (Unpredictable, format!("cleared player {} at the", user))
668 throw!(Ia::BadPieceStateForOperation);
673 let gplayers = &gs.players;
674 if state.users.iter().any(
675 |ust| gplayers.get(ust.player).is_some()
677 PieceMoveable::IfWresting
683 state.do_start_or_stop(piece, was_current, was_implied_running,
684 held, &self.spec, ig)?;
686 let log = log_did_to_piece(ioccults,&gs.occults, gpl, gpc, ipc, &did)
687 .unwrap_or_else(|e| {
688 error!("failed to log: {:?}", &e);
689 vec![LogEntry { html: "<failed to log>".to_html() }]
692 gpc.moveable = moveable;
696 let r: PieceUpdateFromOpSimple = (
697 WhatResponseToClientOp::Unpredictable,
698 PieceUpdateOp::Modify(()),
700 (r.into(), default())
703 let r: UpdateFromOpComplex = (
705 wrc: WhatResponseToClientOp::Predictable,
707 ops: PieceUpdateOps::PerPlayer(default()),
709 unprepared_update(piece),
717 fn held_change_hook(&self,
719 _gplayers: &GPlayers,
721 _goccults: &GOccults,
722 gpieces: &mut GPieces,
724 was_held: Option<PlayerId>)
726 let gpc = gpieces.get_mut(piece);
727 let gpc = if let Some(gpc) = gpc { gpc } else { return default() };
728 let now_held = gpc.held;
729 let state: &mut State = gpc.xdata_mut_exp()?;
730 let was_current = state.current;
731 let was_running = state.implies_running(was_held);
735 if let Some(Current { user }) = state.current;
736 if now_held == Some(state.users[user].player);
738 state.current = Some(Current { user: ! user });
742 state.do_start_or_stop(piece, was_current, was_running,
743 now_held, &self.spec, ig)?;
744 unprepared_update(piece).into()
748 fn save_reloaded_hook(&self, piece: PieceId, gs: &mut GameState,
750 // The effect of this is to reload to the amount remaining at the
751 // last game save. That's probably tolerable, and even arguably
752 // better than having the clock "have kept running" during the
754 let gpc = gs.pieces.byid_mut(piece).context("load hook")?;
756 let state = gpc.xdata_mut(|| State::new(&self.spec))?;
757 state.do_start_or_stop(piece, None, None, held, &self.spec, ig)?;