numples 1.2.0

Yet another colourful sudoku playing game.
Documentation
use std::f32::consts::TAU;

use bevy::prelude::*;
use rand::{Rng, RngCore};
use serde::{Deserialize, Serialize};

use crate::tween::Tween;
use crate::consts::CELL_SIZE;
use crate::{consts::CANDIDATE_SIZE, gameplay::Gameplay};

use super::anim::Anim;
use super::board_cell::BoardCell;
use super::colors::Colors;
use super::error_cell::ErrorCell;
use super::shapes::Shapes;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Cell(u16);

impl Default for Cell {
    fn default() -> Self {
        Cell(0b0001_1111_1111_0000)
    }
}

impl Cell {

    pub fn value(&self) -> u8 {
        (self.0 & 0b0000_0000_0000_1111) as u8
    }

    pub fn set_value(&mut self, value: u8) -> bool {
        let current = self.value();

        if value > 9 || value == current {
            false

        } else if value == 0 {
            self.0 &= 0b0001_1111_1111_0000;
            true

        } else if self.is_candidate_set(value) && current == 0 {
            self.0 |= value as u16;
            true

        } else {
            false
        }
    }

    pub fn toggle_candidate(&mut self, value: u8) -> bool {
        if value == 0 || value > 9 {
            false

        } else if self.is_candidate_set(value) {
            self.do_clean_candidate(value)

        } else {
            self.do_set_candidate(value)
        }
    }

    pub fn is_candidate_set(&self, value: u8) -> bool {
        if value == 0 || value > 9 {
            false
        } else {
            self.0 & (1 << (value + 3)) != 0
        }
    }

    pub fn clean_candidate(&mut self, value: u8) -> bool {
        if self.is_candidate_set(value) {
            self.do_clean_candidate(value)
        } else {
            false
        }
    }

    fn do_set_candidate(&mut self, value: u8) -> bool {
        if value == 0 || value > 9 {
            false
        } else {
            self.0 |= 1 << (value + 3);
            true
        }
    }

    fn do_clean_candidate(&mut self, value: u8) -> bool {
        if value == 0 || value > 9 {
            false
        } else {
            self.0 &= 0b0001_1111_1111_1111 ^ (1 << (value + 3));
            true
        }
    }

    pub fn render(&self,
        x: f32, y: f32,
        ix: usize, iy: usize,
        commands: &mut Commands,
        shapes: &Res<Shapes>,
        colors: &Res<Colors>,
        anims: Vec<Anim>,
    ) {
        let value = self.value();
        if value == 0 {
            self.render_candidates(x, y, ix, iy, commands, shapes, colors, anims, false);
        } else {
            self.render_value(value, x, y, ix, iy, commands, shapes, colors, anims);
        }
    }

    fn render_value(
        &self,
        value: u8,
        xf: f32, yf: f32,
        ix: usize, iy: usize,
        commands: &mut Commands,
        shapes: &Res<Shapes>,
        colors: &Res<Colors>,
        anims: Vec<Anim>,
    ) {
        let mut subcommands = commands.spawn((
            BoardCell,
            Gameplay,
            Mesh2d(shapes.cell.clone_weak()),
            MeshMaterial2d(colors.get(value as usize).clone_weak()),
        ));

        if let Some(&Anim::Set { value, .. }) = anims.first() {
            let x = xf + (((value - 1) % 3) as f32 - 1.0) * CANDIDATE_SIZE;
            let y = yf + (((value - 1) / 3) as f32 - 1.0) * CANDIDATE_SIZE;
            let scale = CANDIDATE_SIZE / CELL_SIZE;
            let tween: Tween = Tween::value()
                .xy(x, y)
                .scale(scale)
                .xyf(xf, yf)
                .into();
            let mut scale = Vec3::splat(scale);
            scale.z = 1.0;
            subcommands.insert(
                Transform {
                    translation: Vec3::new(x, y, 4.0),
                    scale,
                    ..default()
                }
            );
            subcommands.insert(tween);
            self.render_candidates(xf, yf, ix, iy, commands, shapes, colors, anims, true);

        } else {
            subcommands.insert(Transform::from_xyz(xf, yf, 1.0));
        }
    }

    fn render_candidates(
        &self,
        x: f32, y: f32,
        ix: usize, iy: usize,
        commands: &mut Commands,
        shapes: &Res<Shapes>,
        colors: &Res<Colors>,
        anims: Vec<Anim>,
        delete: bool,
    ) {
        let shape = &shapes.cell_candidate;

        for i in 0..9 {
            let ax = x + ((i % 3) as f32 - 1.0) * CANDIDATE_SIZE;
            let ay = y + ((i / 3) as f32 - 1.0) * CANDIDATE_SIZE;

            if self.is_candidate_set(i as u8 + 1) {
                let color = colors.get(i + 1);
                let mut subcommand = commands.spawn((
                    BoardCell,
                    Gameplay,
                    Mesh2d(shape.clone_weak()),
                    MeshMaterial2d(color.clone_weak()),
                    Transform::from_xyz(ax, ay, 1.0),
                ));
                if delete {
                    subcommand.insert(Tween::delete());
                }
                for &anim in anims.iter() {
                    if let Anim::SetCandidate { x: aix, y: aiy, value } = anim {
                        if ix == aix && iy == aiy && value as usize == i + 1 {
                            let tween: Tween = Tween::value()
                                .xy(x, y)
                                .xyf(ax, ay)
                                .scale(CELL_SIZE / CANDIDATE_SIZE)
                                .scalef(1.0)
                                .into();
                            subcommand.insert(tween);
                        }
                    }
                }

            } else {
                for &anim in anims.iter() {
                    if let Anim::UnsetCandidate { x: aix, y: aiy, value } = anim {
                        if ix == aix && iy == aiy && value as usize == i + 1 {
                            let color = colors.get(value as usize);
                            let mut rng = rand::rng();
                            let rays = rng.next_u32() as usize;
                            let rays = rays % 10 + 3;
                            let rotation: f32 = rng.random();
                            let rotation = rotation * TAU / (rays as f32);
                            for anglei in 0..rays {
                                let angle = rotation + TAU * anglei as f32 / (rays as f32);
                                let xf = 100.0 * angle.cos() + ax;
                                let yf = 100.0 * angle.sin() + ay;
                                let tween: Tween = Tween::value()
                                    .xy(ax, ay)
                                    .scale(1.0)
                                    .scalef(0.0)
                                    .xyf(xf, yf)
                                    .remove_entity(true)
                                    .into();

                                commands.spawn((
                                    BoardCell,
                                    Gameplay,
                                    Mesh2d(shape.clone_weak()),
                                    MeshMaterial2d(color.clone_weak()),
                                    Transform::from_xyz(ax, ay, 2.0),
                                    tween,
                                ));
                            }
                        }
                    }
                }
            }
        }

        if self.0 == 0 {
            let shape = &shapes.rect;
            commands.spawn((
                BoardCell,
                Gameplay,
                ErrorCell,
                Mesh2d(shape.clone_weak()),
                MeshMaterial2d(colors.get(1).clone_weak()),
                Transform::from_xyz(x, y, 1.0),
            ));
        }

        if let Some(Anim::Unset { value, .. }) = anims.first() {
            let xf = x + (((value - 1) % 3) as f32 - 1.0) * CANDIDATE_SIZE;
            let yf = y + (((value - 1) / 3) as f32 - 1.0) * CANDIDATE_SIZE;
            let scalef = CANDIDATE_SIZE / CELL_SIZE;
            let tween: Tween = Tween::value()
                .xy(x, y)
                .scale(1.0)
                .scalef(scalef)
                .xyf(xf, yf)
                .remove_entity(true)
                .into();
            commands.spawn((
                BoardCell,
                Gameplay,
                Mesh2d(shapes.cell.clone_weak()),
                MeshMaterial2d(colors.get(*value as usize).clone_weak()),
                Transform {
                    translation: Vec3::new(x, y, 1.0),
                    scale: Vec3::ONE,
                    ..default()
                },
                tween,
            ));
        }
    }
}

impl From<&Cell> for u16 {

    fn from(cell: &Cell) -> Self {
        cell.0
    }
}