Skip to content

Commit

Permalink
feat(Qi): reimplementation for enhanced flexibility
Browse files Browse the repository at this point in the history
This commit makes several enhancements to the `Qi` class implementation, aimed at increasing its flexibility to handle a broader variety of input types. Specific updates include:

1. Allow pieces to be represented by any object type, not just strings.
2. Allow the keys of `squares_hash` to be any object type.
3. Allow items from `turns` to be any object type.
4. Ensure `state` values can also be any object type.
5. `state` keys are now converted to symbols to ensure consistent access.

These changes improve the versatility of the `Qi` class, enabling it to handle a wider range of game scenarios and increasing its usefulness for building board game software.
  • Loading branch information
cyril committed May 29, 2023
1 parent 5092650 commit 90281cd
Show file tree
Hide file tree
Showing 11 changed files with 327 additions and 436 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,6 @@ Metrics/ModuleLength:

Naming/ConstantName:
Enabled: false

Style/NumericPredicate:
EnforcedStyle: comparison
3 changes: 3 additions & 0 deletions .rubocop.yml.erb
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,6 @@ Metrics/ModuleLength:

Naming/ConstantName:
Enabled: false

Style/NumericPredicate:
EnforcedStyle: comparison
2 changes: 1 addition & 1 deletion .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --no-auto-gen-timestamp`
# using RuboCop version 1.50.2.
# using RuboCop version 1.51.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand Down
8 changes: 3 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
PATH
remote: .
specs:
qi (10.0.0.beta12)
kernel-boolean
qi (10.0.0)

GEM
remote: https://rubygems.org/
Expand All @@ -15,7 +14,6 @@ GEM
docile (1.4.0)
expresenter (1.4.0)
json (2.6.3)
kernel-boolean (1.0.1)
matchi (3.3.1)
parallel (1.23.0)
parser (3.2.2.1)
Expand Down Expand Up @@ -44,13 +42,13 @@ GEM
parser (>= 3.2.1.0)
rubocop-capybara (2.18.0)
rubocop (~> 1.41)
rubocop-factory_bot (2.22.0)
rubocop-factory_bot (2.23.1)
rubocop (~> 1.33)
rubocop-gitlab-security (0.1.1)
rubocop (>= 0.51)
rubocop-md (1.2.0)
rubocop (>= 1.0)
rubocop-performance (1.17.1)
rubocop-performance (1.18.0)
rubocop (>= 1.7.0, < 2.0)
rubocop-ast (>= 0.4.0)
rubocop-rake (0.6.0)
Expand Down
111 changes: 63 additions & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
[![RuboCop](https://github.com/sashite/qi.rb/workflows/RuboCop/badge.svg?branch=main)](https://github.com/sashite/qi.rb/actions?query=workflow%3Arubocop+branch%3Amain)
[![License](https://img.shields.io/github/license/sashite/qi.rb?label=License&logo=github)](https://github.com/sashite/qi.rb/raw/main/LICENSE.md)

Welcome to `Qi` (Chinese: 棋; pinyin: __), a flexible and customizable library designed to represent and manipulate board game positions. `Qi` is ideal for a variety of board games, including chess, shogi, xiangqi, makruk, and more.
**Qi** (Chinese: 棋; pinyin: __) is a lightweight, flexible, and adaptable tool for representing board game positions, built in Ruby. It is designed to be game-agnostic and can be used with a variety of board games such as Chess, Four-Player Chess, Go, Makruk, Shogi, and Xiangqi.

With `Qi`, you can easily track the game state, including which pieces are on the board and where, as well as any captured pieces. The library allows for the application of game moves, updating the state of the game and generating a new position, all while preserving the original state.
Qi uses a unique approach where the state of a game is represented through capturing the pieces in play, the arrangement of pieces on the board, the sequence of turns, and other possible states that a game can have.

## Features:
## Features

- **Flexible representation of game states:** `Qi`'s design allows it to adapt to different games with varying rules and pieces.
- **Immutable positions:** Every move generates a new position, preserving the original one. This is particularly useful for scenarios like undoing moves or exploring potential future states in the game.
- **Compact serialization:** `Qi` provides a compact string serialization method for game states, which is useful for saving game progress or transmitting game states over the network.
- **Check state tracking:** In addition to the positions and captured pieces, `Qi` also allows tracking of specific game states such as check in chess.
1. **Game Agnostic:** Qi can be used to represent board game positions for a wide variety of games. Whether you are playing Chess, Makruk, Shogi, or Xiangqi, Qi's flexible structure allows you to accurately capture the state of your game.
2. **Flexible Position Representation:** Qi captures the state of the game by recording the pieces in play, their arrangement on the board, the sequence of turns, and other additional states of the game. This enables a comprehensive view of the game at any given point.
3. **State Manipulation:** Qi allows for manipulation and update of game states through the `commit` method, allowing transitions between game states.
4. **Equality Checks:** With the `eql?` method, Qi allows for comparisons between different game states, which can be useful for tracking game progress, detecting repeats, or even in creating AI for your games.
5. **Turn Management:** Qi keeps track of the sequence of turns allowing users to identify whose turn it is currently.
6. **Access to Game Data:** Qi provides methods to access the current arrangement of pieces on the board (`squares_hash`) and the pieces captured by each player (`captures_hash`), helping users understand the current status of the game. It also allows access to a list of captured pieces (`captures_array`).
7. **Customizability:** Qi is flexible and allows for customization as per your needs. The keys and values of the `captures_hash` and `squares_hash` can be any kind of object, as well as the items from `turns` and values from `state`.

While `Qi` does not generate game moves itself, it serves as a solid foundation upon which game engines can be built. Its design is focused on providing a robust and adaptable representation of game states, paving the way for the development of diverse board game applications.

Expand All @@ -24,7 +27,7 @@ While `Qi` does not generate game moves itself, it serves as a solid foundation
Add this line to your application's Gemfile:

```ruby
gem "qi", ">= 10.0.0.beta12"
gem "qi"
```

And then execute:
Expand All @@ -36,58 +39,70 @@ bundle install
Or install it yourself as:

```sh
gem install qi --pre
gem install qi
```

## Example
## Usage

The following usage example is derived from a classic _tsume shogi_ (詰将棋) problem, which translates to _mate shogi_ - a popular genre of shogi problems where the goal is to checkmate the opponent's king.
In the provided setup, the attacking side is in possession of a silver general (S), a promoted bishop (+B) positioned on square 43, and a promoted pawn (+P) on square 22.

On the defending side, there is a king (k) situated on square 4, surrounded by two silver generals (s) on squares 3 and 5 respectively.

In this scenario, `Qi` allows us to represent the state of the game and apply changes as moves are made. Please follow the given example to understand how to create such a representation and how to update it:

```ruby
require "qi"

north_captures = %w[r r b g g g g s n n n n p p p p p p p p p p p p p p p p p]
south_captures = %w[S]
captures = north_captures + south_captures
squares = { 3 => "s", 4 => "k", 5 => "s", 22 => "+P", 43 => "+B" }

qi0 = Qi.new(*captures, **squares)

qi0.captures # => ["S", "b", "g", "g", "g", "g", "n", "n", "n", "n", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "r", "r", "s"]
qi0.squares # => {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 43=>"+B"}
qi0.in_check? # => false
qi0.not_in_check? # => true
qi0.north_turn? # => false
qi0.south_turn? # => true
qi0.serialize # => "{ S;b;g;g;g;g;n;n;n;n;p;p;p;p;p;p;p;p;p;p;p;p;p;p;p;p;p;r;r;s s@3;k@4;s@5;+P@22;+B@43 ."
qi0.inspect # => "<Qi { S;b;g;g;g;g;n;n;n;n;p;p;p;p;p;p;p;p;p;p;p;p;p;p;p;p;p;r;r;s s@3;k@4;s@5;+P@22;+B@43 .>"
qi0.to_a
# [false,
# ["S", "b", "g", "g", "g", "g", "n", "n", "n", "n", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "r", "r", "s"],
# {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 43=>"+B"},
# false]

qi1 = qi0.commit(is_in_check: true, 43 => nil, 13 => "+B")

qi1.captures # => ["S", "b", "g", "g", "g", "g", "n", "n", "n", "n", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "r", "r", "s"]
qi1.squares # => {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 13=>"+B"}
qi1.in_check? # => true
qi1.not_in_check? # => false
qi1.north_turn? # => true
qi1.south_turn? # => false
qi1.serialize # => "} S;b;g;g;g;g;n;n;n;n;p;p;p;p;p;p;p;p;p;p;p;p;p;p;p;p;p;r;r;s s@3;k@4;s@5;+B@13;+P@22 +"
qi1.inspect # => "<Qi } S;b;g;g;g;g;n;n;n;n;p;p;p;p;p;p;p;p;p;p;p;p;p;p;p;p;p;r;r;s s@3;k@4;s@5;+B@13;+P@22 +>"
qi1.to_a
# [true,
# ["S", "b", "g", "g", "g", "g", "n", "n", "n", "n", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "r", "r", "s"],
# {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 13=>"+B"},
# true]
# Initialize an array for each player's captured pieces
north_captures = %w[r r b g g g g s n n n n p p p p p p p p p p p p p p p p p]
south_captures = %w[S]

# Combine and count each player's captured pieces
captures = Hash.new(0)
(north_captures + south_captures).each { |piece| captures[piece] += 1 }

# Define the squares occupied by each piece on the board
squares = { 3 => "s", 4 => "k", 5 => "s", 22 => "+P", 43 => "+B" }

# Create a new game position
qi0 = Qi.new(captures, squares, [0, 1])

# Verify the properties of the game position
qi0.captures_array # => ["S", "b", "g", "g", "g", "g", "n", "n", "n", "n", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "r", "r", "s"]
qi0.captures_hash # => {"r"=>2, "b"=>1, "g"=>4, "s"=>1, "n"=>4, "p"=>17, "S"=>1}
qi0.squares_hash # => {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 43=>"+B"}
qi0.state # => {}
qi0.turn # => 0
qi0.turns # => [0, 1]
qi0.eql?(Qi.new(captures, squares, [0, 1])) # => true
qi0.eql?(Qi.new(captures, squares, [1, 0])) # => false

# Move a piece on the board and check the game state
qi1 = qi0.commit([], [], { 43 => nil, 13 => "+B" }, in_check: true)

qi1.captures_array # => ["S", "b", "g", "g", "g", "g", "n", "n", "n", "n", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "p", "r", "r", "s"]
qi1.captures_hash # => {"r"=>2, "b"=>1, "g"=>4, "s"=>1, "n"=>4, "p"=>17, "S"=>1}
qi1.squares_hash # => {3=>"s", 4=>"k", 5=>"s", 22=>"+P", 13=>"+B"}
qi1.state # => {:in_check=>true}
qi1.turn # => 1
qi1.turns # => [1, 0]
qi1.eql?(Qi.new(captures, squares, [0, 1])) # => false
qi1.eql?(Qi.new(captures, squares, [1, 0])) # => false
```

In this example, we first create a `Qi` object to represent a game position with `Qi.new`. Then, we check various aspects of the game state using the methods provided by `Qi`. After that, we create a new game state `qi1` by committing changes to the existing state `qi0`. Finally, we again check various aspects of the new game state.

## License

The code is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).

## About Sashité

This [gem](https://rubygems.org/gems/qi) is maintained by [Sashité](https://sashite.com/).
This [gem](https://rubygems.org/gems/qi) is proudly maintained and developed by [Sashité](https://sashite.com/). Our mission is to promote intercultural understanding and appreciation through the universal language of board games.

At Sashité, we believe in the power of games as a medium for sharing and appreciating the richness of different cultures. From Chinese to Japanese, and Western traditions, every culture has its unique representation in the world of board games, particularly in chess.

Our `Qi` gem is a testament to this belief - a flexible, efficient, and inclusive software that allows for the representation and interaction of diverse chess systems. This piece of software is not just a tool; it is a bridge connecting different cultures under the love of strategic play.

With some [lines of code](https://github.com/sashite/), let's share the beauty of Chinese, Japanese and Western cultures through the game of chess!
Join us in our journey as we continue to write [code](https://github.com/sashite/) to share the beauty of these cultures, one game at a time.
2 changes: 1 addition & 1 deletion VERSION.semver
Original file line number Diff line number Diff line change
@@ -1 +1 @@
10.0.0.beta12
10.0.0
83 changes: 39 additions & 44 deletions brutal/qi_brutal.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,61 +57,56 @@ header: |
}.freeze
subject: |
starting_position = Qi.new(**STARTING_POSITION_CONTEXT)
starting_position = Qi.new({}, STARTING_POSITION_CONTEXT, [0, 1])
%{moves}.reduce(starting_position) do |position, kwargs|
position.commit(**kwargs.transform_keys { |k| k.is_a?(::String) ? k.to_sym : k })
%{moves}.reduce(starting_position) do |position, args|
position.commit(*args.first(3), **args.fetch(3) { Hash.new })
end
contexts:
moves:
-
- { 56: nil, 47: "P" }
- [[], [], { 56: nil, 47: "P" }]
-
- { 56: nil, 47: "P" }
- { 3: nil, 11: "g" }
- [[], [], { 56: nil, 47: "P" }]
- [[], [], { 3: nil, 11: "g" }]
-
- { 56: nil, 47: "P" }
- { 3: nil, 11: "g" }
- { 64: nil, 24: "+B", capture: "P"}
- [[], [], { 56: nil, 47: "P" }]
- [[], [], { 3: nil, 11: "g" }]
- [["P"], [], { 64: nil, 24: "+B" }]
-
- { 56: nil, 47: "P" }
- { 3: nil, 11: "g" }
- { 64: nil, 24: "+B", capture: "P"}
- { 5: nil, 14: "g" }
- [[], [], { 56: nil, 47: "P" }]
- [[], [], { 3: nil, 11: "g" }]
- [["P"], [], { 64: nil, 24: "+B" }]
- [[], [], { 5: nil, 14: "g" }]
-
- { 56: nil, 47: "P" }
- { 3: nil, 11: "g" }
- { 64: nil, 24: "+B", capture: "P"}
- { 5: nil, 14: "g" }
- { 24: nil, 14: "+B", capture: "G"}
- [[], [], { 56: nil, 47: "P" }]
- [[], [], { 3: nil, 11: "g" }]
- [["P"], [], { 64: nil, 24: "+B" }]
- [[], [], { 5: nil, 14: "g" }]
- [["G"], [], { 24: nil, 14: "+B" }]
-
- { 56: nil, 47: "P" }
- { 3: nil, 11: "g" }
- { 64: nil, 24: "+B", capture: "P"}
- { 5: nil, 14: "g" }
- { 24: nil, 14: "+B", capture: "G"}
- { 4: nil, 3: "k" }
- [[], [], { 56: nil, 47: "P" }]
- [[], [], { 3: nil, 11: "g" }]
- [["P"], [], { 64: nil, 24: "+B" }]
- [[], [], { 5: nil, 14: "g" }]
- [["G"], [], { 24: nil, 14: "+B" }]
- [[], [], { 4: nil, 3: "k" }]
-
- { 56: nil, 47: "P" }
- { 3: nil, 11: "g" }
- { 64: nil, 24: "+B", capture: "P"}
- { 5: nil, 14: "g" }
- { 24: nil, 14: "+B", capture: "G"}
- { 4: nil, 3: "k" }
- { 13: "G", drop: "G"}
- [[], [], { 56: nil, 47: "P" }]
- [[], [], { 3: nil, 11: "g" }]
- [["P"], [], { 64: nil, 24: "+B" }]
- [[], [], { 5: nil, 14: "g" }]
- [["G"], [], { 24: nil, 14: "+B" }]
- [[], [], { 4: nil, 3: "k" }]
- [[], ["G"], { 13: "G" }, { note: "this is a drop" }]

actuals:
- "%{subject}.captures"
- "%{subject}.squares"
- "%{subject}.in_check?"
- "%{subject}.not_in_check?"
- "%{subject}.north_turn?"
- "%{subject}.south_turn?"
- "(%{subject} == Qi.new(*STARTING_POSITION_CONTEXT))"
- "%{subject}.eql?(Qi.new(*STARTING_POSITION_CONTEXT))"
- "%{subject}.to_a"
- "%{subject}.to_h"
- "%{subject}.hash"
- "%{subject}.serialize"
- "%{subject}.inspect"
- "%{subject}.captures_array"
- "%{subject}.captures_hash"
- "%{subject}.squares_hash"
- "%{subject}.state"
- "%{subject}.turn"
- "%{subject}.turns"
- "(%{subject} == Qi.new({}, STARTING_POSITION_CONTEXT, [0, 1]))"
- "%{subject}.eql?(Qi.new({}, STARTING_POSITION_CONTEXT, [0, 1]))"
Loading

0 comments on commit 90281cd

Please sign in to comment.