diff options
Diffstat (limited to '')
| -rw-r--r-- | gui/src/app.rs | 253 |
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 | }; |
| 13 | use zagan_bet::client::games::roulette::{Cursor, Direction}; | 13 | use zagan_bet::client::games::roulette::Cursor; |
| 14 | use zagan_bet::client::view::{RoulettePhase, RouletteView}; | 14 | use zagan_bet::client::view::{RoulettePhase, RouletteView}; |
| 15 | use zagan_bet::roulette::{BetTarget, Bets, Outside, WHEEL, color as wheel_color}; | 15 | use zagan_bet::roulette::{BetTarget, Bets, Outside, WHEEL, color as wheel_color}; |
| 16 | use zagan_bet::version::VERSION; | 16 | use zagan_bet::version::VERSION; |
| 17 | 17 | ||
| 18 | use crate::roulette::FeltAction; | ||
| 18 | use crate::{roulette, theme}; | 19 | use crate::{roulette, theme}; |
| 19 | 20 | ||
| 20 | const HEADER_HEIGHT: f32 = 64.0; | 21 | const 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 | ||
| 283 | fn result_line(ui: &mut Ui, demo: &Demo) { | 294 | fn 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. | ||
| 339 | fn 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 | |||
| 324 | fn bet_row(ui: &mut Ui, name: &str, amount: u64) { | 364 | fn 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 | ||
| 341 | fn chip_button(ui: &mut Ui, amount: u64, selected: bool) -> egui::Response { | 381 | fn 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 | ||
| 443 | struct Demo { | 501 | struct 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 |
