目次

三並べ (Tic-Tac-Toe)

作成日: 2022-12-14 (水)

src/main.rs

use eframe::egui;

fn main() {
    let options = eframe::NativeOptions {
        initial_window_size: Some(egui::vec2(500.0, 500.0)),
        ..Default::default()
    };
    eframe::run_native(
        "Tic-Tac-Toe",
        options,
        Box::new(|_cc| Box::new(MyApp::default())),
    );
}

struct MyApp {
    turn: Player,
    pieces: Vec<Piece>,
    square_size: f32,
}

impl Default for MyApp {
    fn default() -> Self {
        Self {
            turn: Player::O,
            pieces: Vec::new(),
            square_size: 100.0,
        }
    }
}

impl eframe::App for MyApp {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        let winner = winner(&self.pieces);
        let draw_match = self.pieces.len() == 9 && winner == None;

        if winner == None && !draw_match {
            // step the game
            if let Some(pos) = self.clicked_position(ctx) {
                let board_pos = egui::pos2(
                    (pos.x / self.square_size).floor(),
                    (pos.y / self.square_size).floor(),
                );

                let open_square = self.pieces.iter().all(|p| {
                    p.place.x as i32 != board_pos.x as i32 || p.place.y as i32 != board_pos.y as i32
                });
                let on_board = board_pos.x >= 0.0
                    && board_pos.x <= 2.0
                    && board_pos.y >= 0.0
                    && board_pos.y <= 2.0;

                if open_square && on_board {
                    self.pieces.push(Piece {
                        player: self.turn,
                        place: board_pos,
                    });

                    // next turn
                    self.turn = if self.turn == Player::O {
                        Player::X
                    } else {
                        Player::O
                    };
                }
            }
        } else {
            // wait for right button clicked to restart the game
            if ctx.input().pointer.secondary_clicked() {
                *self = Default::default();
            }
        }

        egui::CentralPanel::default().show(ctx, |ui| {
            self.draw_board(ui.painter(), self.square_size);
            self.draw_pieces(ui.painter(), self.square_size);

            if let Some(player_wins) = winner {
                self.draw_winner(ui.painter(), player_wins);
            }

            if draw_match {
                self.draw_draw_match(ui.painter());
            }
        });
    }
}

impl MyApp {
    fn clicked_position(&self, ctx: &egui::Context) -> Option<egui::Pos2> {
        // avoid dead lock
        // [[https://docs.rs/egui/latest/egui/struct.Context.html#method.input]]
        if let Some(pos) = { ctx.input().pointer.hover_pos() } {
            if ctx.input().pointer.any_click() {
                return Some(pos);
            }
        }
        None
    }

    fn draw_board(&self, painter: &egui::Painter, square_size: f32) {
        for i in 0..3 {
            for j in 0..3 {
                let rect = egui::Rect::from_min_size(
                    egui::Pos2::new(j as f32 * square_size, i as f32 * square_size),
                    egui::vec2(square_size, square_size),
                );
                let rounding = 0.0;
                let color = egui::Color32::GREEN;
                let stroke = egui::Stroke::new(2.0, egui::Color32::LIGHT_GREEN);
                painter.rect(rect, rounding, color, stroke);
            }
        }
    }

    fn draw_pieces(&self, painter: &egui::Painter, square_size: f32) {
        for piece in &self.pieces {
            piece.draw(painter, square_size);
        }
    }

    fn draw_winner(&self, painter: &egui::Painter, winner: Player) {
        painter.text(
            painter.clip_rect().center(),
            egui::Align2::CENTER_CENTER,
            format!(
                "Player {} wins!",
                if winner == Player::O { "O" } else { "X" }
            ),
            egui::FontId::proportional(60.0),
            egui::Color32::RED,
        );
    }

    fn draw_draw_match(&self, painter: &egui::Painter) {
        painter.text(
            painter.clip_rect().center(),
            egui::Align2::CENTER_CENTER,
            "Draw..",
            egui::FontId::proportional(60.0),
            egui::Color32::BLUE,
        );
    }
}

#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
enum Player {
    O,
    X,
}

struct Piece {
    player: Player,
    place: egui::Pos2,
}

impl Piece {
    fn draw(&self, painter: &egui::Painter, square_size: f32) {
        let p = egui::pos2(self.place.x * square_size, self.place.y * square_size);
        match self.player {
            Player::O => {
                painter.circle_stroke(
                    egui::pos2(p.x + square_size / 2.0, p.y + square_size / 2.0),
                    square_size * 0.3,
                    (3.0, egui::Color32::WHITE),
                );
            }
            Player::X => {
                painter.line_segment(
                    [
                        egui::pos2(p.x + square_size * 0.2, p.y + square_size * 0.2),
                        egui::pos2(p.x + square_size * 0.8, p.y + square_size * 0.8),
                    ],
                    (3.0, egui::Color32::BLACK),
                );
                painter.line_segment(
                    [
                        egui::pos2(p.x + square_size * 0.2, p.y + square_size * 0.8),
                        egui::pos2(p.x + square_size * 0.8, p.y + square_size * 0.2),
                    ],
                    (3.0, egui::Color32::BLACK),
                );
            }
        }
    }
}

fn winner(pieces: &[Piece]) -> Option<Player> {
    for player in [Player::O, Player::X] {
        for i in 0..3 {
            if 3 == pieces
                .iter()
                .filter(|p| p.player == player)
                .filter(|p| p.place.x as i32 == i)
                .count()
            {
                return Some(player);
            }
        }

        for i in 0..3 {
            if 3 == pieces
                .iter()
                .filter(|p| p.player == player)
                .filter(|p| p.place.y as i32 == i)
                .count()
            {
                return Some(player);
            }
        }

        if 3 == pieces
            .iter()
            .filter(|p| p.player == player)
            .filter(|p| p.place.x as i32 == p.place.y as i32)
            .count()
        {
            return Some(player);
        }

        if 3 == pieces
            .iter()
            .filter(|p| p.player == player)
            .filter(|p| 2 - p.place.x as i32 == p.place.y as i32)
            .count()
        {
            return Some(player);
        }
    }

    None
}

Cargo.toml

[package]
name = "egui_tictactoe"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
eframe = "0.20.1"