From 81dd267290a5eceec7e4a2c505ec297eb8c4f3b7 Mon Sep 17 00:00:00 2001 From: Lucas Morgan Date: Thu, 26 Sep 2024 21:11:47 -0500 Subject: [PATCH] yarp --- .editorconfig | 232 +++++++++ Shogi.Domain/Other/PieceRulesRegistration.cs | 11 + Shogi.Domain/Other/Rules.cs | 61 +++ Shogi.Domain/ValueObjects/Bishop.cs | 20 +- Shogi.Domain/ValueObjects/BoardState.cs | 440 +++++++++--------- Shogi.Domain/ValueObjects/GoldGeneral.cs | 8 +- Shogi.Domain/ValueObjects/King.cs | 12 +- Shogi.Domain/ValueObjects/Lance.cs | 2 +- Shogi.Domain/ValueObjects/Pawn.cs | 2 +- Shogi.Domain/ValueObjects/Rook.cs | 16 +- Shogi.Domain/ValueObjects/SilverGeneral.cs | 10 +- Shogi.Domain/ValueObjects/WhichPiece.cs | 29 +- .../Pathing/Direction.cs | 35 +- .../YetToBeAssimilatedIntoDDD/Pathing/Path.cs | 64 ++- Shogi.sln | 1 + Tests/UnitTests/RookShould.cs | 32 +- 16 files changed, 636 insertions(+), 339 deletions(-) create mode 100644 .editorconfig create mode 100644 Shogi.Domain/Other/PieceRulesRegistration.cs create mode 100644 Shogi.Domain/Other/Rules.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b564f37 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,232 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 2 +indent_style = tab +tab_width = 2 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = false +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = true + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = false +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = false +csharp_style_expression_bodied_properties = true + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_anonymous_function = true +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true +csharp_style_namespace_declarations = block_scoped +csharp_style_prefer_method_group_conversion = true +csharp_style_prefer_primary_constructors = true +csharp_style_prefer_top_level_statements = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_local_over_anonymous_function = true +csharp_style_prefer_null_check_over_type_check = true +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case diff --git a/Shogi.Domain/Other/PieceRulesRegistration.cs b/Shogi.Domain/Other/PieceRulesRegistration.cs new file mode 100644 index 0000000..9661745 --- /dev/null +++ b/Shogi.Domain/Other/PieceRulesRegistration.cs @@ -0,0 +1,11 @@ +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; + +namespace Shogi.Domain.Other; + +public record PieceRulesRegistration(TPiece WhichPiece, ICollection MoveSet) where TPiece : Enum +{ +} + +public record PieceInPlay(TPiece WhichPiece, int OwningPlayerNumber) where TPiece : Enum +{ +} diff --git a/Shogi.Domain/Other/Rules.cs b/Shogi.Domain/Other/Rules.cs new file mode 100644 index 0000000..d2b3c89 --- /dev/null +++ b/Shogi.Domain/Other/Rules.cs @@ -0,0 +1,61 @@ +using Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; +using System.Drawing; + +namespace Shogi.Domain.Other; + +public class Rules where TPiece : Enum +{ + private Vector2 boardSize; + private TPiece theKing; + private Dictionary> piecePaths; + + /// + /// Begin a new set of rules. If any rules already exist, this method will erase them. + /// + /// The size of the game board in tiles. For examples, Chess is 8x8 and Shogi is 9x9. + /// The piece that represents the King for each player or the piece that, when lost, results in losing the game. + public Rules CreateNewRules(Vector2 boardSize, TPiece king) + { + this.boardSize = boardSize; + theKing = king; + piecePaths = []; + return this; + } + + public Rules RegisterPieceWithRules(PieceRulesRegistration pieceToRegister) + { + if (piecePaths.ContainsKey(pieceToRegister.WhichPiece)) + { + throw new ArgumentException("This type of piece has already been registered.", nameof(pieceToRegister)); + } + + piecePaths.Add(pieceToRegister.WhichPiece, pieceToRegister.MoveSet); + return this; + } + + public int ValidateMove(Vector2 start, Vector2 end, TPiece[][] board) + { + if (board.GetLength(0) != boardSize.X || board.GetLength(1) != boardSize.Y) + { + throw new ArgumentException($"2D array dimensions must match boardSize given during {nameof(CreateNewRules)} method.", nameof(board)); + } + if (start - start != Vector2.Zero) + { + throw new ArgumentException("Negative values not allowed.", nameof(start)); + } + if (end - end != Vector2.Zero) + { + throw new ArgumentException("Negative values not allowed.", nameof(end)); + } + if (start.X >= boardSize.X || start.Y >= boardSize.Y) + { + throw new ArgumentException("Start position must be within the given boardSize.", nameof(start)); + } + if (end.X >= boardSize.X || end.Y >= boardSize.Y) + { + throw new ArgumentException("End position must be within the given boardSize.", nameof(end)); + } + + + } +} \ No newline at end of file diff --git a/Shogi.Domain/ValueObjects/Bishop.cs b/Shogi.Domain/ValueObjects/Bishop.cs index 8302db9..7f7d315 100644 --- a/Shogi.Domain/ValueObjects/Bishop.cs +++ b/Shogi.Domain/ValueObjects/Bishop.cs @@ -7,22 +7,22 @@ namespace Shogi.Domain.ValueObjects { private static readonly ReadOnlyCollection BishopPaths = new(new List(4) { - new Path(Direction.UpLeft, Distance.MultiStep), - new Path(Direction.UpRight, Distance.MultiStep), - new Path(Direction.DownLeft, Distance.MultiStep), - new Path(Direction.DownRight, Distance.MultiStep) + new Path(Direction.ForwardLeft, Distance.MultiStep), + new Path(Direction.ForwardRight, Distance.MultiStep), + new Path(Direction.BackwardLeft, Distance.MultiStep), + new Path(Direction.BackwardRight, Distance.MultiStep) }); public static readonly ReadOnlyCollection PromotedBishopPaths = new(new List(8) { - new Path(Direction.Up), + new Path(Direction.Forward), new Path(Direction.Left), new Path(Direction.Right), - new Path(Direction.Down), - new Path(Direction.UpLeft, Distance.MultiStep), - new Path(Direction.UpRight, Distance.MultiStep), - new Path(Direction.DownLeft, Distance.MultiStep), - new Path(Direction.DownRight, Distance.MultiStep) + new Path(Direction.Backward), + new Path(Direction.ForwardLeft, Distance.MultiStep), + new Path(Direction.ForwardRight, Distance.MultiStep), + new Path(Direction.BackwardLeft, Distance.MultiStep), + new Path(Direction.BackwardRight, Distance.MultiStep) }); public static readonly ReadOnlyCollection Player2Paths = diff --git a/Shogi.Domain/ValueObjects/BoardState.cs b/Shogi.Domain/ValueObjects/BoardState.cs index e1b4ba9..a8c3dbb 100644 --- a/Shogi.Domain/ValueObjects/BoardState.cs +++ b/Shogi.Domain/ValueObjects/BoardState.cs @@ -6,247 +6,249 @@ namespace Shogi.Domain.ValueObjects; public class BoardState { - /// - /// Board state before any moves have been made, using standard setup and rules. - /// - public static BoardState StandardStarting => new( - state: BuildStandardStartingBoardState(), - player1Hand: new(), - player2Hand: new(), - whoseTurn: WhichPlayer.Player1, - playerInCheck: null, - previousMove: new Move()); + /// + /// Board state before any moves have been made, using standard setup and rules. + /// + public static BoardState StandardStarting => new( + state: BuildStandardStartingBoardState(), + player1Hand: [], + player2Hand: [], + whoseTurn: WhichPlayer.Player1, + playerInCheck: null, + previousMove: new Move()); - /// - /// Key is position notation, such as "E4". - /// - private readonly Dictionary board; + /// + /// Key is position notation, such as "E4". + /// + private readonly Dictionary board; - public BoardState( - Dictionary state, - List player1Hand, - List player2Hand, - WhichPlayer whoseTurn, - WhichPlayer? playerInCheck, - Move previousMove) - { - board = state; - Player1Hand = player1Hand; - Player2Hand = player2Hand; - PreviousMove = previousMove; - WhoseTurn = whoseTurn; - InCheck = playerInCheck; - } + public BoardState( + Dictionary state, + List player1Hand, + List player2Hand, + WhichPlayer whoseTurn, + WhichPlayer? playerInCheck, + Move previousMove) + { + board = state; + Player1Hand = player1Hand; + Player2Hand = player2Hand; + PreviousMove = previousMove; + WhoseTurn = whoseTurn; + InCheck = playerInCheck; + } - /// - /// Copy constructor. - /// - public BoardState(BoardState other) - { - board = new(81); - foreach (var kvp in other.board) - { - var piece = kvp.Value; - board[kvp.Key] = piece == null ? null : Piece.Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); - } - WhoseTurn = other.WhoseTurn; - InCheck = other.InCheck; - IsCheckmate = other.IsCheckmate; - PreviousMove = other.PreviousMove; - Player1Hand = new(other.Player1Hand); - Player2Hand = new(other.Player2Hand); - } + /// + /// Copy constructor. + /// + public BoardState(BoardState other) + { + board = new(81); + foreach (var kvp in other.board) + { + var piece = kvp.Value; + board[kvp.Key] = piece == null ? null : Piece.Create(piece.WhichPiece, piece.Owner, piece.IsPromoted); + } + WhoseTurn = other.WhoseTurn; + InCheck = other.InCheck; + IsCheckmate = other.IsCheckmate; + PreviousMove = other.PreviousMove; + Player1Hand = new(other.Player1Hand); + Player2Hand = new(other.Player2Hand); + } - public ReadOnlyDictionary State => new(board); - public List ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; - public Vector2 Player1KingPosition => Notation.FromBoardNotation(board.Where(kvp => kvp.Value != null).Single(kvp => - { - var piece = kvp.Value; - return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1; - }).Key); - public Vector2 Player2KingPosition => Notation.FromBoardNotation(board.Where(kvp => kvp.Value != null).Single(kvp => - { - var piece = kvp.Value; - return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2; - }).Key); - public List Player1Hand { get; } - public List Player2Hand { get; } - public Move PreviousMove { get; set; } - public WhichPlayer WhoseTurn { get; set; } - public WhichPlayer? InCheck { get; set; } - public bool IsCheckmate { get; set; } + public ReadOnlyDictionary State => new(board); + public List ActivePlayerHand => WhoseTurn == WhichPlayer.Player1 ? Player1Hand : Player2Hand; + public Vector2 Player1KingPosition => Notation.FromBoardNotation(board.Where(kvp => kvp.Value != null).Single(kvp => + { + var piece = kvp.Value; + return piece!.IsKing() && piece!.Owner == WhichPlayer.Player1; + }).Key); + public Vector2 Player2KingPosition => Notation.FromBoardNotation(board.Where(kvp => kvp.Value != null).Single(kvp => + { + var piece = kvp.Value; + return piece!.IsKing() && piece!.Owner == WhichPlayer.Player2; + }).Key); + public List Player1Hand { get; } + public List Player2Hand { get; } + public Move PreviousMove { get; set; } + public WhichPlayer WhoseTurn { get; set; } + public WhichPlayer? InCheck { get; set; } + public bool IsCheckmate { get; set; } - public Piece? this[string notation] - { - // TODO: Validate "notation" here and throw an exception if invalid. - get => board[notation]; - set => board[notation] = value; - } + public Piece? this[string notation] + { + // TODO: Validate "notation" here and throw an exception if invalid. + get => board[notation]; + set => board[notation] = value; + } - public Piece? this[Vector2 vector] - { - get => this[Notation.ToBoardNotation(vector)]; - set => this[Notation.ToBoardNotation(vector)] = value; - } + public Piece? this[Vector2 vector] + { + get => this[Notation.ToBoardNotation(vector)]; + set => this[Notation.ToBoardNotation(vector)] = value; + } - public Piece? this[int x, int y] - { - get => this[Notation.ToBoardNotation(x, y)]; - set => this[Notation.ToBoardNotation(x, y)] = value; - } + public Piece? this[int x, int y] + { + get => this[Notation.ToBoardNotation(x, y)]; + set => this[Notation.ToBoardNotation(x, y)] = value; + } - /// - /// Returns true if the given path can be traversed without colliding into a piece. - /// - public bool IsPathBlocked(IEnumerable path) - { - return !path.Any() - || path.SkipLast(1).Any(position => this[position] != null) - || this[path.Last()]?.Owner == WhoseTurn; - } + /// + /// Returns true if the given path can be traversed without colliding into a piece. + /// + public bool IsPathBlocked(IEnumerable path) + { + return !path.Any() + || path.SkipLast(1).Any(position => this[position] != null) + || this[path.Last()]?.Owner == WhoseTurn; + } - internal bool IsWithinPromotionZone(Vector2 position) - { - // TODO: Move this promotion zone logic into the StandardRules class. - return WhoseTurn == WhichPlayer.Player1 && position.Y > 5 - || WhoseTurn == WhichPlayer.Player2 && position.Y < 3; - } + internal bool IsWithinPromotionZone(Vector2 position) + { - internal static bool IsWithinBoardBoundary(Vector2 position) - { - return position.X <= 8 && position.X >= 0 - && position.Y <= 8 && position.Y >= 0; - } - internal List GetTilesOccupiedBy(WhichPlayer whichPlayer) => board - .Where(kvp => kvp.Value?.Owner == whichPlayer) - .Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!)) - .ToList(); + // TODO: Move this promotion zone logic into the StandardRules class. + return WhoseTurn == WhichPlayer.Player1 && position.Y > 5 + || WhoseTurn == WhichPlayer.Player2 && position.Y < 3; + } - internal void Capture(Vector2 to) - { - var piece = this[to]; - if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist."); + internal static bool IsWithinBoardBoundary(Vector2 position) + { + return position.X <= 8 && position.X >= 0 + && position.Y <= 8 && position.Y >= 0; + } - piece.Capture(WhoseTurn); - ActivePlayerHand.Add(piece); - } + internal List GetTilesOccupiedBy(WhichPlayer whichPlayer) => board + .Where(kvp => kvp.Value?.Owner == whichPlayer) + .Select(kvp => new BoardTile(Notation.FromBoardNotation(kvp.Key), kvp.Value!)) + .ToList(); - /// - /// Does not include the start position. - /// - internal static IEnumerable GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction) - { - var next = start; - while (IsWithinBoardBoundary(next + direction)) - { - next += direction; - yield return next; - } - } + internal void Capture(Vector2 to) + { + var piece = this[to]; + if (piece == null) throw new InvalidOperationException("Cannot capture. Piece at position does not exist."); - internal Piece? QueryFirstPieceInPath(IEnumerable path) - { - foreach (var step in path) - { - if (this[step] != null) return this[step]; - } - return null; - } + piece.Capture(WhoseTurn); + ActivePlayerHand.Add(piece); + } - private static Dictionary BuildStandardStartingBoardState() - { - return new Dictionary(81) - { - ["A1"] = new Lance(WhichPlayer.Player1), - ["B1"] = new Knight(WhichPlayer.Player1), - ["C1"] = new SilverGeneral(WhichPlayer.Player1), - ["D1"] = new GoldGeneral(WhichPlayer.Player1), - ["E1"] = new King(WhichPlayer.Player1), - ["F1"] = new GoldGeneral(WhichPlayer.Player1), - ["G1"] = new SilverGeneral(WhichPlayer.Player1), - ["H1"] = new Knight(WhichPlayer.Player1), - ["I1"] = new Lance(WhichPlayer.Player1), + /// + /// Does not include the start position. + /// + internal static IEnumerable GetPathAlongDirectionFromStartToEdgeOfBoard(Vector2 start, Vector2 direction) + { + var next = start; + while (IsWithinBoardBoundary(next + direction)) + { + next += direction; + yield return next; + } + } - ["A2"] = null, - ["B2"] = new Bishop(WhichPlayer.Player1), - ["C2"] = null, - ["D2"] = null, - ["E2"] = null, - ["F2"] = null, - ["G2"] = null, - ["H2"] = new Rook(WhichPlayer.Player1), - ["I2"] = null, + internal Piece? QueryFirstPieceInPath(IEnumerable path) + { + foreach (var step in path) + { + if (this[step] != null) return this[step]; + } + return null; + } - ["A3"] = new Pawn(WhichPlayer.Player1), - ["B3"] = new Pawn(WhichPlayer.Player1), - ["C3"] = new Pawn(WhichPlayer.Player1), - ["D3"] = new Pawn(WhichPlayer.Player1), - ["E3"] = new Pawn(WhichPlayer.Player1), - ["F3"] = new Pawn(WhichPlayer.Player1), - ["G3"] = new Pawn(WhichPlayer.Player1), - ["H3"] = new Pawn(WhichPlayer.Player1), - ["I3"] = new Pawn(WhichPlayer.Player1), + private static Dictionary BuildStandardStartingBoardState() + { + return new Dictionary(81) + { + ["A1"] = new Lance(WhichPlayer.Player1), + ["B1"] = new Knight(WhichPlayer.Player1), + ["C1"] = new SilverGeneral(WhichPlayer.Player1), + ["D1"] = new GoldGeneral(WhichPlayer.Player1), + ["E1"] = new King(WhichPlayer.Player1), + ["F1"] = new GoldGeneral(WhichPlayer.Player1), + ["G1"] = new SilverGeneral(WhichPlayer.Player1), + ["H1"] = new Knight(WhichPlayer.Player1), + ["I1"] = new Lance(WhichPlayer.Player1), - ["A4"] = null, - ["B4"] = null, - ["C4"] = null, - ["D4"] = null, - ["E4"] = null, - ["F4"] = null, - ["G4"] = null, - ["H4"] = null, - ["I4"] = null, + ["A2"] = null, + ["B2"] = new Bishop(WhichPlayer.Player1), + ["C2"] = null, + ["D2"] = null, + ["E2"] = null, + ["F2"] = null, + ["G2"] = null, + ["H2"] = new Rook(WhichPlayer.Player1), + ["I2"] = null, - ["A5"] = null, - ["B5"] = null, - ["C5"] = null, - ["D5"] = null, - ["E5"] = null, - ["F5"] = null, - ["G5"] = null, - ["H5"] = null, - ["I5"] = null, + ["A3"] = new Pawn(WhichPlayer.Player1), + ["B3"] = new Pawn(WhichPlayer.Player1), + ["C3"] = new Pawn(WhichPlayer.Player1), + ["D3"] = new Pawn(WhichPlayer.Player1), + ["E3"] = new Pawn(WhichPlayer.Player1), + ["F3"] = new Pawn(WhichPlayer.Player1), + ["G3"] = new Pawn(WhichPlayer.Player1), + ["H3"] = new Pawn(WhichPlayer.Player1), + ["I3"] = new Pawn(WhichPlayer.Player1), - ["A6"] = null, - ["B6"] = null, - ["C6"] = null, - ["D6"] = null, - ["E6"] = null, - ["F6"] = null, - ["G6"] = null, - ["H6"] = null, - ["I6"] = null, + ["A4"] = null, + ["B4"] = null, + ["C4"] = null, + ["D4"] = null, + ["E4"] = null, + ["F4"] = null, + ["G4"] = null, + ["H4"] = null, + ["I4"] = null, - ["A7"] = new Pawn(WhichPlayer.Player2), - ["B7"] = new Pawn(WhichPlayer.Player2), - ["C7"] = new Pawn(WhichPlayer.Player2), - ["D7"] = new Pawn(WhichPlayer.Player2), - ["E7"] = new Pawn(WhichPlayer.Player2), - ["F7"] = new Pawn(WhichPlayer.Player2), - ["G7"] = new Pawn(WhichPlayer.Player2), - ["H7"] = new Pawn(WhichPlayer.Player2), - ["I7"] = new Pawn(WhichPlayer.Player2), + ["A5"] = null, + ["B5"] = null, + ["C5"] = null, + ["D5"] = null, + ["E5"] = null, + ["F5"] = null, + ["G5"] = null, + ["H5"] = null, + ["I5"] = null, - ["A8"] = null, - ["B8"] = new Rook(WhichPlayer.Player2), - ["C8"] = null, - ["D8"] = null, - ["E8"] = null, - ["F8"] = null, - ["G8"] = null, - ["H8"] = new Bishop(WhichPlayer.Player2), - ["I8"] = null, + ["A6"] = null, + ["B6"] = null, + ["C6"] = null, + ["D6"] = null, + ["E6"] = null, + ["F6"] = null, + ["G6"] = null, + ["H6"] = null, + ["I6"] = null, - ["A9"] = new Lance(WhichPlayer.Player2), - ["B9"] = new Knight(WhichPlayer.Player2), - ["C9"] = new SilverGeneral(WhichPlayer.Player2), - ["D9"] = new GoldGeneral(WhichPlayer.Player2), - ["E9"] = new King(WhichPlayer.Player2), - ["F9"] = new GoldGeneral(WhichPlayer.Player2), - ["G9"] = new SilverGeneral(WhichPlayer.Player2), - ["H9"] = new Knight(WhichPlayer.Player2), - ["I9"] = new Lance(WhichPlayer.Player2) - }; - } + ["A7"] = new Pawn(WhichPlayer.Player2), + ["B7"] = new Pawn(WhichPlayer.Player2), + ["C7"] = new Pawn(WhichPlayer.Player2), + ["D7"] = new Pawn(WhichPlayer.Player2), + ["E7"] = new Pawn(WhichPlayer.Player2), + ["F7"] = new Pawn(WhichPlayer.Player2), + ["G7"] = new Pawn(WhichPlayer.Player2), + ["H7"] = new Pawn(WhichPlayer.Player2), + ["I7"] = new Pawn(WhichPlayer.Player2), + + ["A8"] = null, + ["B8"] = new Rook(WhichPlayer.Player2), + ["C8"] = null, + ["D8"] = null, + ["E8"] = null, + ["F8"] = null, + ["G8"] = null, + ["H8"] = new Bishop(WhichPlayer.Player2), + ["I8"] = null, + + ["A9"] = new Lance(WhichPlayer.Player2), + ["B9"] = new Knight(WhichPlayer.Player2), + ["C9"] = new SilverGeneral(WhichPlayer.Player2), + ["D9"] = new GoldGeneral(WhichPlayer.Player2), + ["E9"] = new King(WhichPlayer.Player2), + ["F9"] = new GoldGeneral(WhichPlayer.Player2), + ["G9"] = new SilverGeneral(WhichPlayer.Player2), + ["H9"] = new Knight(WhichPlayer.Player2), + ["I9"] = new Lance(WhichPlayer.Player2) + }; + } } diff --git a/Shogi.Domain/ValueObjects/GoldGeneral.cs b/Shogi.Domain/ValueObjects/GoldGeneral.cs index 38c22d1..f54aa16 100644 --- a/Shogi.Domain/ValueObjects/GoldGeneral.cs +++ b/Shogi.Domain/ValueObjects/GoldGeneral.cs @@ -7,12 +7,12 @@ namespace Shogi.Domain.ValueObjects { public static readonly ReadOnlyCollection Player1Paths = new(new List(6) { - new Path(Direction.Up), - new Path(Direction.UpLeft), - new Path(Direction.UpRight), + new Path(Direction.Forward), + new Path(Direction.ForwardLeft), + new Path(Direction.ForwardRight), new Path(Direction.Left), new Path(Direction.Right), - new Path(Direction.Down) + new Path(Direction.Backward) }); public static readonly ReadOnlyCollection Player2Paths = diff --git a/Shogi.Domain/ValueObjects/King.cs b/Shogi.Domain/ValueObjects/King.cs index 8ecb744..5238e29 100644 --- a/Shogi.Domain/ValueObjects/King.cs +++ b/Shogi.Domain/ValueObjects/King.cs @@ -7,14 +7,14 @@ namespace Shogi.Domain.ValueObjects { internal static readonly ReadOnlyCollection KingPaths = new(new List(8) { - new Path(Direction.Up), + new Path(Direction.Forward), new Path(Direction.Left), new Path(Direction.Right), - new Path(Direction.Down), - new Path(Direction.UpLeft), - new Path(Direction.UpRight), - new Path(Direction.DownLeft), - new Path(Direction.DownRight) + new Path(Direction.Backward), + new Path(Direction.ForwardLeft), + new Path(Direction.ForwardRight), + new Path(Direction.BackwardLeft), + new Path(Direction.BackwardRight) }); public King(WhichPlayer owner, bool isPromoted = false) diff --git a/Shogi.Domain/ValueObjects/Lance.cs b/Shogi.Domain/ValueObjects/Lance.cs index 3f008d3..690a77c 100644 --- a/Shogi.Domain/ValueObjects/Lance.cs +++ b/Shogi.Domain/ValueObjects/Lance.cs @@ -7,7 +7,7 @@ namespace Shogi.Domain.ValueObjects { public static readonly ReadOnlyCollection Player1Paths = new(new List(1) { - new Path(Direction.Up, Distance.MultiStep), + new Path(Direction.Forward, Distance.MultiStep), }); public static readonly ReadOnlyCollection Player2Paths = diff --git a/Shogi.Domain/ValueObjects/Pawn.cs b/Shogi.Domain/ValueObjects/Pawn.cs index 016e86a..6494f74 100644 --- a/Shogi.Domain/ValueObjects/Pawn.cs +++ b/Shogi.Domain/ValueObjects/Pawn.cs @@ -7,7 +7,7 @@ namespace Shogi.Domain.ValueObjects { public static readonly ReadOnlyCollection Player1Paths = new(new List(1) { - new Path(Direction.Up) + new Path(Direction.Forward) }); public static readonly ReadOnlyCollection Player2Paths = diff --git a/Shogi.Domain/ValueObjects/Rook.cs b/Shogi.Domain/ValueObjects/Rook.cs index f87dd19..45ab9d6 100644 --- a/Shogi.Domain/ValueObjects/Rook.cs +++ b/Shogi.Domain/ValueObjects/Rook.cs @@ -7,22 +7,22 @@ public record class Rook : Piece { public static readonly ReadOnlyCollection Player1Paths = new(new List(4) { - new Path(Direction.Up, Distance.MultiStep), + new Path(Direction.Forward, Distance.MultiStep), new Path(Direction.Left, Distance.MultiStep), new Path(Direction.Right, Distance.MultiStep), - new Path(Direction.Down, Distance.MultiStep) + new Path(Direction.Backward, Distance.MultiStep) }); private static readonly ReadOnlyCollection PromotedPlayer1Paths = new(new List(8) { - new Path(Direction.Up, Distance.MultiStep), + new Path(Direction.Forward, Distance.MultiStep), new Path(Direction.Left, Distance.MultiStep), new Path(Direction.Right, Distance.MultiStep), - new Path(Direction.Down, Distance.MultiStep), - new Path(Direction.UpLeft), - new Path(Direction.UpRight), - new Path(Direction.DownLeft), - new Path(Direction.DownRight) + new Path(Direction.Backward, Distance.MultiStep), + new Path(Direction.ForwardLeft), + new Path(Direction.ForwardRight), + new Path(Direction.BackwardLeft), + new Path(Direction.BackwardRight) }); public static readonly ReadOnlyCollection Player2Paths = diff --git a/Shogi.Domain/ValueObjects/SilverGeneral.cs b/Shogi.Domain/ValueObjects/SilverGeneral.cs index 9d332a4..a37becb 100644 --- a/Shogi.Domain/ValueObjects/SilverGeneral.cs +++ b/Shogi.Domain/ValueObjects/SilverGeneral.cs @@ -7,11 +7,11 @@ namespace Shogi.Domain.ValueObjects { public static readonly ReadOnlyCollection Player1Paths = new(new List(4) { - new Path(Direction.Up), - new Path(Direction.UpLeft), - new Path(Direction.UpRight), - new Path(Direction.DownLeft), - new Path(Direction.DownRight) + new Path(Direction.Forward), + new Path(Direction.ForwardLeft), + new Path(Direction.ForwardRight), + new Path(Direction.BackwardLeft), + new Path(Direction.BackwardRight) }); public static readonly ReadOnlyCollection Player2Paths = diff --git a/Shogi.Domain/ValueObjects/WhichPiece.cs b/Shogi.Domain/ValueObjects/WhichPiece.cs index 604909b..cf7e1b7 100644 --- a/Shogi.Domain/ValueObjects/WhichPiece.cs +++ b/Shogi.Domain/ValueObjects/WhichPiece.cs @@ -1,14 +1,19 @@ -namespace Shogi.Domain.ValueObjects +namespace Shogi.Domain.ValueObjects; + +public enum WhichPiece { - public enum WhichPiece - { - King, - GoldGeneral, - SilverGeneral, - Bishop, - Rook, - Knight, - Lance, - Pawn - } + King, + GoldGeneral, + SilverGeneral, + PromotedSilverGeneral, + Bishop, + PromotedBishop, + Rook, + PromotedRook, + Knight, + PromotedKnight, + Lance, + PromotedLance, + Pawn + PromotedPawn } diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Direction.cs b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Direction.cs index 622d436..24dc12e 100644 --- a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Direction.cs +++ b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Direction.cs @@ -1,22 +1,15 @@ -using System.Numerics; +namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; -namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing -{ - /// - /// Directions are relative to the perspective of Player 1. - /// Up points towards player 1. Down points towards player 2. - /// - public static class Direction - { - public static readonly Vector2 Up = new(0, 1); - public static readonly Vector2 Down = new(0, -1); - public static readonly Vector2 Left = new(-1, 0); - public static readonly Vector2 Right = new(1, 0); - public static readonly Vector2 UpLeft = new(-1, 1); - public static readonly Vector2 UpRight = new(1, 1); - public static readonly Vector2 DownLeft = new(-1, -1); - public static readonly Vector2 DownRight = new(1, -1); - public static readonly Vector2 KnightLeft = new(-1, 2); - public static readonly Vector2 KnightRight = new(1, 2); - } -} +public static class Direction + { + public static readonly Vector2 Forward = new(0, 1); + public static readonly Vector2 Backward = new(0, -1); + public static readonly Vector2 Left = new(-1, 0); + public static readonly Vector2 Right = new(1, 0); + public static readonly Vector2 ForwardLeft = new(-1, 1); + public static readonly Vector2 ForwardRight = new(1, 1); + public static readonly Vector2 BackwardLeft = new(-1, -1); + public static readonly Vector2 BackwardRight = new(1, -1); + public static readonly Vector2 KnightLeft = new(-1, 2); + public static readonly Vector2 KnightRight = new(1, 2); + } diff --git a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs index 2b8042a..3fba1a6 100644 --- a/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs +++ b/Shogi.Domain/YetToBeAssimilatedIntoDDD/Pathing/Path.cs @@ -1,41 +1,33 @@ using System.Diagnostics; -namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing +namespace Shogi.Domain.YetToBeAssimilatedIntoDDD.Pathing; + +[DebuggerDisplay("{Direction} - {Distance}")] +public record Path(Vector2 Direction, Distance Distance = Distance.OneStep) { - [DebuggerDisplay("{Direction} - {Distance}")] - public class Path - { - public Vector2 Direction { get; } - public Distance Distance { get; } - public Path(Vector2 direction, Distance distance = Distance.OneStep) - { - Direction = direction; - Distance = distance; - } - public Path Invert() => new(Vector2.Negate(Direction), Distance); - } - - public static class PathExtensions - { - public static Path GetNearestPath(this IEnumerable paths, Vector2 start, Vector2 end) - { - if (!paths.DefaultIfEmpty().Any()) - { - throw new ArgumentException("No paths to get nearest path from."); - } - - var shortestPath = paths.First(); - foreach (var path in paths.Skip(1)) - { - var distance = Vector2.Distance(start + path.Direction, end); - var shortestDistance = Vector2.Distance(start + shortestPath.Direction, end); - if (distance < shortestDistance) - { - shortestPath = path; - } - } - return shortestPath; - } - } + public Path Invert() => new(Vector2.Negate(Direction), Distance); +} + +public static class PathExtensions +{ + public static Path GetNearestPath(this IEnumerable paths, Vector2 start, Vector2 end) + { + if (!paths.DefaultIfEmpty().Any()) + { + throw new ArgumentException("No paths to get nearest path from."); + } + + var shortestPath = paths.First(); + foreach (var path in paths.Skip(1)) + { + var distance = Vector2.Distance(start + path.Direction, end); + var shortestDistance = Vector2.Distance(start + shortestPath.Direction, end); + if (distance < shortestDistance) + { + shortestPath = path; + } + } + return shortestPath; + } } diff --git a/Shogi.sln b/Shogi.sln index 36312b7..bba4cc1 100644 --- a/Shogi.sln +++ b/Shogi.sln @@ -15,6 +15,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shogi.Contracts", "Shogi.Co EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E69DE334-29A7-46AE-9647-54DC0187CD8D}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig .gitignore = .gitignore azure-pipelines.yml = azure-pipelines.yml global.json = global.json diff --git a/Tests/UnitTests/RookShould.cs b/Tests/UnitTests/RookShould.cs index b472c4a..e1d9b81 100644 --- a/Tests/UnitTests/RookShould.cs +++ b/Tests/UnitTests/RookShould.cs @@ -22,10 +22,10 @@ public class RookShould { var moveSet = rook1.MoveSet; moveSet.Should().HaveCount(4); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); } [Fact] @@ -38,14 +38,14 @@ public class RookShould // Assert var moveSet = rook1.MoveSet; moveSet.Should().HaveCount(8); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.UpLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.DownLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.UpRight, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.DownRight, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardRight, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardRight, Distance.OneStep)); } [Fact] @@ -53,10 +53,10 @@ public class RookShould { var moveSet = rook2.MoveSet; moveSet.Should().HaveCount(4); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); } [Fact] @@ -69,14 +69,14 @@ public class RookShould // Assert var moveSet = rook2.MoveSet; moveSet.Should().HaveCount(8); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Up, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Forward, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Left, Distance.MultiStep)); moveSet.Should().ContainEquivalentOf(new Path(Direction.Right, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.Down, Distance.MultiStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.UpLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.DownLeft, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.UpRight, Distance.OneStep)); - moveSet.Should().ContainEquivalentOf(new Path(Direction.DownRight, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.Backward, Distance.MultiStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardLeft, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.ForwardRight, Distance.OneStep)); + moveSet.Should().ContainEquivalentOf(new Path(Direction.BackwardRight, Distance.OneStep)); }