summaryrefslogtreecommitdiffstats
path: root/gui/src/app.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--gui/src/app.rs253
1 files changed, 153 insertions, 100 deletions
diff --git a/gui/src/app.rs b/gui/src/app.rs
index fde117a..b5a50b3 100644
--- a/gui/src/app.rs
+++ b/gui/src/app.rs
@@ -10,11 +10,12 @@ use eframe::egui::{
10 self, Align, Button, CentralPanel, Color32, Context, Frame, Key, Layout, Margin, Panel, 10 self, Align, Button, CentralPanel, Color32, Context, Frame, Key, Layout, Margin, Panel,
11 RichText, Sense, Stroke, Ui, Vec2, vec2, 11 RichText, Sense, Stroke, Ui, Vec2, vec2,
12}; 12};
13use zagan_bet::client::games::roulette::{Cursor, Direction}; 13use zagan_bet::client::games::roulette::Cursor;
14use zagan_bet::client::view::{RoulettePhase, RouletteView}; 14use zagan_bet::client::view::{RoulettePhase, RouletteView};
15use zagan_bet::roulette::{BetTarget, Bets, Outside, WHEEL, color as wheel_color}; 15use zagan_bet::roulette::{BetTarget, Bets, Outside, WHEEL, color as wheel_color};
16use zagan_bet::version::VERSION; 16use zagan_bet::version::VERSION;
17 17
18use crate::roulette::FeltAction;
18use crate::{roulette, theme}; 19use crate::{roulette, theme};
19 20
20const HEADER_HEIGHT: f32 = 64.0; 21const HEADER_HEIGHT: f32 = 64.0;
@@ -218,10 +219,11 @@ fn sidebar(ui: &mut Ui, demo: &mut Demo) {
218 219
219 ui.label(theme::label("Chip")); 220 ui.label(theme::label("Chip"));
220 ui.add_space(theme::SPACE_2); 221 ui.add_space(theme::SPACE_2);
222 let spinning = demo.phase == RoulettePhase::Spinning;
221 ui.horizontal(|ui| { 223 ui.horizontal(|ui| {
222 ui.spacing_mut().item_spacing.x = theme::SPACE_2; 224 ui.spacing_mut().item_spacing.x = theme::SPACE_2;
223 for amount in CHIP_SIZES { 225 for amount in CHIP_SIZES {
224 if chip_button(ui, amount, demo.chip == amount).clicked() { 226 if chip_button(ui, amount, demo.chip == amount, !spinning).clicked() {
225 demo.chip = amount; 227 demo.chip = amount;
226 } 228 }
227 } 229 }
@@ -247,80 +249,118 @@ fn sidebar(ui: &mut Ui, demo: &mut Demo) {
247 ui.add_space(theme::SPACE_2); 249 ui.add_space(theme::SPACE_2);
248 bet_row(ui, "Total", demo.bets.total()); 250 bet_row(ui, "Total", demo.bets.total());
249 251
250 ui.add_space(theme::SPACE_3); 252 ui.add_space(theme::SPACE_4);
251 let spinning = demo.phase == RoulettePhase::Spinning; 253 let actions = ui.available_width();
252 let spin = Button::new( 254 let clear_width = 108.0;
253 RichText::new("SPIN") 255 ui.horizontal(|ui| {
254 .font(theme::mono(theme::FONT_MONO)) 256 ui.spacing_mut().item_spacing.x = theme::SPACE_2;
255 .color(theme::BG),
256 )
257 .fill(if spinning {
258 theme::WIN.gamma_multiply(0.45)
259 } else {
260 theme::WIN
261 })
262 .corner_radius(theme::RADIUS_MD)
263 .min_size(vec2(ui.available_width(), 44.0));
264 if ui.add_enabled(!spinning, spin).clicked() {
265 demo.start_spin(ui.ctx());
266 }
267 257
268 ui.add_space(theme::SPACE_2); 258 let spin = Button::new(
269 let clear = Button::new( 259 RichText::new("SPIN")
270 RichText::new("Clear bets") 260 .font(theme::mono(theme::FONT_MONO))
271 .font(theme::sans(12.0)) 261 .color(theme::BG),
272 .color(theme::TEXT_DIM), 262 )
273 ) 263 .fill(if spinning {
274 .frame(false); 264 theme::WIN.gamma_multiply(0.45)
275 if ui 265 } else {
276 .add_enabled(!demo.bets.is_empty() && !spinning, clear) 266 theme::WIN
277 .clicked() 267 })
278 { 268 .corner_radius(theme::RADIUS_MD)
279 demo.bets = Bets::default(); 269 .min_size(vec2(actions - clear_width - theme::SPACE_2, 44.0));
280 } 270 let can_spin = !spinning && !demo.bets.is_empty();
271 if ui.add_enabled(can_spin, spin).clicked() {
272 demo.start_spin(ui.ctx());
273 }
274
275 let clear = Button::new(
276 RichText::new("CLEAR")
277 .font(theme::mono(12.0))
278 .color(theme::TEXT_DIM),
279 )
280 .fill(theme::SURFACE_RAISED)
281 .stroke(Stroke::new(1.0, theme::LINE))
282 .corner_radius(theme::RADIUS_MD)
283 .min_size(vec2(clear_width, 44.0));
284 if ui
285 .add_enabled(!demo.bets.is_empty() && !spinning, clear)
286 .clicked()
287 {
288 demo.begin_placing();
289 demo.bets = Bets::default();
290 }
291 });
281} 292}
282 293
283fn result_line(ui: &mut Ui, demo: &Demo) { 294fn result_line(ui: &mut Ui, demo: &Demo) {
284 ui.horizontal(|ui| { 295 ui.horizontal(|ui| match (demo.phase, demo.number) {
285 match (demo.phase, demo.number) { 296 (RoulettePhase::Result, Some(number)) => {
286 (RoulettePhase::Result, Some(number)) => { 297 ui.label(theme::label("Result"));
287 ui.label(theme::label("Result")); 298 ui.label(
288 ui.label( 299 RichText::new(format!("{number} {}", wheel_color(number).to_uppercase()))
289 RichText::new(number.to_string()) 300 .font(theme::mono(14.0))
290 .font(theme::mono(15.0)) 301 .color(color_of(number)),
291 .color(color_of(number)), 302 );
292 ); 303 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
293 ui.label( 304 payout(ui, demo);
294 RichText::new(wheel_color(number).to_uppercase()) 305 });
295 .font(theme::mono(12.0)) 306 }
296 .color(theme::TEXT_DIM), 307 (RoulettePhase::Spinning, _) => {
297 ); 308 ui.label(
298 } 309 RichText::new("SPINNING…")
299 (RoulettePhase::Spinning, _) => { 310 .font(theme::mono(12.0))
300 ui.label( 311 .color(theme::GOLD),
301 RichText::new("SPINNING…") 312 );
302 .font(theme::mono(12.0)) 313 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
303 .color(theme::GOLD),
304 );
305 }
306 _ => {
307 ui.label( 314 ui.label(
308 RichText::new("Place your bets") 315 RichText::new(format!("{} chips", demo.bets.chips.len()))
309 .font(theme::sans(12.0)) 316 .font(theme::mono(10.0))
310 .color(theme::TEXT_DIM), 317 .color(theme::TEXT_FAINT),
311 ); 318 );
312 } 319 });
313 } 320 }
314 ui.with_layout(Layout::right_to_left(Align::Center), |ui| { 321 _ => {
315 ui.label( 322 ui.label(
316 RichText::new(format!("{} chips", demo.bets.chips.len())) 323 RichText::new("Place your bets")
317 .font(theme::mono(10.0)) 324 .font(theme::sans(12.0))
318 .color(theme::TEXT_FAINT), 325 .color(theme::TEXT_DIM),
319 ); 326 );
320 }); 327 ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
328 ui.label(
329 RichText::new(format!("{} chips", demo.bets.chips.len()))
330 .font(theme::mono(10.0))
331 .color(theme::TEXT_FAINT),
332 );
333 });
334 }
321 }); 335 });
322} 336}
323 337
338/// What the round paid, and the net for the spin.
339fn payout(ui: &mut Ui, demo: &Demo) {
340 let winnings = demo.winnings.unwrap_or(0);
341 if winnings == 0 {
342 ui.label(
343 RichText::new("NO WIN")
344 .font(theme::mono(13.0))
345 .color(theme::TEXT_FAINT),
346 );
347 return;
348 }
349
350 let profit = winnings as i64 - demo.spent as i64;
351 ui.label(
352 RichText::new(format!("({})", theme::signed_money(profit)))
353 .font(theme::sans(11.0))
354 .color(if profit >= 0 { theme::WIN } else { theme::LOSS }),
355 );
356 ui.label(
357 RichText::new(theme::money(winnings as i64))
358 .font(theme::mono(15.0))
359 .color(theme::WIN),
360 );
361 ui.label(theme::label("Won"));
362}
363
324fn bet_row(ui: &mut Ui, name: &str, amount: u64) { 364fn bet_row(ui: &mut Ui, name: &str, amount: u64) {
325 ui.horizontal(|ui| { 365 ui.horizontal(|ui| {
326 ui.label( 366 ui.label(
@@ -338,8 +378,13 @@ fn bet_row(ui: &mut Ui, name: &str, amount: u64) {
338 }); 378 });
339} 379}
340 380
341fn chip_button(ui: &mut Ui, amount: u64, selected: bool) -> egui::Response { 381fn chip_button(ui: &mut Ui, amount: u64, selected: bool, enabled: bool) -> egui::Response {
342 let (rect, response) = ui.allocate_exact_size(Vec2::splat(46.0), Sense::click()); 382 let sense = if enabled {
383 Sense::click()
384 } else {
385 Sense::hover()
386 };
387 let (rect, response) = ui.allocate_exact_size(Vec2::splat(46.0), sense);
343 let center = rect.center(); 388 let center = rect.center();
344 let radius = 20.0; 389 let radius = 20.0;
345 let painter = ui.painter(); 390 let painter = ui.painter();
@@ -350,7 +395,11 @@ fn chip_button(ui: &mut Ui, amount: u64, selected: bool) -> egui::Response {
350 Stroke::new(if selected { 2.0 } else { 1.0 }, theme::ACCENT), 395 Stroke::new(if selected { 2.0 } else { 1.0 }, theme::ACCENT),
351 ); 396 );
352 } 397 }
353 roulette::paint_chip(painter, center, radius, amount); 398 if enabled {
399 roulette::paint_chip(painter, center, radius, amount);
400 } else {
401 roulette::paint_chip_preview(painter, center, radius, amount);
402 }
354 response 403 response
355} 404}
356 405
@@ -395,11 +444,20 @@ fn table(ui: &mut Ui, demo: &mut Demo) {
395 let side_pad = ((available.x - size.x) * 0.5).max(0.0); 444 let side_pad = ((available.x - size.x) * 0.5).max(0.0);
396 let top_pad = ((available.y - size.y - strip_height) * 0.5).max(0.0); 445 let top_pad = ((available.y - size.y - strip_height) * 0.5).max(0.0);
397 let chip = demo.chip; 446 let chip = demo.chip;
447 let placing = demo.phase != RoulettePhase::Spinning;
398 ui.add_space(top_pad); 448 ui.add_space(top_pad);
399 ui.horizontal(|ui| { 449 ui.horizontal(|ui| {
400 ui.add_space(side_pad); 450 ui.add_space(side_pad);
401 if let Some(target) = roulette::felt(ui, size, &view) { 451 match roulette::felt(ui, size, &view) {
402 demo.bets.add(target, chip); 452 Some(FeltAction::Place(target)) if placing => {
453 demo.begin_placing();
454 demo.bets.add(target, chip);
455 }
456 Some(FeltAction::Reduce(target)) if placing => {
457 demo.begin_placing();
458 demo.bets.reduce(&target, chip);
459 }
460 _ => {}
403 } 461 }
404 }); 462 });
405 463
@@ -422,7 +480,7 @@ fn table(ui: &mut Ui, demo: &mut Demo) {
422 ui.add_space(theme::SPACE_2); 480 ui.add_space(theme::SPACE_2);
423 ui.label( 481 ui.label(
424 RichText::new( 482 RichText::new(
425 "Click a pocket to place a chip · arrow keys move the cursor · space spins", 483 "Click to place · right or middle click takes back · C clears · space spins",
426 ) 484 )
427 .font(theme::sans(12.0)) 485 .font(theme::sans(12.0))
428 .color(theme::TEXT_FAINT), 486 .color(theme::TEXT_FAINT),
@@ -442,7 +500,6 @@ struct Spin {
442 500
443struct Demo { 501struct Demo {
444 bets: Bets, 502 bets: Bets,
445 cursor: Cursor,
446 chip: u64, 503 chip: u64,
447 phase: RoulettePhase, 504 phase: RoulettePhase,
448 number: Option<u8>, 505 number: Option<u8>,
@@ -450,6 +507,8 @@ struct Demo {
450 spin: Option<Spin>, 507 spin: Option<Spin>,
451 spins: usize, 508 spins: usize,
452 last: Vec<u8>, 509 last: Vec<u8>,
510 winnings: Option<u64>,
511 spent: u64,
453 balance: i64, 512 balance: i64,
454 host: &'static str, 513 host: &'static str,
455 ping_ms: u32, 514 ping_ms: u32,
@@ -471,8 +530,6 @@ impl Demo {
471 530
472 Self { 531 Self {
473 bets, 532 bets,
474 // A split with no chip on it, so the fine lattice cursor shows.
475 cursor: Cursor::Lattice { x: 18, y: 3 },
476 chip: 25, 533 chip: 25,
477 phase: RoulettePhase::Placing, 534 phase: RoulettePhase::Placing,
478 number: None, 535 number: None,
@@ -480,6 +537,8 @@ impl Demo {
480 spin: None, 537 spin: None,
481 spins: 0, 538 spins: 0,
482 last: Vec::new(), 539 last: Vec::new(),
540 winnings: None,
541 spent: 0,
483 balance: 1000, 542 balance: 1000,
484 host: "127.0.0.1:11525", 543 host: "127.0.0.1:11525",
485 ping_ms: 41, 544 ping_ms: 41,
@@ -494,15 +553,24 @@ impl Demo {
494 balance: Some(self.balance), 553 balance: Some(self.balance),
495 chip: self.chip, 554 chip: self.chip,
496 bets: self.bets.clone(), 555 bets: self.bets.clone(),
497 cursor: self.cursor, 556 cursor: Cursor::default(),
498 phase: self.phase, 557 phase: self.phase,
499 number: self.number, 558 number: self.number,
500 color: self.number.map(|number| wheel_color(number).to_string()), 559 color: self.number.map(|number| wheel_color(number).to_string()),
501 winnings: None, 560 winnings: self.winnings,
502 error: None, 561 error: None,
503 } 562 }
504 } 563 }
505 564
565 /// A bet change after a result starts the next round: the wheel stays
566 /// where it stopped, but the payout and the winning highlight do not.
567 fn begin_placing(&mut self) {
568 if self.phase == RoulettePhase::Result {
569 self.phase = RoulettePhase::Placing;
570 self.winnings = None;
571 }
572 }
573
506 fn tick(&mut self, ctx: &Context) { 574 fn tick(&mut self, ctx: &Context) {
507 let now = ctx.input(|input| input.time); 575 let now = ctx.input(|input| input.time);
508 let mut finished = None; 576 let mut finished = None;
@@ -519,52 +587,37 @@ impl Demo {
519 self.spin = None; 587 self.spin = None;
520 self.phase = RoulettePhase::Result; 588 self.phase = RoulettePhase::Result;
521 self.number = Some(number); 589 self.number = Some(number);
590 self.winnings = Some(self.bets.winnings(number));
522 self.last.insert(0, number); 591 self.last.insert(0, number);
523 self.last.truncate(12); 592 self.last.truncate(12);
524 } 593 }
525 } 594 }
526 595
596 /// Pointer-first: the felt reads the mouse. The keyboard keeps the two
597 /// verbs that have no pointer equivalent in profile (spin and clear).
527 fn keys(&mut self, ctx: &Context) { 598 fn keys(&mut self, ctx: &Context) {
528 if self.phase == RoulettePhase::Spinning { 599 if self.phase == RoulettePhase::Spinning {
529 return; 600 return;
530 } 601 }
531 let (left, right, up, down) = ctx.input(|input| {
532 (
533 input.key_pressed(Key::ArrowLeft),
534 input.key_pressed(Key::ArrowRight),
535 input.key_pressed(Key::ArrowUp),
536 input.key_pressed(Key::ArrowDown),
537 )
538 });
539 for (pressed, direction) in [
540 (left, Direction::Left),
541 (right, Direction::Right),
542 (up, Direction::Up),
543 (down, Direction::Down),
544 ] {
545 if pressed {
546 self.cursor = self.cursor.moved(direction);
547 }
548 }
549 if ctx.memory(|memory| memory.focused().is_none()) { 602 if ctx.memory(|memory| memory.focused().is_none()) {
550 if ctx.input(|input| input.key_pressed(Key::Enter)) {
551 if let Some(target) = self.cursor.target() {
552 let chip = self.chip;
553 self.bets.add(target, chip);
554 }
555 }
556 if ctx.input(|input| input.key_pressed(Key::Space)) { 603 if ctx.input(|input| input.key_pressed(Key::Space)) {
557 self.start_spin(ctx); 604 self.start_spin(ctx);
558 } 605 }
606 if ctx.input(|input| input.key_pressed(Key::C)) {
607 self.begin_placing();
608 self.bets = Bets::default();
609 }
559 } 610 }
560 } 611 }
561 612
562 fn start_spin(&mut self, ctx: &Context) { 613 fn start_spin(&mut self, ctx: &Context) {
563 if self.phase == RoulettePhase::Spinning { 614 if self.phase == RoulettePhase::Spinning || self.bets.is_empty() {
564 return; 615 return;
565 } 616 }
566 let target = DEMO_TARGETS[self.spins % DEMO_TARGETS.len()]; 617 let target = DEMO_TARGETS[self.spins % DEMO_TARGETS.len()];
567 self.spins += 1; 618 self.spins += 1;
619 self.spent = self.bets.total();
620 self.winnings = None;
568 621
569 let segment = TAU / WHEEL.len() as f32; 622 let segment = TAU / WHEEL.len() as f32;
570 let index = WHEEL 623 let index = WHEEL