chiark / gitweb /
clock: Explain why separate rendering with abs positions
[otter.git] / src / clock.rs
1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 use crate::prelude::*;
6
7 use nix::sys::time::TimeValLike as TVL;
8
9 // ========== definitions ==========
10
11 const N: usize = 2;
12
13 type Time = i32; // make humantime serde
14
15 // ==================== state ====================
16
17 #[derive(Debug,Clone,Serialize,Deserialize)]
18 pub struct Spec {
19   time: Time,
20   #[serde(default)] per_move: Time,
21 }
22
23 #[derive(Debug,Serialize,Deserialize)]
24 struct Clock { // PieceTrait
25   spec: Spec,
26 }
27
28 #[derive(Debug,Serialize,Deserialize)]
29 struct State {
30   users: [UState; 2],
31   current: Option<Current>,
32   #[serde(skip)] notify: Option<mpsc::Sender<()>>,
33   #[serde(skip)] running: Option<Running>,
34 }
35
36
37 #[derive(Debug,Copy,Clone,Serialize,Deserialize)]
38 struct UState {
39   player: PlayerId,
40   #[serde(with="timespec_serde")] remaining: TimeSpec, // -ve means flag
41 }
42
43 #[derive(Debug,Copy,Clone,Eq,PartialEq,Serialize,Deserialize)]
44 struct Current {
45   user: User,
46 }
47
48 #[derive(Debug,Clone,Copy)]
49 struct Running {
50   expires: TimeSpec,
51 }
52
53 impl Spec {
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.
60     [
61       self.initial_time() + self.per_move(),
62       self.initial_time(),
63     ]
64   }
65 }
66
67 impl State {
68   fn new(spec: &Spec) -> Self {
69     let mut state = State::dummy();
70     state.reset(spec);
71     state
72   }
73
74   fn reset(&mut self, spec: &Spec) {
75     for (ust, t) in izip!(&mut self.users, spec.initial().iter().copied()) {
76       ust.remaining = t;
77     }
78   }
79
80   fn any_expired(&self) -> bool {
81     self.users.iter().any(|ust| ust.remaining < TVL::zero())
82   }
83
84   fn implies_running(&self, held: Option<PlayerId>) -> Option<User> {
85     if_chain! {
86       if let Some(Current { user }) = self.current;
87       if held.is_none();
88       if ! self.any_expired();
89       then { Some(user) }
90       else { None }
91     }
92   }
93 }
94
95 #[typetag::serde(name="ChessClock")]
96 impl PieceXData for State {
97   fn dummy() -> Self {
98     State {
99       users: [UState { player: default(), remaining: TVL::zero() }; N],
100       current: None,
101       notify: None,
102       running: None,
103     }
104   }
105 }
106
107
108 // ==================== users ====================
109
110 struct UserInfo {
111   idchar: char,
112 }
113
114 #[derive(Copy,Clone,Serialize,Deserialize)]
115 #[derive(Eq,Ord,PartialEq,PartialOrd,Hash)]
116 #[serde(try_from="u8", into="u8")]
117 struct User(bool);
118
119 impl<T> Index<User> for [T;2] {
120   type Output = T;
121   fn index(&self, index: User) -> &T { &self[index.0 as usize] }
122 }  
123 impl<T> IndexMut<User> for [T;2] {
124   fn index_mut(&mut self, index: User) -> &mut T { &mut self[index.0 as usize] }
125 }  
126
127 const USERINFOS: [UserInfo; N] = [
128   UserInfo { idchar: 'x' },
129   UserInfo { idchar: 'y' },
130 ];
131
132 const USERS: [User; N] = [ User(false), User(true) ];
133
134 impl fmt::Display for User {
135   fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
136     f.write_char(USERINFOS[*self].idchar)
137   }
138 }
139 impl fmt::Debug for User {
140   fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
141     write!(f, "User({})", self)
142   }
143 }
144 hformat_as_display!{User}
145
146 #[derive(Debug,Clone,Copy,Error,Serialize,Deserialize)]
147 struct BadClockUserError;
148 display_as_debug!{BadClockUserError}
149
150 impl TryFrom<u8> for User {
151   type Error = BadClockUserError;
152   #[throws(BadClockUserError)]
153   fn try_from(u: u8) -> User { User(match u {
154     0 => false,
155     1 => true,
156     _ => throw!(BadClockUserError),
157   }) }
158 }
159
160 impl TryFrom<char> for User {
161   type Error = BadClockUserError;
162   #[throws(BadClockUserError)]
163   fn try_from(c: char) -> User { User(match c {
164     'x' | 'X' => false,
165     'y' | 'Y' => true,
166     _ => throw!(BadClockUserError),
167   }) }
168 }
169
170 impl From<User> for u8 {
171   fn from(user: User) -> u8 { user.0 as u8 }
172 }
173
174 impl std::ops::Not for User {
175   type Output = User;
176   fn not(self) -> User { User(! self.0) }
177 }
178
179 // ==================== rendering, abstract ====================
180
181 #[derive(Debug)]
182 struct URender<'r> {
183   st: URenderState,
184   remaining: TimeSpec, // always >=0
185   nick: Option<&'r str>,
186 }
187
188 #[derive(Debug,Copy,Clone)]
189 #[derive(Eq,Ord,PartialEq,PartialOrd,Hash)]
190 enum URenderState {
191   Running,
192   ActiveHeld,
193   Inactive,
194   OtherFlag,
195   Stopped,
196   Reset,
197   Flag,
198 }
199 use URenderState as URS;
200
201 impl Clock {
202   fn urender<'r>(&self, state: &State, held: Option<PlayerId>,
203                  gplayers: &'r GPlayers) -> [URender<'r>; N]
204   {
205     let mut r: [URender;N] = izip!(
206       USERS.iter(),
207       state.users.iter(),
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())
215         } else {
216           (
217             if let Some(current) = &state.current {
218               if current.user != user {
219                 URS::Inactive
220               } else if state.any_expired() {
221                 URS::OtherFlag
222               } else if held.is_some() {
223                 URS::ActiveHeld
224               } else {
225                 URS::Running
226               }
227             } else if ustate.remaining == initial {
228               URS::Reset
229             } else {
230               URS::Stopped
231             }
232
233             , ustate.remaining
234           )
235         };
236
237       URender { st, remaining, nick }
238     })
239       .collect::<ArrayVec<_,2>>()
240       .into_inner().unwrap();
241
242     if r.iter().filter(|ur| ur.st == URS::Reset).count() == 1 {
243       for ur in &mut r {
244         if ur.st == URS::Reset { ur.st = URS::Stopped }
245       }
246     }
247
248     r
249   }
250 }
251
252 // ==================== running ====================
253
254 impl State {
255   #[throws(IE)]
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>,
260                       spec: &Spec,
261                       ig: &InstanceRef) {
262     let state = self;
263
264     if_chain! {
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();
269       then {
270         let remaining = &mut state.users[now_current.user].remaining;
271         *remaining = *remaining + spec.per_move();
272       }
273     }
274
275     if state.implies_running(held) == was_implied_running { return }
276
277     let now = now()?;
278
279     if_chain! {
280       if let Some(was_running_user) = was_implied_running;
281       if let Some(Running { expires }) = state.running;
282       then { 
283         state.users[was_running_user].remaining = expires - now;
284       }
285     }
286
287     if_chain! {
288       if let Some(now_running_user) = state.implies_running(held);
289       then {
290         let expires = now + state.users[now_running_user].remaining;
291         state.running = Some(Running { expires });
292       }
293     }
294
295     state.notify.get_or_insert_with(||{
296       let (tx,rx) = mpsc::channel();
297       let ts = ThreadState {
298         ig: ig.downgrade_to_weak(),
299         piece,
300         notify: rx,
301         next_wakeup: Some(now),
302       };
303       thread::spawn(move || {
304         ts.run()
305           .unwrap_or_else(|e| error!("clock thread failed: {:?}", e));
306       });
307       tx
308     })
309       .send(())
310       .unwrap_or_else(|e| error!("clock send notify failed: {:?}", e));
311   }
312 }
313
314 #[throws(IE)]
315 fn now() -> TimeSpec {
316   clock_gettime(CLOCK_MONOTONIC).context("clock_gettime")?
317 }
318
319 #[derive(Debug)]
320 struct ThreadState {
321   ig: InstanceWeakRef,
322   piece: PieceId,
323   notify: mpsc::Receiver<()>,
324   next_wakeup: Option<TimeSpec>,
325 }
326
327 impl ThreadState {
328   #[throws(IE)]
329   fn run(mut self) {
330     loop {
331       match self.next_wakeup {
332         Some(wakeup) => {
333           let timeout = wakeup - now()?;
334           if timeout > TVL::zero() {
335             let timeout =
336               Duration::from_nanos(timeout.tv_nsec() as u64) +
337               Duration::from_secs(timeout.tv_sec() as u64);
338
339             use mpsc::RecvTimeoutError::*;
340             match self.notify.recv_timeout(timeout) {
341               Err(Disconnected) => break,
342               Err(Timeout) => { },
343               Ok(()) => { },
344             }
345           }
346         }
347         None => {
348           match self.notify.recv() {
349             Err(mpsc::RecvError) => break,
350             Ok(()) => { },
351           }
352         }
353       };
354
355       let ig = match self.ig.upgrade() {
356         Some(ig) => ig,
357         None => break,
358       };
359       let mut ig = ig.lock().context("relocking game in clock")?;
360
361       let gpc = ig.gs.pieces.get_mut(self.piece);
362       let gpc = if let Some(gpc) = gpc { gpc } else { break };
363       let held = gpc.held;
364       let state: &mut State = gpc.xdata_mut(|| State::dummy())?;
365
366       self.next_wakeup =
367         if let Some(user) = state.implies_running(held) {
368           let now = now()?;
369           let remaining = state.running.ok_or_else(
370             || internal_error_bydebug(&state)
371           )?.expires - now;
372           state.users[user].remaining = remaining;
373           let pause: TimeSpec = libc::timespec {
374             tv_sec: 0,
375             tv_nsec: remaining.tv_nsec(),
376           }.into();
377           Some(pause + now)
378         } else {
379           None
380         };
381
382       PrepareUpdatesBuffer::spontaneous_image(&mut ig, self.piece, None)?;
383     }
384   }
385 }
386
387 // ==================== rendering ====================
388
389 const W: Coord = 40;
390 const H: Coord = 14;
391 const OUTLINE: RectOutline = RectOutline { xy: PosC::new(W as f64, H as f64) };
392
393
394 // ==================== piece management, loading, etc. ====================
395
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));
400   })]
401 }
402
403 #[typetag::serde(name="ChessClock")]
404 impl PieceSpec for Spec {
405   #[throws(SpecError)]
406   fn load(&self, PLA { gpc,.. }: PLA) -> SpecLoaded {
407     if self.time <= 0 { throw!(SpecError::NegativeTimeout) }
408
409     let clock = Clock {
410       spec: self.clone(),
411     };
412
413     gpc.xdata_init(State::new(self))?;
414
415     SpecLoaded {
416       p: Box::new(clock),
417       occultable: None,
418       special: default(),
419     }
420   }
421 }
422
423 #[dyn_upcast]
424 impl OutlineTrait for Clock {
425   delegate!{
426     to OUTLINE {
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>;
431     }
432   }
433 }
434
435 #[dyn_upcast]
436 impl PieceBaseTrait for Clock {
437   fn nfaces(&self) -> RawFaceId { 1 }
438
439   fn itemname(&self) -> &str { "chess-clock" }
440 }
441
442 #[typetag::serde(name="ChessClock")]
443 impl PieceTrait for Clock {
444   #[throws(IE)]
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);
450
451     // player missing, nick is red and pink
452
453     const Y: &[f32] = &[ 7., 0. ];
454
455     struct Show {
456       text:       &'static str,
457       background: &'static str,
458       sigil:      &'static str,
459     }
460
461     impl URenderState {
462       fn show(self) -> Show {
463         use URS::*;
464         let (text, background, sigil) = match self {
465           Running    => ("black",  "yellow",     "&#x25b6;" /* >  */ ),
466           ActiveHeld => ("black",  "yellow",     "&#x2016;" /* || */ ),
467           OtherFlag  => ("black",  "yellow",     ":"                 ),
468           Inactive   => ("black",  "white",      ":"                 ),
469           Stopped    => ("black",  "lightblue",  "&#x25a1;" /* [] */ ),
470           Reset      => ("black",  "lightgreen", "&#x25cb;" /* O  */ ),
471           Flag       => ("white",  "red",        "&#x2691;" /* F  */ ),
472         };
473         Show { text, background, sigil }
474       }
475     }
476     
477     hwrite!(f, r##"
478 <g transform="translate(-20,-7)">"##,
479     )?;
480     for (y, u) in izip!(Y.iter(), urenders.iter()) {
481       hwrite!(f, r##"
482   <rect y="{}" fill="{}" width="40" height="7"/>"##,
483               y,
484               Html::lit(u.st.show().background),
485       )?;
486     }
487     hwrite!(f, r##"
488   <rect fill="none" stroke="black" width="40" height="14"></rect>
489   <clipPath id="def.{}.cl"><rect width="40" height="14"></rect></clipPath>"##,
490             &vpid
491     )?;
492     for (user, y, u) in izip!(USERS.iter(), Y.iter(), urenders.iter()) {
493       let y = y + 6.;
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("&nbsp;".repeat(3 - mins.len()));
499
500       let font = monospace_font(6);
501       hwrite!(f, r##"
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)
506       )?;
507       // We write this separately because, empirically, not all the
508       // sigil characters have the same width, even in a mono font.
509       hwrite!(f, r##"
510   <{} x="14" y="{}" {} fill="{}" >{:02}</text>"##,
511              HTML_TEXT_LABEL_ELEM_START,
512              y, font, Html::lit(show.text),
513              secs
514       )?;
515       let nick_y = y - 0.5;
516       if let Some(nick) = u.nick {
517         hwrite!(f, r##"
518   <{} x="21" y="{}" fill="{}" clip-path="url(#def.{}.cl)" 
519    font-size="4">{}</text>
520               "##,
521                HTML_TEXT_LABEL_ELEM_START,
522                nick_y, show.text,
523                vpid,
524                nick,
525         )?;
526       } else {
527         hwrite!(f, r##"
528   <{} x="27" y="{}" fill="pink" stroke="red"
529    stroke-width="0.1" font-size="4">({})</text>"##,
530                HTML_TEXT_LABEL_ELEM_START,
531                nick_y, user,
532         )?;
533       }
534     }
535     hwrite!(f, r##"
536 </g>"##)?;
537   }
538
539   #[throws(IE)]
540   fn describe_html(&self, _gpc: &GPiece, _goccults: &GOccults) -> Html {
541     Html::lit("the chess clock").into()
542   }
543
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()?;
548
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)
554       }
555     );
556
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 {
560           kind: UoKind::Piece,
561           def_key: userinfo.idchar,
562           opname: format!("start-{}", userinfo.idchar),
563           desc: if state.current.is_none() {
564             hformat!("Start, with player {}", &upchar)
565           } else {
566             hformat!("Make player {} current", &upchar)
567           },
568           wrc: WRC::Predictable,
569         });
570       }
571     }
572
573     if state.current.is_some() {
574       upd.push(UoDescription {
575         kind: UoKind::Piece,
576         def_key: 'S',
577         opname: "stop".to_string(),
578         desc: Html::lit("Stop").into(),
579         wrc: WRC::Predictable,
580       });
581     }
582     if state.current.is_none() {
583       upd.push(UoDescription {
584         kind: UoKind::Piece,
585         def_key: 'R',
586         opname: "reset".to_string(),
587         desc: Html::lit("Reset").into(),
588         wrc: WRC::Unpredictable,
589       });
590     }
591
592     for (_user, userinfo, ust, upchar) in for_users() {
593       if let Some(_gpl) = gs.players.get(ust.player) {
594         upd.push(UoDescription {
595           kind: UoKind::Piece,
596           def_key: upchar.0,
597           opname: format!("unclaim-{}", userinfo.idchar),
598           desc: hformat!("Clear player {}", &upchar),
599           wrc: WRC::Unpredictable,
600         });
601       } else {
602         upd.push(UoDescription {
603           kind: UoKind::Piece,
604           def_key: upchar.0,
605           opname: format!("claim-{}", userinfo.idchar),
606           desc: hformat!("Become player {}", &upchar),
607           wrc: WRC::Unpredictable,
608         });
609       }
610     }
611   }
612
613   #[throws(ApiPieceOpError)]
614   fn ui_operation(&self, _: ShowUnocculted, args: ApiPieceOpArgs<'_>,
615                   opname: &str, _wrc: WhatResponseToClientOp)
616                   -> OpOutcomeThunk {
617     let ApiPieceOpArgs { gs,piece,player,ioccults,ipc,ig,.. } = args;
618     let gpc = gs.pieces.byid_mut(piece)?;
619     let held = gpc.held;
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();
623
624     enum Howish {
625       UniversalImage,
626       Unpredictable,
627     }
628     use Howish::*;
629
630     let was_current = state.current;
631     let was_implied_running = state.implies_running(held);
632
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))
638       },
639       "stop" => {
640         state.current = None;
641         (UniversalImage, format!("stopped"))
642       },
643       "reset" => {
644         if state.current.is_some() {
645           throw!(Ia::BadPieceStateForOperation);
646         }
647         state.reset(&self.spec);
648         (Unpredictable, format!("reset"))
649       },
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);
654         }
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();
659         }
660         (Unpredictable, format!("became player {} at the", user))
661       },
662       "unclaim-x" | "unclaim-y" => {
663         let user = get_user();
664         state.users[user].player = default();
665         (Unpredictable, format!("cleared player {} at the", user))
666       },
667       _ => {
668         throw!(Ia::BadPieceStateForOperation);
669       }
670     };
671
672     let moveable = {
673       let gplayers = &gs.players;
674       if state.users.iter().any(
675         |ust| gplayers.get(ust.player).is_some()
676       ) {
677         PieceMoveable::IfWresting
678       } else {
679         PieceMoveable::Yes
680       }
681     };
682
683     state.do_start_or_stop(piece, was_current, was_implied_running,
684                            held, &self.spec, ig)?;
685
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() }]
690       });
691
692     gpc.moveable = moveable;
693     
694     match howish {
695       Unpredictable => {
696         let r: PieceUpdateFromOpSimple = (
697           WhatResponseToClientOp::Unpredictable,
698           PieceUpdateOp::Modify(()),
699           log);
700         (r.into(), default())
701       }
702       UniversalImage => {
703         let r: UpdateFromOpComplex = (
704           PieceUpdate {
705             wrc: WhatResponseToClientOp::Predictable,
706             log,
707             ops: PieceUpdateOps::PerPlayer(default()),
708           },
709           unprepared_update(piece),
710         );
711         r
712       }
713     }.into()
714   }
715
716   #[throws(IE)]
717   fn held_change_hook(&self,
718                       ig: &InstanceRef,
719                       _gplayers: &GPlayers,
720                       _ipieces: &IPieces,
721                       _goccults: &GOccults,
722                       gpieces: &mut GPieces,
723                       piece: PieceId,
724                       was_held: Option<PlayerId>)
725                       -> OpHookThunk {
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);
732
733     if_chain! {
734       if was_held == None;
735       if let Some(Current { user }) = state.current;
736       if now_held == Some(state.users[user].player);
737       then {
738         state.current = Some(Current { user: ! user });
739       }
740     }
741
742     state.do_start_or_stop(piece, was_current, was_running,
743                            now_held, &self.spec, ig)?;
744     unprepared_update(piece).into()
745   }
746
747   #[throws(IE)]
748   fn save_reloaded_hook(&self, piece: PieceId, gs: &mut GameState,
749                         ig: &InstanceRef) {
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
753     // lost state.
754     let gpc = gs.pieces.byid_mut(piece).context("load hook")?;
755     let held = gpc.held;
756     let state = gpc.xdata_mut(|| State::new(&self.spec))?;
757     state.do_start_or_stop(piece, None, None, held, &self.spec, ig)?;
758   }
759 }