Made it work

This commit is contained in:
Lukas Wölfer
2020-02-12 00:50:31 +01:00
parent 37cb35c90e
commit d752ffb24c
25 changed files with 411 additions and 128 deletions

View File

@@ -5,6 +5,7 @@ verify_ssl = true
[dev-packages] [dev-packages]
mypy = "*" mypy = "*"
black = "*"
[packages] [packages]
pyautogui = "*" pyautogui = "*"
@@ -13,3 +14,6 @@ opencv-python = "*"
[requires] [requires]
python_version = "3.8" python_version = "3.8"
[pipenv]
allow_prereleases = true

71
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "41d1e74c7c7d13395735d3e515153460aa6b572d2ca18a8692dbf0d77cdd66fd" "sha256": "fa4a7a5c49ad466a4a7ae75d530e60ef7868f6f7e8619d2e6306952065482656"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@@ -160,6 +160,35 @@
} }
}, },
"develop": { "develop": {
"appdirs": {
"hashes": [
"sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
"sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
],
"version": "==1.4.3"
},
"attrs": {
"hashes": [
"sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
"sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
],
"version": "==19.3.0"
},
"black": {
"hashes": [
"sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b",
"sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"
],
"index": "pypi",
"version": "==19.10b0"
},
"click": {
"hashes": [
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
],
"version": "==7.0"
},
"mypy": { "mypy": {
"hashes": [ "hashes": [
"sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a", "sha256:0a9a45157e532da06fe56adcfef8a74629566b607fa2c1ac0122d1ff995c748a",
@@ -187,6 +216,46 @@
], ],
"version": "==0.4.3" "version": "==0.4.3"
}, },
"pathspec": {
"hashes": [
"sha256:163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424",
"sha256:562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"
],
"version": "==0.7.0"
},
"regex": {
"hashes": [
"sha256:07b39bf943d3d2fe63d46281d8504f8df0ff3fe4c57e13d1656737950e53e525",
"sha256:0932941cdfb3afcbc26cc3bcf7c3f3d73d5a9b9c56955d432dbf8bbc147d4c5b",
"sha256:0e182d2f097ea8549a249040922fa2b92ae28be4be4895933e369a525ba36576",
"sha256:10671601ee06cf4dc1bc0b4805309040bb34c9af423c12c379c83d7895622bb5",
"sha256:23e2c2c0ff50f44877f64780b815b8fd2e003cda9ce817a7fd00dea5600c84a0",
"sha256:26ff99c980f53b3191d8931b199b29d6787c059f2e029b2b0c694343b1708c35",
"sha256:27429b8d74ba683484a06b260b7bb00f312e7c757792628ea251afdbf1434003",
"sha256:3e77409b678b21a056415da3a56abfd7c3ad03da71f3051bbcdb68cf44d3c34d",
"sha256:4e8f02d3d72ca94efc8396f8036c0d3bcc812aefc28ec70f35bb888c74a25161",
"sha256:4eae742636aec40cf7ab98171ab9400393360b97e8f9da67b1867a9ee0889b26",
"sha256:6a6ae17bf8f2d82d1e8858a47757ce389b880083c4ff2498dba17c56e6c103b9",
"sha256:6a6ba91b94427cd49cd27764679024b14a96874e0dc638ae6bdd4b1a3ce97be1",
"sha256:7bcd322935377abcc79bfe5b63c44abd0b29387f267791d566bbb566edfdd146",
"sha256:98b8ed7bb2155e2cbb8b76f627b2fd12cf4b22ab6e14873e8641f266e0fb6d8f",
"sha256:bd25bb7980917e4e70ccccd7e3b5740614f1c408a642c245019cff9d7d1b6149",
"sha256:d0f424328f9822b0323b3b6f2e4b9c90960b24743d220763c7f07071e0778351",
"sha256:d58e4606da2a41659c84baeb3cfa2e4c87a74cec89a1e7c56bee4b956f9d7461",
"sha256:e3cd21cc2840ca67de0bbe4071f79f031c81418deb544ceda93ad75ca1ee9f7b",
"sha256:e6c02171d62ed6972ca8631f6f34fa3281d51db8b326ee397b9c83093a6b7242",
"sha256:e7c7661f7276507bce416eaae22040fd91ca471b5b33c13f8ff21137ed6f248c",
"sha256:ecc6de77df3ef68fee966bb8cb4e067e84d4d1f397d0ef6fce46913663540d77"
],
"version": "==2020.1.8"
},
"toml": {
"hashes": [
"sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
"sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
],
"version": "==0.10.0"
},
"typed-ast": { "typed-ast": {
"hashes": [ "hashes": [
"sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",

0
pictures/20190809172206_1.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 642 KiB

After

Width:  |  Height:  |  Size: 642 KiB

0
pictures/20190809172213_1.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 636 KiB

After

Width:  |  Height:  |  Size: 636 KiB

0
pictures/20190809172219_1.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 633 KiB

After

Width:  |  Height:  |  Size: 633 KiB

0
pictures/20190809172225_1.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 633 KiB

After

Width:  |  Height:  |  Size: 633 KiB

0
pictures/20190809172232_1.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 636 KiB

After

Width:  |  Height:  |  Size: 636 KiB

0
pictures/20190809172238_1.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 639 KiB

After

Width:  |  Height:  |  Size: 639 KiB

0
pictures/specific/BaiBlack.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 650 KiB

0
pictures/specific/BaiShiny.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 664 KiB

After

Width:  |  Height:  |  Size: 664 KiB

0
pictures/specific/BunkerCards.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 646 KiB

After

Width:  |  Height:  |  Size: 646 KiB

0
pictures/specific/FaShiny.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 637 KiB

After

Width:  |  Height:  |  Size: 637 KiB

0
pictures/specific/ZhongShiny.jpg Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 649 KiB

After

Width:  |  Height:  |  Size: 649 KiB

View File

@@ -36,6 +36,9 @@ class NumberCard:
"""Returns unique identifier representing this card""" """Returns unique identifier representing this card"""
return int(self.number - 1 + 9 ** int(self.suit.value)) return int(self.number - 1 + 9 ** int(self.suit.value))
def __repr__(self) -> str:
return f"NumberCard({self.suit.name} {self.number})"
Card = Union[NumberCard, SpecialCard] Card = Union[NumberCard, SpecialCard]
@@ -58,16 +61,38 @@ class Board:
def __init__(self) -> None: def __init__(self) -> None:
self.field: List[List[Card]] = [[]] * Board.MAX_COLUMN_SIZE self.field: List[List[Card]] = [[]] * Board.MAX_COLUMN_SIZE
self.bunker: List[Union[Tuple[SpecialCard, int], Optional[Card]]] = [None] * 3 self.bunker: List[Union[Tuple[SpecialCard, int], Optional[Card]]] = [None] * 3
self.goal: Dict[NumberCard.Suit, int] = { self.goal: List[Optional[NumberCard]] = [None] * 3
NumberCard.Suit.Red: 0,
NumberCard.Suit.Green: 0,
NumberCard.Suit.Black: 0,
}
self.flower_gone: bool = False self.flower_gone: bool = False
def getGoal(self, suit: NumberCard.Suit) -> int:
for card in self.goal:
if card is not None and card.suit == suit:
return card.number
else:
return 0
def getGoalId(self, suit: NumberCard.Suit) -> int:
for index, card in enumerate(self.goal):
if card is not None and card.suit == suit:
return index
else:
return self.goal.index(None)
def setGoal(self, suit: NumberCard.Suit, value: int) -> None:
assert len(self.goal) == 3
assert 0 <= value
assert value <= 9
if value == 0:
self.goal[self.getGoalId(suit)] = None
else:
self.goal[self.getGoalId(suit)] = NumberCard(suit, number=value)
def incGoal(self, suit: NumberCard.Suit) -> None:
self.setGoal(suit, self.getGoal(suit) + 1)
def solved(self) -> bool: def solved(self) -> bool:
"""Returns true if the board is solved""" """Returns true if the board is solved"""
if any(x != 9 for x in self.goal.values()): if any(x.number != 9 for x in self.goal if x is not None):
return False return False
if any(not isinstance(x, tuple) for x in self.bunker): if any(not isinstance(x, tuple) for x in self.bunker):
return False return False
@@ -97,9 +122,14 @@ class Board:
if self.flower_gone: if self.flower_gone:
result |= 1 result |= 1
for _, goal_count in self.goal.items(): assert len(self.goal) == 3
result <<= 4 suit_sequence = list(NumberCard.Suit)
result |= goal_count for card in self.goal:
result <<= 5
if card is None:
result |= len(suit_sequence) * 10
else:
result |= suit_sequence.index(card.suit) * 10 + card.number
# Max stack size is 13 # Max stack size is 13
# (4 random cards from the start, plus a stack from 9 to 1) # (4 random cards from the start, plus a stack from 9 to 1)
@@ -146,7 +176,7 @@ class Board:
number_cards[card.suit].add(card.number) number_cards[card.suit].add(card.number)
for suit, numbers in number_cards.items(): for suit, numbers in number_cards.items():
if set(range(self.goal[suit] + 1, 10)) != numbers: if set(range(self.getGoal(suit) + 1, 10)) != numbers:
return False return False
for cardtype, count in special_cards.items(): for cardtype, count in special_cards.items():

View File

@@ -211,7 +211,7 @@ def parse_goal_field(
return best_card_name return best_card_name
def parse_goal(image: np.ndarray, conf: Configuration) -> Dict[NumberCard.Suit, int]: def parse_goal(image: np.ndarray, conf: Configuration) -> List[Optional[NumberCard]]:
goal_squares = card_finder.get_field_squares( goal_squares = card_finder.get_field_squares(
image, fake_adjustment(conf.goal_adjustment), count_x=1, count_y=3 image, fake_adjustment(conf.goal_adjustment), count_x=1, count_y=3
) )
@@ -219,11 +219,8 @@ def parse_goal(image: np.ndarray, conf: Configuration) -> Dict[NumberCard.Suit,
parse_goal_field(square, conf.catalogue, conf.green_card) parse_goal_field(square, conf.catalogue, conf.green_card)
for square in goal_squares for square in goal_squares
] ]
base_goal_dict = {suit: 0 for suit in NumberCard.Suit}
base_goal_dict.update( return goal_list
{x.suit: x.number for x in (x for x in goal_list if x is not None)}
)
return base_goal_dict
def parse_board(image: np.ndarray, conf: Configuration) -> Board: def parse_board(image: np.ndarray, conf: Configuration) -> Board:

View File

View File

@@ -0,0 +1,104 @@
import shenzhen_solitaire.solver.board_actions as board_actions
import shenzhen_solitaire.card_detection.configuration as configuration
import shenzhen_solitaire.card_detection.adjustment as adjustment
import shenzhen_solitaire.board as board
from typing import List, Tuple
import pyautogui
import time
def drag(
src: Tuple[int, int], dst: Tuple[int, int], offset: Tuple[int, int] = (0, 0)
) -> None:
pyautogui.moveTo(x=src[0] + offset[0], y=src[1] + offset[1])
pyautogui.dragTo(x=dst[0] + offset[0], y=dst[1] + offset[1],
duration=0.4, tween=lambda x: 0 if x < 0.5 else 1)
def click(point: Tuple[int, int], offset: Tuple[int, int] = (0, 0)) -> None:
pyautogui.moveTo(x=point[0] + offset[0], y=point[1] + offset[1])
pyautogui.mouseDown()
time.sleep(0.2)
pyautogui.mouseUp()
def handle_action(
action: board_actions.Action,
offset: Tuple[int, int],
conf: configuration.Configuration,
) -> None:
if isinstance(action, board_actions.MoveAction):
src_x, src_y, _, _ = adjustment.get_square(
conf.field_adjustment,
index_x=action.source_id,
index_y=action.source_row_index,
)
dst_x, dst_y, _, _ = adjustment.get_square(
conf.field_adjustment,
index_x=action.destination_id,
index_y=action.destination_row_index,
)
drag((src_x, src_y), (dst_x, dst_y), offset)
return
if isinstance(action, board_actions.HuaKillAction):
time.sleep(1)
return
if isinstance(action, board_actions.BunkerizeAction):
field_x, field_y, _, _ = adjustment.get_square(
conf.field_adjustment,
index_x=action.field_id,
index_y=action.field_row_index,
)
bunker_x, bunker_y, _, _ = adjustment.get_square(
conf.bunker_adjustment, index_x=action.bunker_id, index_y=0,
)
if action.to_bunker:
drag((field_x, field_y), (bunker_x, bunker_y), offset)
else:
drag((bunker_x, bunker_y), (field_x, field_y), offset)
return
if isinstance(action, board_actions.DragonKillAction):
dragon_sequence = [
board.SpecialCard.Zhong,
board.SpecialCard.Fa,
board.SpecialCard.Bai,
]
field_x, field_y, size_x, size_y = adjustment.get_square(
conf.special_button_adjustment,
index_x=0,
index_y=dragon_sequence.index(action.dragon),
)
click((field_x + (size_x - field_x) // 2, field_y + (size_y - field_y) // 2), offset)
time.sleep(0.5)
return
if isinstance(action, board_actions.GoalAction):
if action.obvious:
time.sleep(1)
return
dst_x, dst_y, _, _ = adjustment.get_square(
conf.goal_adjustment, index_x=action.goal_id, index_y=0,
)
if action.source_position == board.Position.Field:
assert action.source_row_index is not None
src_x, src_y, _, _ = adjustment.get_square(
conf.field_adjustment,
index_x=action.source_id,
index_y=action.source_row_index,
)
else:
assert action.source_position == board.Position.Bunker
src_x, src_y, _, _ = adjustment.get_square(
conf.bunker_adjustment, index_x=action.source_id, index_y=0,
)
drag((src_x, src_y), (dst_x, dst_y), offset)
return
raise AssertionError("You forgot an Action type")
def handle_actions(
actions: List[board_actions.Action],
offset: Tuple[int, int],
conf: configuration.Configuration,
) -> None:
for action in actions:
handle_action(action, offset, conf)

View File

View File

@@ -1,11 +1,12 @@
"""Contains actions that can be used on the board""" """Contains actions that can be used on the board"""
from typing import List, Tuple from typing import List, Tuple, Optional
from dataclasses import dataclass from dataclasses import dataclass
from .. import board from .. import board
class Action: class Action:
"""Base class for a card move action on a solitaire board""" """Base class for a card move action on a solitaire board"""
_before_state: int = 0 _before_state: int = 0
_after_state: int = 0 _after_state: int = 0
@@ -36,26 +37,30 @@ class GoalAction(Action):
card: board.NumberCard card: board.NumberCard
source_id: int source_id: int
source_row_index: Optional[int]
source_position: board.Position source_position: board.Position
goal_id: int
obvious: bool
def _apply(self, action_board: board.Board) -> None: def _apply(self, action_board: board.Board) -> None:
"""Do action""" """Do action"""
assert action_board.getGoalId(self.card.suit) == self.goal_id
assert action_board.getGoal(self.card.suit) + 1 == self.card.number
if self.source_position == board.Position.Field: if self.source_position == board.Position.Field:
assert action_board.field[self.source_id][-1] == self.card assert action_board.field[self.source_id][-1] == self.card
assert action_board.goal[self.card.suit] + 1 == self.card.number
action_board.field[self.source_id].pop() action_board.field[self.source_id].pop()
action_board.goal[self.card.suit] += 1 action_board.incGoal(self.card.suit)
elif self.source_position == board.Position.Bunker: elif self.source_position == board.Position.Bunker:
assert action_board.bunker[self.source_id] == self.card assert action_board.bunker[self.source_id] == self.card
assert action_board.goal[self.card.suit] + 1 == self.card.number
action_board.bunker[self.source_id] = None action_board.bunker[self.source_id] = None
action_board.goal[self.card.suit] += 1 action_board.incGoal(self.card.suit)
else: else:
raise RuntimeError("Unknown position") raise RuntimeError("Unknown position")
def _undo(self, action_board: board.Board) -> None: def _undo(self, action_board: board.Board) -> None:
"""Undo action""" """Undo action"""
assert action_board.goal[self.card.suit] == self.card.number assert action_board.getGoalId(self.card.suit) == self.goal_id
assert action_board.getGoal(self.card.suit) == self.card.number
if self.source_position == board.Position.Field: if self.source_position == board.Position.Field:
action_board.field[self.source_id].append(self.card) action_board.field[self.source_id].append(self.card)
elif self.source_position == board.Position.Bunker: elif self.source_position == board.Position.Bunker:
@@ -63,7 +68,7 @@ class GoalAction(Action):
action_board.bunker[self.source_id] = self.card action_board.bunker[self.source_id] = self.card
else: else:
raise RuntimeError("Unknown position") raise RuntimeError("Unknown position")
action_board.goal[self.card.suit] -= 1 action_board.setGoal(self.card.suit, action_board.getGoal(self.card.suit) - 1)
@dataclass @dataclass
@@ -73,6 +78,7 @@ class BunkerizeAction(Action):
card: board.Card card: board.Card
bunker_id: int bunker_id: int
field_id: int field_id: int
field_row_index: int
to_bunker: bool to_bunker: bool
def _move_from_bunker(self, action_board: board.Board) -> None: def _move_from_bunker(self, action_board: board.Board) -> None:
@@ -107,21 +113,17 @@ class MoveAction(Action):
cards: List[board.Card] cards: List[board.Card]
source_id: int source_id: int
source_row_index: int
destination_id: int destination_id: int
destination_row_index: int
def _shift( def _shift(self, action_board: board.Board, source: int, dest: int) -> None:
self,
action_board: board.Board,
source: int,
dest: int) -> None:
"""Shift a card from the field id 'source' to field id 'dest'""" """Shift a card from the field id 'source' to field id 'dest'"""
for stack_offset, card in enumerate( for stack_offset, card in enumerate(self.cards, start=-len(self.cards)):
self.cards, start=-len(self.cards)):
assert action_board.field[source][stack_offset] == card assert action_board.field[source][stack_offset] == card
action_board.field[source] = action_board.field[ action_board.field[source] = action_board.field[source][: -len(self.cards)]
source][:-len(self.cards)]
action_board.field[dest].extend(self.cards) action_board.field[dest].extend(self.cards)
def _apply(self, action_board: board.Board) -> None: def _apply(self, action_board: board.Board) -> None:
@@ -174,8 +176,7 @@ class DragonKillAction(Action):
def _undo(self, action_board: board.Board) -> None: def _undo(self, action_board: board.Board) -> None:
"""Undo action""" """Undo action"""
assert action_board.bunker[self.destination_bunker_id] == ( assert action_board.bunker[self.destination_bunker_id] == (self.dragon, 4)
self.dragon, 4)
assert len(self.source_stacks) == 4 assert len(self.source_stacks) == 4
action_board.bunker[self.destination_bunker_id] = None action_board.bunker[self.destination_bunker_id] = None
for position, index in self.source_stacks: for position, index in self.source_stacks:
@@ -192,12 +193,12 @@ class HuaKillAction(Action):
"""Remove the flower card""" """Remove the flower card"""
source_field_id: int source_field_id: int
source_field_row_index: int
def _apply(self, action_board: board.Board) -> None: def _apply(self, action_board: board.Board) -> None:
"""Do action""" """Do action"""
assert not action_board.flower_gone assert not action_board.flower_gone
assert (action_board.field[self.source_field_id][-1] assert action_board.field[self.source_field_id][-1] == board.SpecialCard.Hua
== board.SpecialCard.Hua)
action_board.field[self.source_field_id].pop() action_board.field[self.source_field_id].pop()
action_board.flower_gone = True action_board.flower_gone = True

View File

@@ -2,19 +2,22 @@
from typing import Iterator, List, Tuple from typing import Iterator, List, Tuple
from .. import board from .. import board
from . import board_actions from . import board_actions
import pdb
def possible_huakill_action( def possible_huakill_action(
search_board: board.Board search_board: board.Board,
) -> Iterator[board_actions.HuaKillAction]: ) -> Iterator[board_actions.HuaKillAction]:
"""Check if the flowercard can be eliminated""" """Check if the flowercard can be eliminated"""
for index, stack in enumerate(search_board.field): for index, stack in enumerate(search_board.field):
if stack and stack[-1] == board.SpecialCard.Hua: if stack and stack[-1] == board.SpecialCard.Hua:
yield board_actions.HuaKillAction(source_field_id=index) yield board_actions.HuaKillAction(
source_field_id=index, source_field_row_index=len(stack) - 1
)
def possible_dragonkill_actions( def possible_dragonkill_actions(
search_board: board.Board search_board: board.Board,
) -> Iterator[board_actions.DragonKillAction]: ) -> Iterator[board_actions.DragonKillAction]:
"""Enumerate all possible dragon kills""" """Enumerate all possible dragon kills"""
possible_dragons = [ possible_dragons = [
@@ -30,8 +33,7 @@ def possible_dragonkill_actions(
possible_dragons = new_possible_dragons possible_dragons = new_possible_dragons
for dragon in possible_dragons: for dragon in possible_dragons:
bunker_dragons = [i for i, d in bunker_dragons = [i for i, d in enumerate(search_board.bunker) if d == dragon]
enumerate(search_board.bunker) if d == dragon]
field_dragons = [ field_dragons = [
i for i, f in enumerate(search_board.field) if f if f[-1] == dragon i for i, f in enumerate(search_board.field) if f if f[-1] == dragon
] ]
@@ -46,8 +48,7 @@ def possible_dragonkill_actions(
][0] ][0]
source_stacks = [(board.Position.Bunker, i) for i in bunker_dragons] source_stacks = [(board.Position.Bunker, i) for i in bunker_dragons]
source_stacks.extend([(board.Position.Field, i) source_stacks.extend([(board.Position.Field, i) for i in field_dragons])
for i in field_dragons])
yield board_actions.DragonKillAction( yield board_actions.DragonKillAction(
dragon=dragon, dragon=dragon,
@@ -57,12 +58,10 @@ def possible_dragonkill_actions(
def possible_bunkerize_actions( def possible_bunkerize_actions(
search_board: board.Board search_board: board.Board,
) -> Iterator[board_actions.BunkerizeAction]: ) -> Iterator[board_actions.BunkerizeAction]:
"""Enumerates all possible card moves from the field to the bunker""" """Enumerates all possible card moves from the field to the bunker"""
open_bunker_list = [ open_bunker_list = [i for i, x in enumerate(search_board.bunker) if x is None]
i for i, x in enumerate(
search_board.bunker) if x is None]
if not open_bunker_list: if not open_bunker_list:
return return
@@ -74,13 +73,14 @@ def possible_bunkerize_actions(
yield board_actions.BunkerizeAction( yield board_actions.BunkerizeAction(
card=stack[-1], card=stack[-1],
field_id=index, field_id=index,
field_row_index=len(stack) - 1,
bunker_id=open_bunker, bunker_id=open_bunker,
to_bunker=True to_bunker=True,
) )
def possible_debunkerize_actions( def possible_debunkerize_actions(
search_board: board.Board search_board: board.Board,
) -> Iterator[board_actions.BunkerizeAction]: ) -> Iterator[board_actions.BunkerizeAction]:
"""Enumerates all possible card moves from the bunker to the field""" """Enumerates all possible card moves from the bunker to the field"""
bunker_number_cards = [ bunker_number_cards = [
@@ -102,12 +102,13 @@ def possible_debunkerize_actions(
card=card, card=card,
bunker_id=index, bunker_id=index,
field_id=other_index, field_id=other_index,
to_bunker=False field_row_index=len(other_stack),
to_bunker=False,
) )
def possible_goal_move_actions( def possible_goal_move_actions(
search_board: board.Board search_board: board.Board,
) -> Iterator[board_actions.GoalAction]: ) -> Iterator[board_actions.GoalAction]:
"""Enumerates all possible moves from anywhere to the goal""" """Enumerates all possible moves from anywhere to the goal"""
field_cards = [ field_cards = [
@@ -117,18 +118,28 @@ def possible_goal_move_actions(
if isinstance(stack[-1], board.NumberCard) if isinstance(stack[-1], board.NumberCard)
] ]
bunker_cards = [ bunker_cards = [
(board.Position.Bunker, index, stack) (board.Position.Bunker, index, card)
for index, stack in enumerate(search_board.bunker) for index, card in enumerate(search_board.bunker)
if isinstance(stack, board.NumberCard) if isinstance(card, board.NumberCard)
] ]
top_cards = field_cards + bunker_cards top_cards = field_cards + bunker_cards
for suit, number in search_board.goal.items(): for source, index, card in top_cards:
for source, index, stack in top_cards: if not (card.number == search_board.getGoal(card.suit) + 1):
if not (stack.suit == suit and stack.number == number + 1):
continue continue
obvious = all(
search_board.getGoal(other_suit) >= search_board.getGoal(card.suit)
for other_suit in set(board.NumberCard.Suit) - {card.suit}
)
yield board_actions.GoalAction( yield board_actions.GoalAction(
card=stack, source_id=index, source_position=source card=card,
source_id=index,
source_row_index=len(search_board.field[index]) - 1
if source == board.Position.Field
else None,
source_position=source,
goal_id=search_board.getGoalId(card.suit),
obvious=obvious,
) )
break break
@@ -163,42 +174,44 @@ def _get_cardstacks(search_board: board.Board) -> List[List[board.Card]]:
def possible_field_move_actions( def possible_field_move_actions(
search_board: board.Board search_board: board.Board,
) -> Iterator[board_actions.MoveAction]: ) -> Iterator[board_actions.MoveAction]:
"""Enumerate all possible move actions """Enumerate all possible move actions
from one field stack to another field stack""" from one field stack to another field stack"""
first_empty_field_id = -1 first_empty_field_id = -1
cardstacks = [(index, stack) cardstacks = [x for x in enumerate(_get_cardstacks(search_board)) if x[1]]
for index, stack in enumerate(_get_cardstacks(search_board))]
cardstacks = [x for x in cardstacks if x[1]]
cardstacks = sorted(cardstacks, key=lambda x: len(x[1])) cardstacks = sorted(cardstacks, key=lambda x: len(x[1]))
substacks: List[Tuple[int, List[board.Card]]] = [] substacks: List[Tuple[int, List[board.Card]]] = []
for index, stack in cardstacks: for index, stack in cardstacks:
substacks.extend((index, substack) substacks.extend(
for substack in (stack[i:] (index, substack) for substack in (stack[i:] for i in range(len(stack)))
for i in range(len(stack)))) )
for index, substack in substacks: for source_index, source_substack in substacks:
for other_index, other_stack in enumerate(search_board.field): for destination_index, destination_stack in enumerate(search_board.field):
if index == other_index: if source_index == destination_index:
continue continue
if other_stack: if destination_stack:
if not _can_stack(other_stack[-1], substack[0]): if not _can_stack(destination_stack[-1], source_substack[0]):
continue continue
elif len(substack) == len(search_board.field[index]): elif len(source_substack) == len(search_board.field[source_index]):
continue continue
elif first_empty_field_id == -1: elif first_empty_field_id == -1:
first_empty_field_id = other_index first_empty_field_id = destination_index
elif other_index != first_empty_field_id: elif destination_index != first_empty_field_id:
continue continue
yield board_actions.MoveAction( yield board_actions.MoveAction(
cards=substack, source_id=index, destination_id=other_index cards=source_substack,
source_id=source_index,
source_row_index=len(search_board.field[source_index])
- len(source_substack),
destination_id=destination_index,
destination_row_index=len(destination_stack),
) )
def possible_actions( def possible_actions(search_board: board.Board) -> Iterator[board_actions.Action]:
search_board: board.Board) -> Iterator[board_actions.Action]:
"""Enumerate all possible actions on the current search_board""" """Enumerate all possible actions on the current search_board"""
yield from possible_huakill_action(search_board) yield from possible_huakill_action(search_board)
yield from possible_dragonkill_actions(search_board) yield from possible_dragonkill_actions(search_board)

View File

@@ -84,7 +84,7 @@ def solve(board: Board) -> Iterator[List[board_actions.Action]]:
count += 1 count += 1
if count > 5000: if count > 5000:
count = 0 count = 0
print(f"{len(stack)} {sum(board.goal.values())}") print(f"{len(stack)} {board.goal}")
# _limit_stack_size(80) # _limit_stack_size(80)

View File

@@ -2,7 +2,12 @@
import unittest import unittest
from shenzhen_solitaire.board import NumberCard, Position from shenzhen_solitaire.board import NumberCard, Position
from shenzhen_solitaire.solver.board_actions import MoveAction, BunkerizeAction, GoalAction, HuaKillAction from shenzhen_solitaire.solver.board_actions import (
MoveAction,
BunkerizeAction,
GoalAction,
HuaKillAction,
)
from shenzhen_solitaire.solver import board_possibilities from shenzhen_solitaire.solver import board_possibilities
from .boards import TEST_BOARD from .boards import TEST_BOARD
@@ -17,53 +22,63 @@ class ChainTest(unittest.TestCase):
sequence = [ sequence = [
MoveAction( MoveAction(
cards=[ cards=[
NumberCard( NumberCard(suit=NumberCard.Suit.Red, number=7),
suit=NumberCard.Suit.Red, NumberCard(suit=NumberCard.Suit.Green, number=6),
number=7), ],
NumberCard(
suit=NumberCard.Suit.Green,
number=6)],
source_id=3, source_id=3,
destination_id=7), source_row_index=3,
destination_id=7,
destination_row_index=5,
),
BunkerizeAction( BunkerizeAction(
card=NumberCard( card=NumberCard(suit=NumberCard.Suit.Red, number=6),
suit=NumberCard.Suit.Red,
number=6),
bunker_id=0, bunker_id=0,
field_id=2, field_id=2,
to_bunker=True), field_row_index=4,
to_bunker=True,
),
GoalAction( GoalAction(
card=NumberCard( card=NumberCard(suit=NumberCard.Suit.Green, number=1),
suit=NumberCard.Suit.Green,
number=1),
source_id=2, source_id=2,
source_position=Position.Field), source_row_index=3,
source_position=Position.Field,
obvious=True,
goal_id=0,
),
MoveAction(
cards=[NumberCard(suit=NumberCard.Suit.Red, number=4)],
source_id=2,
source_row_index=2,
destination_id=5,
destination_row_index=5,
),
GoalAction(
card=NumberCard(suit=NumberCard.Suit.Red, number=1),
source_id=2,
source_row_index=1,
source_position=Position.Field,
obvious=True,
goal_id=1,
),
HuaKillAction(source_field_id=2, source_field_row_index=0),
MoveAction( MoveAction(
cards=[ cards=[
NumberCard( NumberCard(suit=NumberCard.Suit.Black, number=9),
suit=NumberCard.Suit.Red, NumberCard(suit=NumberCard.Suit.Red, number=8),
number=4)], ],
source_id=2,
destination_id=5),
GoalAction(
card=NumberCard(
suit=NumberCard.Suit.Red,
number=1),
source_id=2,
source_position=Position.Field),
HuaKillAction(source_field_id=2),
MoveAction(
cards=[
NumberCard(
suit=NumberCard.Suit.Black, number=9),
NumberCard(suit=NumberCard.Suit.Red, number=8)],
source_id=6, source_id=6,
destination_id=2), source_row_index=3,
destination_id=2,
destination_row_index=0,
),
GoalAction( GoalAction(
card=NumberCard( card=NumberCard(suit=NumberCard.Suit.Green, number=2),
suit=NumberCard.Suit.Green, number=2),
source_id=6, source_id=6,
source_position=Position.Field) source_row_index=2,
source_position=Position.Field,
obvious=False,
goal_id=0,
),
] ]
for action in sequence: for action in sequence:
step = list(board_possibilities.possible_actions(TEST_BOARD)) step = list(board_possibilities.possible_actions(TEST_BOARD))

View File

@@ -2,7 +2,7 @@
import copy import copy
import unittest import unittest
from typing import List, Tuple, Union from typing import List, Tuple, Union, Optional
import cv2 import cv2
import numpy as np import numpy as np
@@ -79,8 +79,8 @@ class CardDetectionTest(unittest.TestCase):
def test_goal_parsing(self) -> None: def test_goal_parsing(self) -> None:
loaded_config = configuration.load("test_config.zip") loaded_config = configuration.load("test_config.zip")
imagenames: List[Tuple[str, List[NumberCard]]] = [ imagenames: List[Tuple[str, List[Optional[NumberCard]]]] = [
("BaiBlack", [NumberCard(NumberCard.Suit.Green, 2)],), ("BaiBlack", [NumberCard(NumberCard.Suit.Green, 2), None, None],),
( (
"BaiShiny", "BaiShiny",
[ [
@@ -94,15 +94,13 @@ class CardDetectionTest(unittest.TestCase):
[ [
NumberCard(NumberCard.Suit.Red, 1), NumberCard(NumberCard.Suit.Red, 1),
NumberCard(NumberCard.Suit.Black, 1), NumberCard(NumberCard.Suit.Black, 1),
None,
], ],
), ),
("FaShiny", [NumberCard(NumberCard.Suit.Green, 2)]), ("FaShiny", [NumberCard(NumberCard.Suit.Green, 2), None, None]),
("ZhongShiny", [NumberCard(NumberCard.Suit.Green, 2)]), ("ZhongShiny", [NumberCard(NumberCard.Suit.Green, 2), None, None]),
] ]
base_goal_dict = {suit: 0 for suit in NumberCard.Suit}
for imagename, goal in imagenames: for imagename, goal in imagenames:
image = cv2.imread(f"pictures/specific/{imagename}.jpg") image = cv2.imread(f"pictures/specific/{imagename}.jpg")
my_goal_dict = copy.deepcopy(base_goal_dict)
my_goal_dict.update({x.suit: x.number for x in goal})
my_board = board_parser.parse_board(image, loaded_config) my_board = board_parser.parse_board(image, loaded_config)
self.assertDictEqual(my_goal_dict, my_board.goal) self.assertListEqual(goal, my_board.goal)

52
tools/assistant.py Normal file
View File

@@ -0,0 +1,52 @@
import pyautogui
import cv2
import numpy as np
from pathlib import Path
from shenzhen_solitaire.card_detection.board_parser import parse_board
import shenzhen_solitaire.card_detection.configuration as configuration
import shenzhen_solitaire.solver.solver as solver
import tempfile
import shenzhen_solitaire.clicker.main as clicker
import time
OFFSET = (0, 0)
SIZE = (2560, 1440)
NEW_BUTTON=(1900,1100)
def debug_screenshot(image):
cv2.namedWindow("Name", cv2.WINDOW_KEEPRATIO)
cv2.imshow("Name", image)
cv2.waitKey(0)
input()
cv2.destroyAllWindows()
def solve() -> None:
screenshot_dir = Path(tempfile.mkdtemp())
screenshot_file = screenshot_dir / 'screenshot.png'
screenshot = pyautogui.screenshot(region=(*OFFSET, *SIZE))
screenshot.save(screenshot_file)
image = cv2.imread(str(screenshot_file))
# debug_screenshot()
print("Solving")
conf = configuration.load("test_config.zip")
board = parse_board(image, conf)
print(board)
solution = list(next(solver.solve(board)))
print(*solution, sep="\n")
time.sleep(1)
for step in solution:
print(step)
#time.sleep(0.5)
clicker.handle_action(step, OFFSET, conf)
clicker.click(NEW_BUTTON, OFFSET)
time.sleep(10)
def main() -> None:
time.sleep(3)
while True:
solve()
if __name__ == "__main__":
main()

View File