Added texture and board
This commit is contained in:
parent
90a6ae7716
commit
a7ada984c5
@ -2,6 +2,7 @@
|
||||
name = "sol_chess"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
default-run = "sol_chess"
|
||||
|
||||
[dependencies]
|
||||
argh = "0.1.13"
|
||||
|
55
README.md
55
README.md
@ -6,14 +6,18 @@ Goal: Generate 'hard' puzzles.
|
||||
|
||||
- Install Rust from [here](https://www.rust-lang.org/tools/install).
|
||||
- Run `cargo install --git https://github.com/cool-mist/sol_chess` to install the tool.
|
||||
- Run `sol_chess --help` to see the options.
|
||||
|
||||
## Usage
|
||||
|
||||
- Run `sol_chess` to start a windowed GUI game.
|
||||
- Run `sol_cli` to start the CLI tool.
|
||||
|
||||
## CLI Usage
|
||||
|
||||
- Generate a puzzle
|
||||
|
||||
```bash
|
||||
$ sol_chess -g -n 6
|
||||
$ sol_cli -g -n 6
|
||||
Generating a puzzle with 6 pieces with a maximum of 5 solutions
|
||||
Total attempts: 7
|
||||
Total pieces placed: 71
|
||||
@ -27,12 +31,17 @@ Generating a puzzle with 6 pieces with a maximum of 5 solutions
|
||||
♔ . ♘ ♙
|
||||
|
||||
. . . .
|
||||
|
||||
|
||||
id: 202859896274992
|
||||
```
|
||||
|
||||
- Solve a puzzle
|
||||
- Solve a puzzle by ID, or by board string
|
||||
|
||||
```bash
|
||||
$ sol_chess -- --solve N...P.R.K.NP....
|
||||
$ sol_cli --solve 202859896274992
|
||||
$ sol_cli --solve-board N...P.R.K.NP....
|
||||
|
||||
♘ . . .
|
||||
|
||||
♙ . ♖ .
|
||||
@ -42,39 +51,15 @@ $ sol_chess -- --solve N...P.R.K.NP....
|
||||
. . . .
|
||||
|
||||
|
||||
id: 202859896274992
|
||||
|
||||
Found 3 solutions
|
||||
1. Rc3 -> a3
|
||||
2. Ra3 -> a4
|
||||
3. Ra4 -> a2
|
||||
4. Ra2 -> c2
|
||||
5. Rc2 -> d2
|
||||
```
|
||||
1. RxNc2
|
||||
2. RxPd2
|
||||
3. RxKa2
|
||||
4. RxPa3
|
||||
5. RxNa4
|
||||
|
||||
- Generate and solve a puzzle
|
||||
|
||||
```bash
|
||||
$ sol_chess -g -n 6 --print
|
||||
Generating a puzzle with 6 pieces with a maximum of 5 solutions
|
||||
Total attempts: 4
|
||||
Total pieces placed: 34
|
||||
Success pieces placed: 24
|
||||
Total time (ms): 38
|
||||
|
||||
. . ♙ .
|
||||
|
||||
♕ . . ♘
|
||||
|
||||
. . . .
|
||||
|
||||
♗ ♖ . ♘
|
||||
|
||||
|
||||
Found 5 solutions
|
||||
1. Rb1 -> a1
|
||||
2. Ra1 -> d1
|
||||
3. Rd1 -> d3
|
||||
4. Qa3 -> d3
|
||||
5. Qd3 -> c4
|
||||
```
|
||||
|
||||
## Heuristics of current algorithm
|
||||
|
BIN
assets/pieces.png
Normal file
BIN
assets/pieces.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 251 KiB |
106
src/bin/sol_cli.rs
Normal file
106
src/bin/sol_cli.rs
Normal file
@ -0,0 +1,106 @@
|
||||
use argh::FromArgs;
|
||||
|
||||
use sol_chess::board::Board;
|
||||
use sol_chess::generator;
|
||||
use sol_chess::solver::Solver;
|
||||
|
||||
fn main() {
|
||||
let args: Args = argh::from_env();
|
||||
|
||||
if args.generate {
|
||||
let puzzle = generate_puzzle(args.num_pieces, args.solutions);
|
||||
let Some(board) = puzzle else {
|
||||
return;
|
||||
};
|
||||
|
||||
board.pretty_print();
|
||||
if args.print {
|
||||
solve_puzzle(board);
|
||||
}
|
||||
} else {
|
||||
let board = if let Some(board_string) = args.solve_board {
|
||||
Board::from_string(board_string)
|
||||
} else if let Some(board_id) = args.solve {
|
||||
Board::from_id(board_id)
|
||||
} else {
|
||||
println!("Use --help to see available options");
|
||||
return;
|
||||
};
|
||||
let Ok(board) = board else {
|
||||
println!("Invalid board string/id");
|
||||
return;
|
||||
};
|
||||
board.pretty_print();
|
||||
solve_puzzle(board);
|
||||
}
|
||||
}
|
||||
|
||||
fn solve_puzzle(board: Board) {
|
||||
let solutions = Solver::new(board).solve();
|
||||
if solutions.len() == 0 {
|
||||
println!("No solutions found");
|
||||
return;
|
||||
}
|
||||
println!("Found {} solutions", solutions.len());
|
||||
let solution = solutions.first().unwrap();
|
||||
let mut idx = 0;
|
||||
solution.iter().for_each(|m| {
|
||||
idx += 1;
|
||||
println!("{}. {}", idx, m.notation());
|
||||
});
|
||||
}
|
||||
|
||||
fn generate_puzzle(num_pieces: Option<u32>, num_solutions: Option<u32>) -> Option<Board> {
|
||||
let mut num_pieces = num_pieces.unwrap_or(5);
|
||||
if num_pieces < 2 {
|
||||
num_pieces = 2;
|
||||
}
|
||||
|
||||
let mut num_solutions = num_solutions.unwrap_or(5);
|
||||
if num_solutions < 1 {
|
||||
num_solutions = 5;
|
||||
}
|
||||
|
||||
println!(
|
||||
"Generating a puzzle with {} pieces with a maximum of {} solutions",
|
||||
num_pieces, num_solutions
|
||||
);
|
||||
let gen = generator::generate(num_pieces, num_solutions);
|
||||
gen.print_stats();
|
||||
|
||||
let Some(board) = gen.board() else {
|
||||
println!("Failed to generate a puzzle, try again");
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(board)
|
||||
}
|
||||
|
||||
/// Solitaire Chess puzzle generator and solver
|
||||
/// - v0.0.1 cool-mist
|
||||
#[derive(FromArgs)]
|
||||
struct Args {
|
||||
#[argh(switch, short = 'g')]
|
||||
/// generate a puzzle
|
||||
generate: bool,
|
||||
|
||||
#[argh(option, short = 'n')]
|
||||
/// number of pieces to place on the board while generating a puzzle
|
||||
num_pieces: Option<u32>,
|
||||
|
||||
#[argh(option)]
|
||||
/// maximum number of solutions allowed for the generated puzzle. atleast 1. defaults to 5
|
||||
solutions: Option<u32>,
|
||||
|
||||
#[argh(switch)]
|
||||
/// print the solution. When solving a puzzle, this is always set to true
|
||||
print: bool,
|
||||
|
||||
#[argh(option, short = 's')]
|
||||
/// the id of the board to solve
|
||||
solve: Option<u128>,
|
||||
|
||||
#[argh(option)]
|
||||
/// the board to solve in board representation
|
||||
solve_board: Option<String>,
|
||||
}
|
@ -1,22 +1,22 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Display,
|
||||
mem,
|
||||
};
|
||||
pub mod cmove;
|
||||
mod constants;
|
||||
pub mod errors;
|
||||
pub mod piece;
|
||||
pub mod square;
|
||||
|
||||
use super::{
|
||||
cmove::CMove,
|
||||
constants::BOARD_SIZE,
|
||||
errors::SError,
|
||||
piece::Piece,
|
||||
square::{Square, SquarePair},
|
||||
};
|
||||
use std::{collections::HashSet, mem};
|
||||
|
||||
use cmove::CMove;
|
||||
use constants::BOARD_SIZE;
|
||||
use errors::SError;
|
||||
use piece::Piece;
|
||||
use square::{Square, SquarePair};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Board {
|
||||
pub(crate) cells: [[Option<Piece>; BOARD_SIZE]; BOARD_SIZE],
|
||||
pub(crate) legal_moves: HashSet<CMove>,
|
||||
pub(crate) game_state: GameState,
|
||||
pub struct Board {
|
||||
pub cells: [[Option<Piece>; BOARD_SIZE]; BOARD_SIZE],
|
||||
pub legal_moves: HashSet<CMove>,
|
||||
pub game_state: GameState,
|
||||
pieces_remaining: u8,
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ pub enum GameState {
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub(crate) fn new() -> Self {
|
||||
pub fn new() -> Self {
|
||||
Board {
|
||||
cells: [[None; BOARD_SIZE]; BOARD_SIZE],
|
||||
legal_moves: HashSet::new(),
|
||||
@ -38,7 +38,7 @@ impl Board {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn from_id(board_id: u128) -> Result<Self, SError> {
|
||||
pub fn from_id(board_id: u128) -> Result<Self, SError> {
|
||||
let mut board = Board::new();
|
||||
let mut working = board_id;
|
||||
for i in (0..BOARD_SIZE).rev() {
|
||||
@ -53,14 +53,12 @@ impl Board {
|
||||
Ok(board)
|
||||
}
|
||||
|
||||
pub(crate) fn from_string(board_string: String) -> Result<Self, SError> {
|
||||
pub fn from_string(board_string: String) -> Result<Self, SError> {
|
||||
if board_string.chars().count() != 16 {
|
||||
return Err(SError::InvalidBoard);
|
||||
}
|
||||
|
||||
let mut board = Board::new();
|
||||
let mut file = 0;
|
||||
let mut rank = 0;
|
||||
let mut chars = board_string.chars();
|
||||
for r in 0..BOARD_SIZE {
|
||||
for f in 0..BOARD_SIZE {
|
||||
@ -83,7 +81,7 @@ impl Board {
|
||||
Ok(board)
|
||||
}
|
||||
|
||||
pub(crate) fn set(&mut self, square: Square) -> Option<Piece> {
|
||||
pub fn set(&mut self, square: Square) -> Option<Piece> {
|
||||
let new_is_occuppied = square.piece.is_some();
|
||||
let existing = mem::replace(&mut self.cells[square.file][square.rank], square.piece);
|
||||
|
||||
@ -101,7 +99,7 @@ impl Board {
|
||||
existing
|
||||
}
|
||||
|
||||
pub(crate) fn make_move(&mut self, mv: CMove) -> Option<CMove> {
|
||||
pub fn make_move(&mut self, mv: CMove) -> Option<CMove> {
|
||||
if !self.legal_moves.contains(&mv) {
|
||||
println!("Invalid move - {}", mv.notation());
|
||||
println!("Legal moves - ");
|
||||
@ -112,14 +110,14 @@ impl Board {
|
||||
}
|
||||
|
||||
let from_piece = mem::replace(&mut self.cells[mv.from.file][mv.from.rank], None);
|
||||
mem::replace(&mut self.cells[mv.to.file][mv.to.rank], from_piece);
|
||||
self.cells[mv.to.file][mv.to.rank] = from_piece;
|
||||
|
||||
self.pieces_remaining -= 1;
|
||||
self.board_state_changed();
|
||||
Some(mv)
|
||||
}
|
||||
|
||||
pub(crate) fn empty_squares(&self) -> Vec<Square> {
|
||||
pub fn empty_squares(&self) -> Vec<Square> {
|
||||
let mut empty_squares = Vec::new();
|
||||
for file in 0..BOARD_SIZE {
|
||||
for rank in 0..BOARD_SIZE {
|
||||
@ -131,12 +129,12 @@ impl Board {
|
||||
empty_squares
|
||||
}
|
||||
|
||||
pub(crate) fn pretty_print(&self) {
|
||||
pub fn pretty_print(&self) {
|
||||
println!("{}", self.print(true));
|
||||
println!("{:^40}\n", format!("id: {}", self.id()));
|
||||
}
|
||||
|
||||
pub(crate) fn id(&self) -> u128 {
|
||||
pub fn id(&self) -> u128 {
|
||||
let mut res: u128 = 0;
|
||||
|
||||
for i in 0..BOARD_SIZE {
|
||||
@ -256,8 +254,6 @@ impl Board {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn calc_game_state(&mut self) {
|
||||
@ -356,12 +352,20 @@ fn get_square_for_display(piece: &Option<Piece>, pretty: bool) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::engine::cmove::mv;
|
||||
use crate::engine::piece::p;
|
||||
use crate::engine::square::sq;
|
||||
|
||||
use super::*;
|
||||
|
||||
macro_rules! sq {
|
||||
($sq:literal) => {
|
||||
Square::parse($sq)
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! mv {
|
||||
($from:literal, $to:literal) => {{
|
||||
CMove::new(sq!($from), sq!($to))
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! validate_board {
|
||||
($board:expr, $row1:literal, $row2:literal, $row3:literal, $row4:literal) => {
|
||||
let printed = $board.print(false);
|
@ -1,19 +1,19 @@
|
||||
use super::{board::Board, piece::Piece, square::Square};
|
||||
use super::{piece::Piece, square::Square};
|
||||
|
||||
#[derive(PartialEq, Hash, Eq, Clone)]
|
||||
pub(crate) struct CMove {
|
||||
pub(crate) from_piece: Piece,
|
||||
pub(crate) from: Square,
|
||||
pub(crate) to_piece: Piece,
|
||||
pub(crate) to: Square,
|
||||
pub struct CMove {
|
||||
pub from_piece: Piece,
|
||||
pub from: Square,
|
||||
pub to_piece: Piece,
|
||||
pub to: Square,
|
||||
|
||||
// Used to disambiguate when looking at notation
|
||||
disambig: String,
|
||||
}
|
||||
|
||||
impl CMove {
|
||||
pub(crate) fn new(from: Square, to: Square) -> Self {
|
||||
let qualifier = String::from("");
|
||||
pub fn new(from: Square, to: Square) -> Self {
|
||||
let disambig = String::from("");
|
||||
let from_piece = from.piece.expect("Trying to move a blank");
|
||||
let to_piece = to.piece.expect("Trying to capture a blank");
|
||||
CMove {
|
||||
@ -21,11 +21,11 @@ impl CMove {
|
||||
from,
|
||||
to_piece,
|
||||
to,
|
||||
disambig: "".to_string(),
|
||||
disambig
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn notation(&self) -> String {
|
||||
pub fn notation(&self) -> String {
|
||||
let piece_qualifier = match &self.from_piece {
|
||||
Piece::Pawn => self.from.file_notation(),
|
||||
p => p.notation(),
|
||||
@ -38,11 +38,3 @@ impl CMove {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! mv {
|
||||
($from:literal, $to:literal) => {{
|
||||
CMove::new(sq!($from), sq!($to))
|
||||
}};
|
||||
}
|
||||
|
||||
pub(crate) use mv;
|
@ -1,4 +1,4 @@
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum SError {
|
||||
pub enum SError {
|
||||
InvalidBoard,
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
#[derive(Clone, Eq, Hash, Copy, Debug, PartialEq)]
|
||||
pub(crate) enum Piece {
|
||||
pub enum Piece {
|
||||
King,
|
||||
Queen,
|
||||
Bishop,
|
||||
@ -9,7 +9,7 @@ pub(crate) enum Piece {
|
||||
}
|
||||
|
||||
impl Piece {
|
||||
pub(crate) fn parse(piece: &str) -> Option<Self> {
|
||||
pub fn parse(piece: &str) -> Option<Self> {
|
||||
match piece {
|
||||
"K" => Some(Piece::King),
|
||||
"Q" => Some(Piece::Queen),
|
||||
@ -22,7 +22,7 @@ impl Piece {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn notation(&self) -> String {
|
||||
pub fn notation(&self) -> String {
|
||||
let n = match self {
|
||||
Piece::King => "K",
|
||||
Piece::Queen => "Q",
|
||||
@ -35,7 +35,7 @@ impl Piece {
|
||||
n.to_string()
|
||||
}
|
||||
|
||||
pub(crate) fn pretty(&self) -> String {
|
||||
pub fn pretty(&self) -> String {
|
||||
let n = match self {
|
||||
Piece::King => "♔",
|
||||
Piece::Queen => "♕",
|
||||
@ -49,18 +49,16 @@ impl Piece {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! p {
|
||||
($piece:literal) => {
|
||||
Piece::parse($piece)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use p;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
macro_rules! p {
|
||||
($piece:literal) => {
|
||||
Piece::parse($piece)
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_piece_parse() {
|
||||
assert_eq!(p!("K"), Some(Piece::King));
|
@ -1,34 +1,33 @@
|
||||
use crate::engine::constants::BOARD_SIZE;
|
||||
|
||||
use super::constants::BOARD_SIZE;
|
||||
use super::piece::Piece;
|
||||
use core::fmt;
|
||||
|
||||
#[derive(Clone, Eq, Hash, PartialEq)]
|
||||
pub(crate) struct Square {
|
||||
pub struct Square {
|
||||
// a = 0, b = 1, c = 2, d = 3 and so on.
|
||||
pub(crate) file: usize,
|
||||
pub file: usize,
|
||||
|
||||
// 1 = 0, 2 = 1, 3 = 2, 4 = 3 and so on.
|
||||
pub(crate) rank: usize,
|
||||
pub rank: usize,
|
||||
|
||||
pub(crate) piece: Option<Piece>,
|
||||
pub piece: Option<Piece>,
|
||||
}
|
||||
|
||||
pub(crate) struct SquarePair {
|
||||
pub(crate) start: Square,
|
||||
pub(crate) end: Square,
|
||||
pub(crate) dx: usize,
|
||||
pub(crate) dy: usize,
|
||||
pub(crate) x_dir: i8,
|
||||
pub(crate) y_dir: i8,
|
||||
pub struct SquarePair {
|
||||
pub start: Square,
|
||||
pub end: Square,
|
||||
pub dx: usize,
|
||||
pub dy: usize,
|
||||
pub x_dir: i8,
|
||||
pub y_dir: i8,
|
||||
}
|
||||
|
||||
impl Square {
|
||||
pub(crate) fn new(file: usize, rank: usize, piece: Option<Piece>) -> Self {
|
||||
pub fn new(file: usize, rank: usize, piece: Option<Piece>) -> Self {
|
||||
Square { file, rank, piece }
|
||||
}
|
||||
|
||||
pub(crate) fn parse(notation: &str) -> Self {
|
||||
pub fn parse(notation: &str) -> Self {
|
||||
let mut chars = notation.chars();
|
||||
let piece = chars.next().expect("Piece missing");
|
||||
let piece = Piece::parse(&piece.to_string());
|
||||
@ -49,15 +48,15 @@ impl Square {
|
||||
Square::new(file, rank, piece)
|
||||
}
|
||||
|
||||
pub(crate) fn file_notation(&self) -> String {
|
||||
pub fn file_notation(&self) -> String {
|
||||
String::from("abcd".chars().nth(self.file).unwrap())
|
||||
}
|
||||
|
||||
pub(crate) fn rank_notation(&self) -> String {
|
||||
pub fn rank_notation(&self) -> String {
|
||||
format!("{}", BOARD_SIZE - self.rank)
|
||||
}
|
||||
|
||||
pub(crate) fn notation(&self) -> String {
|
||||
pub fn notation(&self) -> String {
|
||||
format!(
|
||||
"{}{}{}",
|
||||
self.piece_notation(),
|
||||
@ -66,7 +65,7 @@ impl Square {
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn is_occupied(&self) -> bool {
|
||||
pub fn is_occupied(&self) -> bool {
|
||||
self.piece.is_some()
|
||||
}
|
||||
|
||||
@ -80,7 +79,7 @@ impl Square {
|
||||
}
|
||||
|
||||
impl SquarePair {
|
||||
pub(crate) fn new(start: Square, end: Square) -> Self {
|
||||
pub fn new(start: Square, end: Square) -> Self {
|
||||
let mut dx = 0;
|
||||
let mut dy = 0;
|
||||
let mut x_dir = 0;
|
||||
@ -111,19 +110,11 @@ impl SquarePair {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_different(&self) -> bool {
|
||||
pub fn is_different(&self) -> bool {
|
||||
self.dx != 0 || self.dy != 0
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! sq {
|
||||
($sq:literal) => {
|
||||
Square::parse($sq)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use sq;
|
||||
|
||||
impl fmt::Debug for Square {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}({},{})", self.notation(), self.file, self.rank)
|
@ -1,6 +0,0 @@
|
||||
pub(crate) mod constants;
|
||||
pub(crate) mod errors;
|
||||
pub(crate) mod board;
|
||||
pub(crate) mod square;
|
||||
pub(crate) mod cmove;
|
||||
pub(crate) mod piece;
|
1
src/game.rs
Normal file
1
src/game.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod texture;
|
@ -1,3 +0,0 @@
|
||||
pub(crate) fn run() {
|
||||
println!("Running game...");
|
||||
}
|
@ -1 +0,0 @@
|
||||
pub(crate) mod game;
|
45
src/game/texture.rs
Normal file
45
src/game/texture.rs
Normal file
@ -0,0 +1,45 @@
|
||||
use macroquad::prelude::*;
|
||||
use sol_chess::board::piece::Piece;
|
||||
|
||||
pub struct PieceTexture {
|
||||
x: f32,
|
||||
y: f32,
|
||||
w: f32,
|
||||
h: f32,
|
||||
}
|
||||
|
||||
impl PieceTexture {
|
||||
fn new(x: u32, y: u32) -> Self {
|
||||
Self {
|
||||
x: x as f32 * 128.0,
|
||||
y: y as f32 * 128.0,
|
||||
w: 128.0,
|
||||
h: 128.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn for_piece(piece: Piece, sprite_size: f32) -> DrawTextureParams {
|
||||
let index = match piece {
|
||||
Piece::Pawn => 0,
|
||||
Piece::Knight => 1,
|
||||
Piece::Bishop => 2,
|
||||
Piece::Rook => 3,
|
||||
Piece::Queen => 4,
|
||||
Piece::King => 5,
|
||||
};
|
||||
|
||||
let color = 0;
|
||||
let texture_rect = PieceTexture::new(index, color);
|
||||
|
||||
DrawTextureParams {
|
||||
source: Some(Rect::new(
|
||||
texture_rect.x,
|
||||
texture_rect.y,
|
||||
texture_rect.w,
|
||||
texture_rect.h,
|
||||
)),
|
||||
dest_size: Some(Vec2::new(sprite_size, sprite_size)),
|
||||
..DrawTextureParams::default()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,14 @@
|
||||
use std::{fmt::Display, thread::Builder, time::Duration};
|
||||
use std::{fmt::Display, time::Duration};
|
||||
|
||||
use crate::{
|
||||
engine::{board::Board, piece::Piece, square::Square},
|
||||
solver::{self, solver::Solver},
|
||||
board::{piece::Piece, Board},
|
||||
solver::Solver,
|
||||
};
|
||||
use indicatif::ProgressBar;
|
||||
use rand::{seq::*, Rng};
|
||||
|
||||
pub(crate) fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats {
|
||||
let mut rand = rand::thread_rng();
|
||||
pub fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats {
|
||||
let rand = rand::thread_rng();
|
||||
let candidate_pieces = vec![
|
||||
Piece::Pawn,
|
||||
Piece::Pawn,
|
||||
@ -16,8 +16,14 @@ pub(crate) fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats {
|
||||
Piece::Pawn,
|
||||
Piece::Bishop,
|
||||
Piece::Bishop,
|
||||
Piece::Bishop,
|
||||
Piece::Bishop,
|
||||
Piece::Knight,
|
||||
Piece::Knight,
|
||||
Piece::Knight,
|
||||
Piece::Queen,
|
||||
Piece::Rook,
|
||||
Piece::Rook,
|
||||
];
|
||||
|
||||
if num_pieces > candidate_pieces.len().try_into().unwrap() {
|
||||
@ -31,7 +37,7 @@ pub(crate) fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats {
|
||||
let bar = ProgressBar::new_spinner();
|
||||
bar.enable_steady_tick(Duration::from_millis(100));
|
||||
let mut overall_stats = GenerateStats::new(0, 0, 0, 0, None);
|
||||
for i in 0..attempts {
|
||||
for _ in 0..attempts {
|
||||
let stats = try_generate(
|
||||
num_pieces,
|
||||
num_solutions,
|
||||
@ -56,7 +62,7 @@ pub(crate) fn generate(num_pieces: u32, num_solutions: u32) -> GenerateStats {
|
||||
overall_stats
|
||||
}
|
||||
|
||||
pub(crate) struct GenerateStats {
|
||||
pub struct GenerateStats {
|
||||
piece_total: u32,
|
||||
piece_success: u32,
|
||||
total: u32,
|
||||
@ -81,7 +87,7 @@ impl GenerateStats {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn print_stats(&self) {
|
||||
pub fn print_stats(&self) {
|
||||
let mut stats = String::new();
|
||||
add_stat(&mut stats, "Total attempts", self.total);
|
||||
add_stat(&mut stats, "Total pieces placed", self.piece_total);
|
||||
@ -91,7 +97,7 @@ impl GenerateStats {
|
||||
println!("{}", stats);
|
||||
}
|
||||
|
||||
pub(crate) fn board(mut self) -> Option<Board> {
|
||||
pub fn board(self) -> Option<Board> {
|
||||
self.board
|
||||
}
|
||||
}
|
||||
@ -112,7 +118,7 @@ fn try_generate(
|
||||
let mut board = Board::new();
|
||||
let mut piece_total = 0;
|
||||
let mut piece_success = 0;
|
||||
let mut now = std::time::Instant::now();
|
||||
let now = std::time::Instant::now();
|
||||
for _ in 0..num_pieces {
|
||||
let mut placed = false;
|
||||
let empty_squares = board.empty_squares();
|
||||
@ -155,7 +161,7 @@ fn try_generate(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{engine::board::GameState, solver::solver::Solver};
|
||||
use crate::{board::GameState, solver::Solver};
|
||||
|
||||
use super::*;
|
||||
|
@ -1 +0,0 @@
|
||||
pub(crate) mod generator;
|
3
src/lib.rs
Normal file
3
src/lib.rs
Normal file
@ -0,0 +1,3 @@
|
||||
pub mod board;
|
||||
pub mod generator;
|
||||
pub mod solver;
|
223
src/main.rs
223
src/main.rs
@ -1,125 +1,122 @@
|
||||
#[allow(unused)]
|
||||
mod engine;
|
||||
use game::texture::PieceTexture;
|
||||
use macroquad::prelude::*;
|
||||
use sol_chess::board::{square::Square, Board};
|
||||
|
||||
#[allow(unused)]
|
||||
mod solver;
|
||||
|
||||
#[allow(unused)]
|
||||
mod generator;
|
||||
|
||||
#[allow(unused)]
|
||||
mod game;
|
||||
|
||||
use argh::FromArgs;
|
||||
use engine::board::Board;
|
||||
#[macroquad::main("Solitaire Chess")]
|
||||
async fn main() {
|
||||
let background_color = Color::from_rgba(196, 195, 208, 255);
|
||||
let game = init().await;
|
||||
loop {
|
||||
clear_background(background_color);
|
||||
game.draw();
|
||||
next_frame().await
|
||||
}
|
||||
}
|
||||
|
||||
use crate::game::game as sol_chess_game;
|
||||
use crate::generator::generator::generate;
|
||||
use crate::solver::solver::Solver;
|
||||
async fn init() -> Game {
|
||||
set_pc_assets_folder("./assets");
|
||||
let texture_res = load_texture("pieces.png").await.unwrap();
|
||||
texture_res.set_filter(FilterMode::Nearest);
|
||||
build_textures_atlas();
|
||||
let mut board = Board::new();
|
||||
board.set(Square::parse("Pa4"));
|
||||
board.set(Square::parse("Pa3"));
|
||||
board.set(Square::parse("Na2"));
|
||||
board.set(Square::parse("Na1"));
|
||||
board.set(Square::parse("Bb4"));
|
||||
board.set(Square::parse("Bb3"));
|
||||
board.set(Square::parse("Rb2"));
|
||||
board.set(Square::parse("Rb1"));
|
||||
board.set(Square::parse("Kc4"));
|
||||
board.set(Square::parse("Kc3"));
|
||||
board.set(Square::parse("Qc2"));
|
||||
board.set(Square::parse("Qc1"));
|
||||
|
||||
fn main() {
|
||||
let args: Args = argh::from_env();
|
||||
let square_width = 128.0;
|
||||
let num_squares = 4;
|
||||
let x = (screen_width() - (square_width * num_squares as f32)) / 2.0;
|
||||
let y = (screen_height() - (square_width * num_squares as f32)) / 2.0;
|
||||
let game = Game::new(board, x, y, square_width, num_squares, texture_res);
|
||||
|
||||
if args.game {
|
||||
sol_chess_game::run();
|
||||
} else if args.generate {
|
||||
let puzzle = generate_puzzle(args.num_pieces, args.solutions);
|
||||
let Some(board) = puzzle else {
|
||||
return;
|
||||
};
|
||||
game
|
||||
}
|
||||
|
||||
board.pretty_print();
|
||||
if args.print {
|
||||
solve_puzzle(board);
|
||||
struct Game {
|
||||
board: Board,
|
||||
squares: Vec<GameSquare>,
|
||||
texture_res: Texture2D,
|
||||
num_squares: usize,
|
||||
}
|
||||
|
||||
impl Game {
|
||||
fn new(
|
||||
board: Board,
|
||||
x: f32,
|
||||
y: f32,
|
||||
square_width: f32,
|
||||
num_squares: usize,
|
||||
texture_res: Texture2D,
|
||||
) -> Self {
|
||||
let dark = Color::from_rgba(83, 104, 120, 255);
|
||||
let light = Color::from_rgba(190, 190, 190, 255);
|
||||
let mut rects = Vec::new();
|
||||
for i in 0..num_squares {
|
||||
for j in 0..num_squares {
|
||||
let x_eff = x + (i as f32 * square_width);
|
||||
let y_eff = y + (j as f32 * square_width);
|
||||
let rect = Rect::new(x_eff, y_eff, square_width, square_width);
|
||||
let color = match (i + j) % 2 {
|
||||
1 => dark,
|
||||
_ => light,
|
||||
};
|
||||
|
||||
rects.push(GameSquare { rect, color, i, j });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let board = if let Some(board_string) = args.solve_board {
|
||||
Board::from_string(board_string)
|
||||
} else if let Some(board_id) = args.solve {
|
||||
Board::from_id(board_id)
|
||||
} else {
|
||||
println!("Use --help to see available options");
|
||||
return;
|
||||
};
|
||||
let Ok(board) = board else {
|
||||
println!("Invalid board string/id");
|
||||
return;
|
||||
};
|
||||
board.pretty_print();
|
||||
solve_puzzle(board);
|
||||
|
||||
Self {
|
||||
board,
|
||||
squares: rects,
|
||||
num_squares,
|
||||
texture_res,
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&mut self, i: usize, j: usize) -> &mut GameSquare {
|
||||
&mut self.squares[i * self.num_squares + j]
|
||||
}
|
||||
|
||||
fn draw(&self) {
|
||||
let sprite_size = 100.0;
|
||||
self.squares.iter().for_each(|square| {
|
||||
draw_rectangle(
|
||||
square.rect.x,
|
||||
square.rect.y,
|
||||
square.rect.w,
|
||||
square.rect.h,
|
||||
square.color,
|
||||
);
|
||||
|
||||
if let Some(p) = &self.board.cells[square.i][square.j] {
|
||||
let offset = (square.rect.w - sprite_size) / 2.0;
|
||||
let dtp = PieceTexture::for_piece(*p, sprite_size);
|
||||
draw_texture_ex(
|
||||
&self.texture_res,
|
||||
square.rect.x + offset,
|
||||
square.rect.y + offset,
|
||||
WHITE,
|
||||
dtp,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn solve_puzzle(board: Board) {
|
||||
let solutions = Solver::new(board).solve();
|
||||
if solutions.len() == 0 {
|
||||
println!("No solutions found");
|
||||
return;
|
||||
}
|
||||
println!("Found {} solutions", solutions.len());
|
||||
let solution = solutions.first().unwrap();
|
||||
let mut idx = 0;
|
||||
solution.iter().for_each(|m| {
|
||||
idx += 1;
|
||||
println!("{}. {}", idx, m.notation());
|
||||
});
|
||||
}
|
||||
|
||||
fn generate_puzzle(num_pieces: Option<u32>, num_solutions: Option<u32>) -> Option<Board> {
|
||||
let mut num_pieces = num_pieces.unwrap_or(5);
|
||||
if num_pieces < 2 {
|
||||
num_pieces = 2;
|
||||
}
|
||||
|
||||
let mut num_solutions = num_solutions.unwrap_or(5);
|
||||
if num_solutions < 1 {
|
||||
num_solutions = 5;
|
||||
}
|
||||
|
||||
println!(
|
||||
"Generating a puzzle with {} pieces with a maximum of {} solutions",
|
||||
num_pieces, num_solutions
|
||||
);
|
||||
let gen = generate(num_pieces, num_solutions);
|
||||
gen.print_stats();
|
||||
|
||||
let Some(board) = gen.board() else {
|
||||
println!("Failed to generate a puzzle, try again");
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(board)
|
||||
}
|
||||
|
||||
/// Solitaire Chess puzzle generator and solver
|
||||
/// - v0.0.1 cool-mist
|
||||
#[derive(FromArgs)]
|
||||
struct Args {
|
||||
#[argh(switch, short = 'g')]
|
||||
/// generate a puzzle
|
||||
generate: bool,
|
||||
|
||||
#[argh(switch)]
|
||||
/// run the game
|
||||
game: bool,
|
||||
|
||||
#[argh(option, short = 'n')]
|
||||
/// number of pieces to place on the board while generating a puzzle
|
||||
num_pieces: Option<u32>,
|
||||
|
||||
#[argh(option)]
|
||||
/// maximum number of solutions allowed for the generated puzzle. atleast 1. defaults to 5
|
||||
solutions: Option<u32>,
|
||||
|
||||
#[argh(switch)]
|
||||
/// print the solution. When solving a puzzle, this is always set to true
|
||||
print: bool,
|
||||
|
||||
#[argh(option, short = 's')]
|
||||
/// the id of the board to solve
|
||||
solve: Option<u128>,
|
||||
|
||||
#[argh(option)]
|
||||
/// the board to solve in board representation
|
||||
solve_board: Option<String>,
|
||||
struct GameSquare {
|
||||
rect: Rect,
|
||||
color: Color,
|
||||
i: usize,
|
||||
j: usize,
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
use crate::engine::{
|
||||
board::{Board, GameState},
|
||||
use crate::board::{
|
||||
cmove::CMove,
|
||||
{Board, GameState},
|
||||
};
|
||||
|
||||
pub(crate) struct Solver {
|
||||
pub(crate) board: Board,
|
||||
pub struct Solver {
|
||||
pub board: Board,
|
||||
moves: Vec<CMove>,
|
||||
}
|
||||
|
||||
impl Solver {
|
||||
pub(crate) fn new(board: Board) -> Solver {
|
||||
pub fn new(board: Board) -> Solver {
|
||||
Solver {
|
||||
board,
|
||||
moves: vec![],
|
||||
@ -24,7 +24,7 @@ impl Solver {
|
||||
Solver { board, moves }
|
||||
}
|
||||
|
||||
pub(crate) fn solve(&self) -> Vec<Vec<CMove>> {
|
||||
pub fn solve(&self) -> Vec<Vec<CMove>> {
|
||||
let mut solutions = Vec::new();
|
||||
if let GameState::Won = self.board.game_state {
|
||||
solutions.push(self.moves.clone());
|
||||
@ -36,7 +36,7 @@ impl Solver {
|
||||
};
|
||||
|
||||
self.board.legal_moves.iter().for_each(|m| {
|
||||
let mut solver = self.clone(m.clone());
|
||||
let solver = self.clone(m.clone());
|
||||
let more_solutions = solver.solve();
|
||||
solutions.extend(more_solutions);
|
||||
});
|
||||
@ -47,12 +47,14 @@ impl Solver {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::engine::{
|
||||
piece::{p, Piece},
|
||||
square::{sq, Square},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::board::{square::Square, Board};
|
||||
|
||||
macro_rules! sq {
|
||||
($sq:literal) => {
|
||||
Square::parse($sq)
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn solver_smoke() {
|
@ -1 +0,0 @@
|
||||
pub(crate) mod solver;
|
26
tools/puzzle_checker/games/1.txt
Normal file
26
tools/puzzle_checker/games/1.txt
Normal file
@ -0,0 +1,26 @@
|
||||
*********** Game 1 ************
|
||||
|
||||
Generating a puzzle with 7 pieces with a maximum of 5 solutions
|
||||
Total attempts: 328
|
||||
Total pieces placed: 3363
|
||||
Success pieces placed: 2296
|
||||
Total time (ms): 1840
|
||||
|
||||
♗ ♗ ♙ ♘
|
||||
|
||||
. . . ♖
|
||||
|
||||
. . ♙ .
|
||||
|
||||
. . . ♙
|
||||
|
||||
|
||||
id: 140771860875974
|
||||
|
||||
Found 1 solutions
|
||||
1. BxPc2
|
||||
2. BxPd1
|
||||
3. RxBd1
|
||||
4. RxNd4
|
||||
5. RxPc4
|
||||
6. RxBb4
|
26
tools/puzzle_checker/games/10.txt
Normal file
26
tools/puzzle_checker/games/10.txt
Normal file
@ -0,0 +1,26 @@
|
||||
*********** Game 10 ************
|
||||
|
||||
Generating a puzzle with 7 pieces with a maximum of 5 solutions
|
||||
Total attempts: 43
|
||||
Total pieces placed: 407
|
||||
Success pieces placed: 301
|
||||
Total time (ms): 238
|
||||
|
||||
♙ . ♖ .
|
||||
|
||||
. . ♘ ♙
|
||||
|
||||
. ♙ ♘ .
|
||||
|
||||
♗ . . .
|
||||
|
||||
|
||||
id: 211381923512704
|
||||
|
||||
Found 4 solutions
|
||||
1. BxPb2
|
||||
2. NxPa4
|
||||
3. RxNc2
|
||||
4. RxBb2
|
||||
5. NxRb2
|
||||
6. NxPd3
|
26
tools/puzzle_checker/games/2.txt
Normal file
26
tools/puzzle_checker/games/2.txt
Normal file
@ -0,0 +1,26 @@
|
||||
*********** Game 2 ************
|
||||
|
||||
Generating a puzzle with 7 pieces with a maximum of 5 solutions
|
||||
Total attempts: 47
|
||||
Total pieces placed: 473
|
||||
Success pieces placed: 329
|
||||
Total time (ms): 279
|
||||
|
||||
. . . ♗
|
||||
|
||||
♘ . ♗ .
|
||||
|
||||
♙ . . .
|
||||
|
||||
. ♘ ♖ ♗
|
||||
|
||||
|
||||
id: 25288852387844
|
||||
|
||||
Found 4 solutions
|
||||
1. RxBd1
|
||||
2. BxBc3
|
||||
3. RxNb1
|
||||
4. NxRb1
|
||||
5. NxBc3
|
||||
6. NxPa2
|
26
tools/puzzle_checker/games/3.txt
Normal file
26
tools/puzzle_checker/games/3.txt
Normal file
@ -0,0 +1,26 @@
|
||||
*********** Game 3 ************
|
||||
|
||||
Generating a puzzle with 7 pieces with a maximum of 5 solutions
|
||||
Total attempts: 22
|
||||
Total pieces placed: 239
|
||||
Success pieces placed: 154
|
||||
Total time (ms): 160
|
||||
|
||||
♗ . ♖ ♙
|
||||
|
||||
. . . ♙
|
||||
|
||||
. . . ♙
|
||||
|
||||
. ♙ . ♗
|
||||
|
||||
|
||||
id: 140737595313588
|
||||
|
||||
Found 5 solutions
|
||||
1. RxBa4
|
||||
2. RxPd4
|
||||
3. RxPd3
|
||||
4. RxPd2
|
||||
5. RxBd1
|
||||
6. RxPb1
|
26
tools/puzzle_checker/games/4.txt
Normal file
26
tools/puzzle_checker/games/4.txt
Normal file
@ -0,0 +1,26 @@
|
||||
*********** Game 4 ************
|
||||
|
||||
Generating a puzzle with 7 pieces with a maximum of 5 solutions
|
||||
Total attempts: 200
|
||||
Total pieces placed: 2059
|
||||
Success pieces placed: 1388
|
||||
Total time (ms): 929
|
||||
|
||||
♙ ♘ . ♗
|
||||
|
||||
. ♖ ♙ ♘
|
||||
|
||||
. . . .
|
||||
|
||||
. . ♙ .
|
||||
|
||||
|
||||
id: 211152405031232
|
||||
|
||||
Found 1 solutions
|
||||
1. RxNb4
|
||||
2. RxPa4
|
||||
3. RxBd4
|
||||
4. RxNd3
|
||||
5. RxPc3
|
||||
6. RxPc1
|
26
tools/puzzle_checker/games/5.txt
Normal file
26
tools/puzzle_checker/games/5.txt
Normal file
@ -0,0 +1,26 @@
|
||||
*********** Game 5 ************
|
||||
|
||||
Generating a puzzle with 7 pieces with a maximum of 5 solutions
|
||||
Total attempts: 74
|
||||
Total pieces placed: 771
|
||||
Success pieces placed: 512
|
||||
Total time (ms): 437
|
||||
|
||||
♗ ♙ . ♘
|
||||
|
||||
. ♖ . ♘
|
||||
|
||||
. . . .
|
||||
|
||||
. ♗ ♗ .
|
||||
|
||||
|
||||
id: 140792316316480
|
||||
|
||||
Found 4 solutions
|
||||
1. BxRb3
|
||||
2. BxNd3
|
||||
3. NxBb3
|
||||
4. NxBc1
|
||||
5. NxBd3
|
||||
6. NxPb4
|
26
tools/puzzle_checker/games/6.txt
Normal file
26
tools/puzzle_checker/games/6.txt
Normal file
@ -0,0 +1,26 @@
|
||||
*********** Game 6 ************
|
||||
|
||||
Generating a puzzle with 7 pieces with a maximum of 5 solutions
|
||||
Total attempts: 1
|
||||
Total pieces placed: 9
|
||||
Success pieces placed: 7
|
||||
Total time (ms): 0
|
||||
|
||||
. ♙ . .
|
||||
|
||||
. . . .
|
||||
|
||||
♗ . ♗ ♗
|
||||
|
||||
♖ ♙ . ♘
|
||||
|
||||
|
||||
id: 2456822087717
|
||||
|
||||
Found 5 solutions
|
||||
1. RxBa2
|
||||
2. RxBc2
|
||||
3. RxBd2
|
||||
4. RxNd1
|
||||
5. RxPb1
|
||||
6. RxPb4
|
26
tools/puzzle_checker/games/7.txt
Normal file
26
tools/puzzle_checker/games/7.txt
Normal file
@ -0,0 +1,26 @@
|
||||
*********** Game 7 ************
|
||||
|
||||
Generating a puzzle with 7 pieces with a maximum of 5 solutions
|
||||
Total attempts: 22
|
||||
Total pieces placed: 230
|
||||
Success pieces placed: 154
|
||||
Total time (ms): 109
|
||||
|
||||
♖ . . ♙
|
||||
|
||||
. . . ♗
|
||||
|
||||
♗ ♙ ♘ .
|
||||
|
||||
. . ♖ .
|
||||
|
||||
|
||||
id: 107752945007872
|
||||
|
||||
Found 2 solutions
|
||||
1. RxNc2
|
||||
2. RxPb2
|
||||
3. RxBa2
|
||||
4. RxRa4
|
||||
5. RxPd4
|
||||
6. RxBd3
|
26
tools/puzzle_checker/games/8.txt
Normal file
26
tools/puzzle_checker/games/8.txt
Normal file
@ -0,0 +1,26 @@
|
||||
*********** Game 8 ************
|
||||
|
||||
Generating a puzzle with 7 pieces with a maximum of 5 solutions
|
||||
Total attempts: 63
|
||||
Total pieces placed: 639
|
||||
Success pieces placed: 441
|
||||
Total time (ms): 345
|
||||
|
||||
. . ♖ .
|
||||
|
||||
. ♙ . .
|
||||
|
||||
♙ . ♗ .
|
||||
|
||||
♗ ♙ . ♗
|
||||
|
||||
|
||||
id: 3579962327044
|
||||
|
||||
Found 5 solutions
|
||||
1. BxBc2
|
||||
2. RxBc2
|
||||
3. RxPa2
|
||||
4. RxBa1
|
||||
5. RxPb1
|
||||
6. RxPb3
|
26
tools/puzzle_checker/games/9.txt
Normal file
26
tools/puzzle_checker/games/9.txt
Normal file
@ -0,0 +1,26 @@
|
||||
*********** Game 9 ************
|
||||
|
||||
Generating a puzzle with 7 pieces with a maximum of 5 solutions
|
||||
Total attempts: 250
|
||||
Total pieces placed: 2557
|
||||
Success pieces placed: 1750
|
||||
Total time (ms): 1298
|
||||
|
||||
. . . .
|
||||
|
||||
♘ ♘ . ♙
|
||||
|
||||
. ♙ . ♗
|
||||
|
||||
♙ . ♘ .
|
||||
|
||||
|
||||
id: 22408723452320
|
||||
|
||||
Found 1 solutions
|
||||
1. BxNc1
|
||||
2. NxBc1
|
||||
3. NxPd3
|
||||
4. NxPb2
|
||||
5. axNb2
|
||||
6. bxNa3
|
9
tools/puzzle_checker/gen_games.sh
Executable file
9
tools/puzzle_checker/gen_games.sh
Executable file
@ -0,0 +1,9 @@
|
||||
if [ ! -d games ]; then
|
||||
mkdir games
|
||||
fi
|
||||
|
||||
for i in {1..10}; do
|
||||
echo "*********** Game $i ************" >> games/$i.txt
|
||||
echo "" >> games/$i.txt
|
||||
sol_cli -g -n 7 --print >> games/$i.txt
|
||||
done
|
Loading…
x
Reference in New Issue
Block a user