From 9aec3f89e7239eded3fd18b5428947143463b213 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Bruyère Date: Mon, 20 Sep 2021 12:00:59 +0000 Subject: tesselation, should be optional --- README.md | 6 ++ src/BoardWidget.cs | 211 +++++++++++++++++++++++++++++++++++-------- src/VkChess.cs | 205 +++++++++++++++++++++++++++-------------- ui/Button.tmp | 2 +- ui/Spinner.tmp | 11 +++ ui/Window.template | 6 +- ui/board.crow | 6 -- ui/chess.crow | 3 - ui/chess.style | 55 +++++++++-- ui/icoButton.tmp | 11 +++ ui/icoToggle.tmp | 14 +++ ui/icons/board.svg | 11 +++ ui/icons/clock.svg | 7 ++ ui/icons/crossed-arrows.svg | 6 ++ ui/icons/dashboard.svg | 7 ++ ui/icons/exit-symbol-1.svg | 8 ++ ui/icons/exit.svg | 6 ++ ui/icons/exit2.svg | 7 ++ ui/icons/expand-arrows-1.svg | 9 ++ ui/icons/flag.svg | 7 ++ ui/icons/hint.svg | 7 ++ ui/icons/human.svg | 6 ++ ui/icons/load.svg | 7 ++ ui/icons/move-arrows.svg | 6 ++ ui/icons/moves.svg | 10 ++ ui/icons/open-book.svg | 8 ++ ui/icons/options.svg | 7 ++ ui/icons/options2.svg | 8 ++ ui/icons/pause.svg | 7 ++ ui/icons/play.svg | 6 ++ ui/icons/save.svg | 7 ++ ui/icons/undo.svg | 6 ++ ui/icons/warning.svg | 6 ++ ui/menuCommand.itmp | 7 +- ui/menuItem.itmp | 2 +- ui/menuItem.template | 4 +- ui/menuOverlay.crow | 12 +++ ui/newGame.crow | 20 ---- ui/winBoard.crow | 7 ++ ui/winLoad.crow | 19 ++++ ui/winMoves.crow | 4 +- ui/winNewGame.crow | 20 ++++ ui/winOptions.crow | 10 +- vkChess.net.csproj | 48 ++++++++-- 44 files changed, 674 insertions(+), 168 deletions(-) create mode 100644 ui/Spinner.tmp delete mode 100644 ui/board.crow create mode 100644 ui/icoButton.tmp create mode 100644 ui/icoToggle.tmp create mode 100644 ui/icons/board.svg create mode 100644 ui/icons/clock.svg create mode 100644 ui/icons/crossed-arrows.svg create mode 100644 ui/icons/dashboard.svg create mode 100644 ui/icons/exit-symbol-1.svg create mode 100644 ui/icons/exit.svg create mode 100644 ui/icons/exit2.svg create mode 100644 ui/icons/expand-arrows-1.svg create mode 100644 ui/icons/flag.svg create mode 100644 ui/icons/hint.svg create mode 100644 ui/icons/human.svg create mode 100644 ui/icons/load.svg create mode 100644 ui/icons/move-arrows.svg create mode 100644 ui/icons/moves.svg create mode 100644 ui/icons/open-book.svg create mode 100644 ui/icons/options.svg create mode 100644 ui/icons/options2.svg create mode 100644 ui/icons/pause.svg create mode 100644 ui/icons/play.svg create mode 100644 ui/icons/save.svg create mode 100644 ui/icons/undo.svg create mode 100644 ui/icons/warning.svg create mode 100644 ui/menuOverlay.crow delete mode 100644 ui/newGame.crow create mode 100644 ui/winBoard.crow create mode 100644 ui/winLoad.crow create mode 100644 ui/winNewGame.crow diff --git a/README.md b/README.md index ab84fc5..5eb252b 100644 --- a/README.md +++ b/README.md @@ -1 +1,7 @@ # VkChess.net + +

+ + + +

diff --git a/src/BoardWidget.cs b/src/BoardWidget.cs index 7188a41..456bfe5 100644 --- a/src/BoardWidget.cs +++ b/src/BoardWidget.cs @@ -3,7 +3,9 @@ // This code is licensed under the MIT license (MIT) (http://opensource.org/licenses/MIT) using System; +using System.Collections.Generic; using System.ComponentModel; +using System.IO; using Crow; using Crow.Drawing; //using vkvg; @@ -13,10 +15,13 @@ namespace vkChess public class BoardWidget : Widget { Picture sprites; + string filePath; + IList moves; + string[,] currentBoard; Piece[,] board; [DefaultValue(null)] - public virtual Piece[,] Board { + public Piece[,] Board { get { return board; } set { if (board == value) { @@ -28,15 +33,84 @@ namespace vkChess RegisterForGraphicUpdate (); } } - public BoardWidget ():base() + public virtual IList Moves { + get => moves; + set { + if (moves == value) + return; + + if (moves is ObservableList ol) { + ol.ListAdd -= Lines_ListAdd; + ol.ListRemove -= Lines_ListRemove; + ol.ListClear -= Lines_ListClear; + } + + moves = value; + + if (moves is ObservableList olNew) { + olNew.ListAdd += Lines_ListAdd; + olNew.ListRemove += Lines_ListRemove; + olNew.ListClear += Lines_ListClear; + } + + NotifyValueChanged ("Moves", moves); + + replayAllMoves (); + + RegisterForGraphicUpdate (); + } + } + public string FilePath { + get => FilePath; + set { + if (filePath == value) + return; + filePath = value; + NotifyValueChangedAuto (filePath); + RegisterForGraphicUpdate (); + + if (string.IsNullOrEmpty (filePath)) + resetCurrentBoard (); + else + loadFromFile (); + } + } + public BoardWidget (): base() { sprites = new SvgPicture ("data/Pieces.svg"); } + static readonly string[,] boardInit = { + {"wr", "wp", "", "", "", "", "bp", "br"}, + {"wk", "wp", "", "", "", "", "bp", "bk"}, + {"wb", "wp", "", "", "", "", "bp", "bb"}, + {"wq", "wp", "", "", "", "", "bp", "bq"}, + {"wK", "wp", "", "", "", "", "bp", "bK"}, + {"wb", "wp", "", "", "", "", "bp", "bb"}, + {"wk", "wp", "", "", "", "", "bp", "bk"}, + {"wr", "wp", "", "", "", "", "bp", "br"} + }; + void Lines_ListAdd (object sender, ListChangedEventArg e) + { + processMove (currentBoard, e.Element.ToString()); + RegisterForRedraw (); + } + void Lines_ListRemove (object sender, ListChangedEventArg e) + { + replayAllMoves (); + RegisterForRedraw (); + } + void Lines_ListClear (object sender, ListClearEventArg e) + { + resetCurrentBoard(); + RegisterForRedraw (); + } protected override void onDraw (Context gr) { base.onDraw (gr); + this.setFontForContext (gr); + /*gr.FontFace = Font.Name; gr.FontSize = (uint)Font.Size;*/ @@ -88,46 +162,107 @@ namespace vkChess } gr.Fill (); - if (board == null) + if (board != null) { + for (int x = 0; x < 8; x++) { + for (int y = 0; y < 8; y++) { + if (board [x, y] == null) + continue; + + string spriteName; + if (board [x, y].Player == ChessColor.White) + spriteName = "w"; + else + spriteName = "b"; + + switch (board[x,y].Type) { + case PieceType.Pawn: + spriteName += "p"; + break; + case PieceType.Rook: + spriteName += "r"; + break; + case PieceType.Knight: + spriteName += "k"; + break; + case PieceType.Bishop: + spriteName += "b"; + break; + case PieceType.King: + spriteName += "K"; + break; + case PieceType.Queen: + spriteName += "q"; + break; + } + sprites.Paint (IFace, gr,new Rectangle( + (int)(gridX + x * cellSize), + (int)(gridY + (7 - y) * cellSize), + (int)cellSize, + (int)cellSize),spriteName); + } + } + } else if (moves != null && currentBoard != null) { + for (int x = 0; x < 8; x++) { + for (int y = 0; y < 8; y++) { + if (string.IsNullOrEmpty(currentBoard [x, y])) + continue; + sprites.Paint (IFace, gr,new Rectangle( + (int)(gridX + x * cellSize), + (int)(gridY + (7 - y) * cellSize), + (int)cellSize, + (int)cellSize),currentBoard[x, y]); + } + } + } + } + void loadFromFile () { + try { + using (Stream stream = new FileStream (filePath, FileMode.Open)) + using (StreamReader sw = new StreamReader (stream)) + Moves = sw.ReadLine ().Split (' '); + } catch (Exception ex) { + Crow.MessageBox.ShowModal (IFace, MessageBox.Type.Error, $"Failed loading {filePath}\n{ex.Message}"); + } + } + void resetCurrentBoard () => currentBoard = boardInit.Clone() as string[,]; + void replayAllMoves () { + resetCurrentBoard (); + if (moves == null) return; - for (int x = 0; x < 8; x++) { - for (int y = 0; y < 8; y++) { - if (board [x, y] == null) - continue; - - string spriteName; - if (board [x, y].Player == ChessColor.White) - spriteName = "w"; - else - spriteName = "b"; - - switch (board[x,y].Type) { - case PieceType.Pawn: - spriteName += "p"; - break; - case PieceType.Rook: - spriteName += "r"; - break; - case PieceType.Knight: - spriteName += "k"; - break; - case PieceType.Bishop: - spriteName += "b"; - break; - case PieceType.King: - spriteName += "K"; - break; - case PieceType.Queen: - spriteName += "q"; - break; + int i = 0; + + lock (VkChess.movesMutex) { + foreach (string move in moves) { + try { + processMove (currentBoard, move); + } catch { + Console.WriteLine ($"move: {move} i:{i}"); } - sprites.Paint (IFace, gr,new Rectangle( - (int)(gridX + x * cellSize), - (int)(gridY + (7 - y) * cellSize), - (int)cellSize, - (int)cellSize),spriteName); + i++; + } + } + } + void processMove (string[,] board, string move) { + ReadOnlySpan m = move.AsSpan(); + Point p0 = VkChess.getChessCell (m.Slice (0, 2)); + Point p1 = VkChess.getChessCell (m.Slice (2, 2)); + + if (board[p0.X, p0.Y][1] == 'p') {//pawn + if (m.Length == 5) //pawnPromotion + board[p0.X, p0.Y] = board[p0.X, p0.Y][0].ToString() + m[4]; + else if (p0.X != p1.X && string.IsNullOrEmpty (board[p1.X, p1.Y]))//prise en passant + board[p0.X, p1.Y] = null; + }else if (board[p0.X, p0.Y][1] == 'K' && Math.Abs(p0.X - p1.X) > 1) {//rook + if (p0.X - p1.X < 0) {//small rock + board[5, p0.Y] = board[7, p0.Y]; + board[7, p0.Y] = null; + } else { //big rock + board[3, p0.Y] = board[0, p0.Y]; + board[0, p0.Y] = null; } } + board[p1.X, p1.Y] = board[p0.X, p0.Y]; + board[p0.X, p0.Y] = null; } } } diff --git a/src/VkChess.cs b/src/VkChess.cs index f83b1d6..cfc1d9d 100644 --- a/src/VkChess.cs +++ b/src/VkChess.cs @@ -12,6 +12,8 @@ using Vulkan; using System.Reflection; using System.Net; using System.Runtime.InteropServices; +using Window = Crow.Window; +using System.Text; namespace vkChess { @@ -53,21 +55,34 @@ namespace vkChess app.Run (); } - public CommandGroup Commands => new CommandGroup ( - new CommandGroup ("Menu", - new Command ("New Game", () => loadWindow ("ui/newGame.crow", this)), - new Command ("Save", () => loadWindow ("ui/newGame.crow", this)), - new Command ("Load", () => loadWindow ("ui/newGame.crow", this)), - new Command ("Options", () => loadWindow ("ui/winOptions.crow", this)), - new Command ("Moves", () => loadWindow ("ui/winMoves.crow", this)), -#if DEBUG - new Command ("Log", () => loadWindow ("ui/winLog.crow", this)), -#endif - new Command ("Quit", () => Close()) - ), - new Command ("Undo", () => undo()) - //new Command ("Redo", () => Close()) - ); + public CommandGroup Commands, IconBar; + public Command CMDUndo, CMDOptions, CMDViewBoard, CMDViewMoves, CMDLoadSelectedFile; + public ToggleCommand CMDToggleHint; + void initCommands () { + CMDUndo = new ActionCommand ("Undo", () => undo(), "#icons.undo.svg"); + CMDOptions = new ActionCommand ("Options", () => loadWindow ("#ui.winOptions.crow", this), "#icons.options.svg"); + CMDViewMoves = new ActionCommand ("Moves", () => loadWindow ("#ui.winMoves.crow", this), "#icons.moves.svg"); + CMDViewBoard = new ActionCommand ("Mini Board", () => loadWindow ("#ui.winBoard.crow", this), "#icons.board.svg"); + CMDLoadSelectedFile = new ActionCommand ("Load", () => loadGameFromFile (selectedSavedGame), "#icons.load.svg"); + CMDToggleHint = new ToggleCommand (this, "Hint", new Binding ("EnableHint"), "#icons.hint.svg", null, true); + + IconBar = new CommandGroup (CMDOptions, CMDViewBoard, CMDViewMoves, CMDUndo, CMDToggleHint); + + Commands = new CommandGroup ( + new CommandGroup ("Menu", + new ActionCommand ("New Game", () => loadWindow ("#ui.winNewGame.crow", this), "#icons.flag.svg"), + new ActionCommand ("Save", () => loadWindow ("#ui.save.crow", this), "#icons.save.svg"), + new ActionCommand ("Load", () => loadWindow ("#ui.winLoad.crow", this), "#icons.load.svg"), + CMDOptions, + CMDViewBoard, + CMDViewMoves, + #if DEBUG + new ActionCommand ("Log", () => loadWindow ("#ui.winLog.crow", this), "#icons.load.svg"), + #endif + new ActionCommand ("Quit", () => Close(), "#icons.exit.svg") + ) + ); + } public override string [] EnabledInstanceExtensions => new string [] { @@ -127,6 +142,7 @@ namespace vkChess } protected override void initVulkan () { initLog (); + initCommands(); base.initVulkan (); @@ -182,9 +198,12 @@ namespace vkChess initBoard (); initStockfish (); - iFace.Load ("ui/chess.crow").DataSource = this; + iFace.Load ("#ui.menuOverlay.crow").DataSource = this; + iFace.Load ("#ui.chess.crow").DataSource = this; - loadCurrentGame (); + loadGameFromPositionString (Configuration.Global.Get ("CurrentGame")); + + restoreSavedOpenedWindows (); } void createCamera () { camera = new Camera (Utils.DegreesToRadians (CameraFOV), 1f, 0.1f, 32f); @@ -388,6 +407,7 @@ namespace vkChess protected override void Dispose (bool disposing) { saveCurrentGame (); + saveOpenedWindowConfig (); Configuration.Global.Save(); if (disposing) { if (!isDisposed) { @@ -551,20 +571,13 @@ namespace vkChess loadWindow (@"ui/winSSR.crow", this); break; case Glfw.Key.F2: - /*loadWindow (@"ui/board.crow", this); - */ - //loadWindow (@"ui/scene.crow", this.renderer.model); - loadWindow (@"ui/board.crow", this); - NotifyValueChanged ("board", board); + saveGame (); break; case Glfw.Key.F3: checkBoardIntegrity (); break; case Glfw.Key.F4: - if (modifiers.HasFlag (Modifier.Shift)) - loadCurrentGame (); - else - saveCurrentGame (); + loadWindow (@"ui/winLoad.crow", this); break; case Glfw.Key.Keypad0: whitePieces [0].X = 0; @@ -685,7 +698,7 @@ namespace vkChess } } void onNewWhiteGame (object sender, MouseButtonEventArgs e) { - closeWindow ("ui/newGame.crow"); + closeWindow ("#ui.winNewGame.crow"); if (CurrentState < GameState.Play) return; Log(LogType.Custom1, "New White Game"); @@ -708,7 +721,7 @@ namespace vkChess Animation.StartAnimation (new AngleAnimation (this, "CameraAngleY", 0, 50)); } void onNewBlackGame (object sender, MouseButtonEventArgs e) { - closeWindow ("ui/newGame.crow"); + closeWindow ("#ui.winNewGame.crow"); if (CurrentState < GameState.Play) return; Log(LogType.Custom1, "New Black Game"); @@ -986,8 +999,91 @@ namespace vkChess Configuration.Global.Set ("OptGameIsExpanded", value); } } + string defaultSaveDirectory => System.IO.Path.Combine (Configuration.AppConfigPath, "SavedGames"); + public string[] SavedGames { + get { + if (!Directory.Exists (defaultSaveDirectory)) + return null; + return Directory.GetFiles (defaultSaveDirectory, "*.chess"); + } + } + string selectedSavedGame; + public string SelectedSavedGame { + get => selectedSavedGame; + set { + if (selectedSavedGame == value) + return; + selectedSavedGame = value; + NotifyValueChanged (selectedSavedGame); + if (string.IsNullOrEmpty (selectedSavedGame)) + CMDLoadSelectedFile.CanExecute = false; + else if (File.Exists (selectedSavedGame)) + CMDLoadSelectedFile.CanExecute = true; + } + } + void saveCurrentGame() { + if (historyIdx > 0) + StockfishMoves = savedMoves; + Configuration.Global.Set ("CurrentGame", stockfishPositionCommand); + } + + void loadGameFromPositionString (string positions) { + if (string.IsNullOrEmpty (positions)) + return; + + stockfishMoves = new ObservableList (positions.Split (' ')); + + if (currentState < GameState.Play) + return; + resync3DScene (); + } + void saveGame (string fileName = null) { + if (!Directory.Exists (defaultSaveDirectory)) + Directory.CreateDirectory (defaultSaveDirectory); + + if (string.IsNullOrEmpty (fileName)) + fileName = $"{DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss")}.chess"; + else if (!fileName.EndsWith (".chess")) + fileName += ".chess"; + + string filePath = System.IO.Path.Combine (defaultSaveDirectory, fileName); + using (Stream stream = new FileStream (filePath, FileMode.Create)) + using (StreamWriter sw = new StreamWriter (stream)) + sw.WriteLine (stockfishPositionCommand); + } + void loadGameFromFile (string fullPath) { + closeWindow ("#ui.winBoard.crow"); + if (string.IsNullOrEmpty (fullPath) && !File.Exists (fullPath)) + return; + using (Stream stream = new FileStream (fullPath, FileMode.Open)) + using (StreamReader sw = new StreamReader (stream)) + loadGameFromPositionString (sw.ReadLine ()); + } + void saveOpenedWindowConfig () { + StringBuilder sb = new StringBuilder(100); + foreach (Window win in iFace.GraphicTree.OfType()) + sb.Append ($"{win.Name};{win.Left};{win.Top};{win.Width};{win.Height};{win.HorizontalAlignment};{win.VerticalAlignment}|"); + Configuration.Global.Set ("OpenedWindows", + sb.Length > 0 ? sb.ToString (0, sb.Length - 1) : ""); + } + void restoreSavedOpenedWindows () { + string wins = Configuration.Global.Get ("OpenedWindows"); + if (string.IsNullOrEmpty (wins)) + return; + foreach (string w in wins.Split ('|')) { + string [] tmp = w.Split (';'); + loadWindow (tmp[0], this); + Widget win = iFace.FindByName (tmp[0]); + win.Left = int.Parse (tmp[1]); + win.Top = int.Parse (tmp[2]); + win.Width = Measure.Parse (tmp[3]); + win.Height = Measure.Parse (tmp[4]); + win.HorizontalAlignment = EnumsNET.Enums.Parse (tmp[5]); + win.VerticalAlignment = EnumsNET.Enums.Parse (tmp[6]); + } + } #endregion #region LOGS @@ -995,7 +1091,7 @@ namespace vkChess ObservableList logs = new ObservableList(); public ObservableList MainLog => logs; void initLog () { - LogContextMenu = new CommandGroup (new Command("Clear Log", () => ResetLog())); + LogContextMenu = new CommandGroup (new ActionCommand("Clear Log", () => ResetLog())); } public void Log(LogType type, string message) { if (string.IsNullOrEmpty (message)) @@ -1019,7 +1115,8 @@ namespace vkChess if (historyIdx == 0) { //save current positions - savedMoves = StockfishMoves; + lock (movesMutex) + savedMoves = StockfishMoves; } historyIdx = value; @@ -1027,12 +1124,15 @@ namespace vkChess if (historyIdx == 0) { //restore current saved positions - StockfishMoves = savedMoves; + lock (movesMutex) + StockfishMoves = savedMoves; savedMoves = null; resync3DScene (); startTurn (); } else { - StockfishMoves = savedMoves.Take (savedMoves.Count - historyIdx).ToList(); + lock (movesMutex) + StockfishMoves = savedMoves.Take (savedMoves.Count - historyIdx).ToList(); + CurrentState = GameState.Play; resync3DScene (); } } @@ -1044,7 +1144,7 @@ namespace vkChess #endregion #region Stockfish - object movesMutex = new object (); + public static object movesMutex = new object (); Process stockfish; volatile bool waitAnimationFinished; //Queue stockfishCmdQueue = new Queue (); @@ -1169,24 +1269,6 @@ namespace vkChess get => stockfishMoves; set { stockfishMoves = value; } } - void saveCurrentGame() { - if (historyIdx > 0) - StockfishMoves = savedMoves; - Configuration.Global.Set ("CurrentGame", stockfishPositionCommand); - } - void loadCurrentGame() { - - resetBoard (false); - - string curGame = Configuration.Global.Get ("CurrentGame"); - if (string.IsNullOrEmpty (curGame)) - return; - stockfishMoves = new ObservableList (curGame.Split (' ')); - - if (currentState < GameState.Play) - return; - resync3DScene (); - } void resync3DScene () { replaySilently (); @@ -1668,20 +1750,6 @@ namespace vkChess p.Reset (animate); board [p.initX, p.initY] = p; } - } - void saveGame (string fileName = "default.chess") { - string defaultSaveDirectory = System.IO.Path.Combine (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile), ".config", - System.Reflection.Assembly.GetEntryAssembly ().GetName().Name, "SavedGames"); - if (!Directory.Exists (defaultSaveDirectory)) - Directory.CreateDirectory (defaultSaveDirectory); - - string filePath = System.IO.Path.Combine (defaultSaveDirectory, fileName); - using (Stream stream = new FileStream (filePath, FileMode.Create)) - using (StreamWriter sw = new StreamWriter (stream)) - sw.WriteLine (stockfishPositionCommand); - } - void loadGame (string fileName = "default.chess") { - } void addPiece (ChessColor player, PieceType _type, int col, int line) { Piece p = new Piece (player, _type, col, line, @@ -2012,15 +2080,14 @@ namespace vkChess preview_Captured = null; } string getChessCell (Point cell) => getChessCell (cell.X, cell.Y); - string getChessCell (int col, int line) { + public static string getChessCell (int col, int line) { if (col < 0 || line < 0) return null; char c = (char)(col + 97); return c.ToString () + (line + 1).ToString (); } - Point getChessCell (string s) { - return new Point ((int)s [0] - 97, int.Parse (s [1].ToString ()) - 1); - } + public static Point getChessCell (ReadOnlySpan s) + => new Point ((int)s [0] - 97, int.Parse (s [1].ToString ()) - 1); Vector3 getCurrentCapturePosition (Piece p) { float x, y; if (p.Player == ChessColor.White) { diff --git a/ui/Button.tmp b/ui/Button.tmp index 4fcdde1..f3e6f22 100644 --- a/ui/Button.tmp +++ b/ui/Button.tmp @@ -4,7 +4,7 @@ MouseEnter="{Foreground=vgradient|0:White|0.2:Grey|0.9:Grey|1:Black};{caption.Foreground=White}" MouseLeave="{Foreground=Transparent};{caption.Foreground=LightGrey}" MouseDown="{Foreground=vgradient|0:Black|0.05:Grey|0.85:Grey|1:White}" - MouseUp="{Foreground=vgradient|0:White|0.2:Grey|0.9:Grey|1:Black}" + MouseUp="{Foreground=vgradient|0:White|0.2:Grey|0.9:Grey|1:Black}" >