Spaces:
Configuration error
Configuration error
EZTIME2025 commited on
Commit ·
d9c12b7
1
Parent(s): 511ed36
trade - add multiple trade option
Browse files- game_viz.log +31 -0
- pycatan/game_moves_3Players.txt +2 -0
- pycatan/human_user.py +164 -12
- tests/test_human_user.py +149 -1
game_viz.log
CHANGED
|
@@ -895,3 +895,34 @@ Current Player: [1m[92m► c[0m
|
|
| 895 |
[93m-----[0m
|
| 896 |
Board Tiles: 19 tiles configured
|
| 897 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 895 |
[93m-----[0m
|
| 896 |
Board Tiles: 19 tiles configured
|
| 897 |
|
| 898 |
+
[92m✓[0m c proposed a trade
|
| 899 |
+
|
| 900 |
+
[1m[96m==================================================[0m
|
| 901 |
+
[1m[96m GAME STATE [0m
|
| 902 |
+
[1m[96m==================================================[0m
|
| 903 |
+
|
| 904 |
+
Turn: [1m11[0m
|
| 905 |
+
Current Player: [1m[92m► c[0m
|
| 906 |
+
|
| 907 |
+
[1m[93mPLAYERS[0m
|
| 908 |
+
[93m-------[0m
|
| 909 |
+
|
| 910 |
+
[97ma[0m
|
| 911 |
+
Victory Points: [97m2[0m
|
| 912 |
+
Resources: None
|
| 913 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 914 |
+
|
| 915 |
+
[97mb[0m
|
| 916 |
+
Victory Points: [97m2[0m
|
| 917 |
+
Resources: None
|
| 918 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m4[0m
|
| 919 |
+
|
| 920 |
+
[1m[92m► c[0m
|
| 921 |
+
Victory Points: [97m2[0m
|
| 922 |
+
Resources: None
|
| 923 |
+
Buildings: Settlements: [92m2[0m, Cities: [91m0[0m, Roads: [92m2[0m
|
| 924 |
+
|
| 925 |
+
[1m[93mBOARD[0m
|
| 926 |
+
[93m-----[0m
|
| 927 |
+
Board Tiles: 19 tiles configured
|
| 928 |
+
|
pycatan/game_moves_3Players.txt
CHANGED
|
@@ -29,3 +29,5 @@ buy dev card
|
|
| 29 |
use road rd 14 24 rd 24 25
|
| 30 |
end
|
| 31 |
roll
|
|
|
|
|
|
|
|
|
| 29 |
use road rd 14 24 rd 24 25
|
| 30 |
end
|
| 31 |
roll
|
| 32 |
+
t player a nothing for ore 2
|
| 33 |
+
y
|
pycatan/human_user.py
CHANGED
|
@@ -403,12 +403,25 @@ class HumanUser(User):
|
|
| 403 |
raise UserInputError("Invalid trade format or resource names")
|
| 404 |
|
| 405 |
def _parse_player_trade(self, parts: List[str], game_state: GameState = None) -> Action:
|
| 406 |
-
"""
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
try:
|
| 411 |
-
#
|
| 412 |
try:
|
| 413 |
target_player = int(parts[0])
|
| 414 |
except ValueError:
|
|
@@ -425,19 +438,151 @@ class HumanUser(User):
|
|
| 425 |
if target_player is None:
|
| 426 |
raise UserInputError(f"Player '{parts[0]}' not found. Use player name or ID (0-{len(game_state.players_state)-1 if game_state else 3})")
|
| 427 |
|
| 428 |
-
|
| 429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
|
| 431 |
params = {
|
| 432 |
'target_player': target_player,
|
| 433 |
-
'offer':
|
| 434 |
-
'request':
|
| 435 |
}
|
| 436 |
|
| 437 |
return Action(ActionType.TRADE_PROPOSE, self.user_id, params)
|
| 438 |
|
| 439 |
-
except (ValueError, KeyError):
|
| 440 |
-
raise UserInputError("Invalid trade format
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
|
| 442 |
def _parse_use_dev_card(self, parts: List[str], game_state: GameState) -> Action:
|
| 443 |
"""Parse development card usage command."""
|
|
@@ -693,8 +838,15 @@ class HumanUser(User):
|
|
| 693 |
print()
|
| 694 |
print("💰 TRADING:")
|
| 695 |
print(" trade bank <give> <amount> <get> <amount>")
|
| 696 |
-
print("
|
| 697 |
-
print(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 698 |
print()
|
| 699 |
print("🃏 DEVELOPMENT CARDS:")
|
| 700 |
print(" buy / dev - Buy dev card (cost: 1 Ore + 1 Sheep + 1 Wheat)")
|
|
|
|
| 403 |
raise UserInputError("Invalid trade format or resource names")
|
| 404 |
|
| 405 |
def _parse_player_trade(self, parts: List[str], game_state: GameState = None) -> Action:
|
| 406 |
+
"""
|
| 407 |
+
Parse player trading command.
|
| 408 |
+
|
| 409 |
+
Supports flexible trading formats:
|
| 410 |
+
- Simple 1:1: 'trade player 1 wood sheep'
|
| 411 |
+
- With amounts: 'trade player 1 wood 2 sheep 1'
|
| 412 |
+
- Multiple resources: 'trade player 1 wood 2 brick 1 for wheat 2 ore 1'
|
| 413 |
+
- Gifts: 'trade player 1 nothing wheat 1' or 'trade player 1 wood 2 nothing'
|
| 414 |
+
"""
|
| 415 |
+
if len(parts) < 2:
|
| 416 |
+
raise UserInputError("Player trade format: 'trade player [player_id_or_name] [give_resources] [for/get] [receive_resources]'\n"
|
| 417 |
+
"Examples:\n"
|
| 418 |
+
" - trade player 1 wood sheep (1:1 simple)\n"
|
| 419 |
+
" - trade player 1 wood 2 sheep 1 (with amounts)\n"
|
| 420 |
+
" - trade player 1 wood 2 brick 1 for wheat 2 (separator word)\n"
|
| 421 |
+
" - trade player 1 nothing wheat 1 (gift from them)")
|
| 422 |
|
| 423 |
try:
|
| 424 |
+
# Parse target player
|
| 425 |
try:
|
| 426 |
target_player = int(parts[0])
|
| 427 |
except ValueError:
|
|
|
|
| 438 |
if target_player is None:
|
| 439 |
raise UserInputError(f"Player '{parts[0]}' not found. Use player name or ID (0-{len(game_state.players_state)-1 if game_state else 3})")
|
| 440 |
|
| 441 |
+
# Find separator word (for/get/want) or split at middle
|
| 442 |
+
separator_words = ['for', 'get', 'want', 'receive']
|
| 443 |
+
separator_index = None
|
| 444 |
+
|
| 445 |
+
for i, part in enumerate(parts[1:], start=1):
|
| 446 |
+
if part.lower() in separator_words:
|
| 447 |
+
separator_index = i
|
| 448 |
+
break
|
| 449 |
+
|
| 450 |
+
# Parse offer and request based on separator
|
| 451 |
+
if separator_index:
|
| 452 |
+
# Has separator word
|
| 453 |
+
offer_parts = parts[1:separator_index]
|
| 454 |
+
request_parts = parts[separator_index + 1:]
|
| 455 |
+
else:
|
| 456 |
+
# No separator - try to detect format
|
| 457 |
+
# Check if it's old simple format (2 resources) or new format with amounts
|
| 458 |
+
remaining = parts[1:]
|
| 459 |
+
|
| 460 |
+
# Try to parse as pairs: [resource amount resource amount...]
|
| 461 |
+
offer, request = self._split_trade_resources(remaining)
|
| 462 |
+
offer_parts = offer
|
| 463 |
+
request_parts = request
|
| 464 |
+
|
| 465 |
+
# Parse offer and request
|
| 466 |
+
offer_dict = self._parse_resource_list(offer_parts if separator_index else offer_parts)
|
| 467 |
+
request_dict = self._parse_resource_list(request_parts if separator_index else request_parts)
|
| 468 |
|
| 469 |
params = {
|
| 470 |
'target_player': target_player,
|
| 471 |
+
'offer': offer_dict,
|
| 472 |
+
'request': request_dict
|
| 473 |
}
|
| 474 |
|
| 475 |
return Action(ActionType.TRADE_PROPOSE, self.user_id, params)
|
| 476 |
|
| 477 |
+
except (ValueError, KeyError) as e:
|
| 478 |
+
raise UserInputError(f"Invalid trade format: {e}")
|
| 479 |
+
|
| 480 |
+
def _split_trade_resources(self, parts: List[str]) -> tuple:
|
| 481 |
+
"""
|
| 482 |
+
Split trade resources into offer and request when no separator word is present.
|
| 483 |
+
Handles both simple format (wood sheep) and amount format (wood 2 sheep 1).
|
| 484 |
+
|
| 485 |
+
Strategy: Parse from left, consuming resource-amount pairs.
|
| 486 |
+
When we've consumed about half the tokens, split there.
|
| 487 |
+
"""
|
| 488 |
+
if len(parts) == 2:
|
| 489 |
+
# Simple 1:1 format: [resource1] [resource2]
|
| 490 |
+
return [parts[0]], [parts[1]]
|
| 491 |
+
|
| 492 |
+
# Parse tokens and identify resource groups
|
| 493 |
+
# A group is: resource_name [optional_number]
|
| 494 |
+
groups = []
|
| 495 |
+
i = 0
|
| 496 |
+
while i < len(parts):
|
| 497 |
+
token = parts[i]
|
| 498 |
+
|
| 499 |
+
# Skip standalone numbers (shouldn't happen but safeguard)
|
| 500 |
+
if token.isdigit():
|
| 501 |
+
i += 1
|
| 502 |
+
continue
|
| 503 |
+
|
| 504 |
+
# Check if this is 'nothing' keyword
|
| 505 |
+
if token.lower() == 'nothing':
|
| 506 |
+
groups.append([token])
|
| 507 |
+
i += 1
|
| 508 |
+
continue
|
| 509 |
+
|
| 510 |
+
# Check if next token is a number
|
| 511 |
+
if i + 1 < len(parts) and parts[i + 1].isdigit():
|
| 512 |
+
# Resource with amount: [resource, amount]
|
| 513 |
+
groups.append([token, parts[i + 1]])
|
| 514 |
+
i += 2
|
| 515 |
+
else:
|
| 516 |
+
# Resource without amount: [resource]
|
| 517 |
+
groups.append([token])
|
| 518 |
+
i += 1
|
| 519 |
+
|
| 520 |
+
# Split groups roughly in half
|
| 521 |
+
mid = len(groups) // 2
|
| 522 |
+
|
| 523 |
+
# Flatten groups back to parts
|
| 524 |
+
offer_parts = []
|
| 525 |
+
for group in groups[:mid]:
|
| 526 |
+
offer_parts.extend(group)
|
| 527 |
+
|
| 528 |
+
request_parts = []
|
| 529 |
+
for group in groups[mid:]:
|
| 530 |
+
request_parts.extend(group)
|
| 531 |
+
|
| 532 |
+
return offer_parts, request_parts
|
| 533 |
+
|
| 534 |
+
def _parse_resource_list(self, parts: List[str]) -> dict:
|
| 535 |
+
"""
|
| 536 |
+
Parse a list of resources with optional amounts.
|
| 537 |
+
Examples:
|
| 538 |
+
- ['wood', 'brick'] -> {'wood': 1, 'brick': 1}
|
| 539 |
+
- ['wood', '2', 'brick', '1'] -> {'wood': 2, 'brick': 1}
|
| 540 |
+
- ['nothing'] -> {}
|
| 541 |
+
- [] -> {}
|
| 542 |
+
"""
|
| 543 |
+
if not parts or (len(parts) == 1 and parts[0].lower() == 'nothing'):
|
| 544 |
+
return {}
|
| 545 |
+
|
| 546 |
+
result = {}
|
| 547 |
+
i = 0
|
| 548 |
+
|
| 549 |
+
while i < len(parts):
|
| 550 |
+
# Skip if this is a number (should have been consumed as amount)
|
| 551 |
+
if parts[i].isdigit():
|
| 552 |
+
i += 1
|
| 553 |
+
continue
|
| 554 |
+
|
| 555 |
+
resource_name = parts[i]
|
| 556 |
+
|
| 557 |
+
# Check for 'nothing' keyword
|
| 558 |
+
if resource_name.lower() == 'nothing':
|
| 559 |
+
i += 1
|
| 560 |
+
continue
|
| 561 |
+
|
| 562 |
+
# Check if next part is a number (amount)
|
| 563 |
+
amount = 1
|
| 564 |
+
if i + 1 < len(parts) and parts[i + 1].isdigit():
|
| 565 |
+
amount = int(parts[i + 1])
|
| 566 |
+
i += 2
|
| 567 |
+
else:
|
| 568 |
+
i += 1
|
| 569 |
+
|
| 570 |
+
# Parse the resource
|
| 571 |
+
try:
|
| 572 |
+
resource = self._parse_resource(resource_name)
|
| 573 |
+
resource_key = resource.name.lower()
|
| 574 |
+
|
| 575 |
+
# Add to result (accumulate if resource appears multiple times)
|
| 576 |
+
if resource_key in result:
|
| 577 |
+
result[resource_key] += amount
|
| 578 |
+
else:
|
| 579 |
+
result[resource_key] = amount
|
| 580 |
+
except UserInputError:
|
| 581 |
+
# If we can't parse it as a resource, skip it
|
| 582 |
+
# This handles edge cases
|
| 583 |
+
continue
|
| 584 |
+
|
| 585 |
+
return result
|
| 586 |
|
| 587 |
def _parse_use_dev_card(self, parts: List[str], game_state: GameState) -> Action:
|
| 588 |
"""Parse development card usage command."""
|
|
|
|
| 838 |
print()
|
| 839 |
print("💰 TRADING:")
|
| 840 |
print(" trade bank <give> <amount> <get> <amount>")
|
| 841 |
+
print(" Example: 'trade bank wood 4 sheep 1'")
|
| 842 |
+
print()
|
| 843 |
+
print(" trade player <id_or_name> <resources_offer> [for] <resources_want>")
|
| 844 |
+
print(" Simple 1:1: 't player 1 wood sheep'")
|
| 845 |
+
print(" With amounts: 't player bob wood 2 sheep 1'")
|
| 846 |
+
print(" Multiple: 't player 1 wood 2 brick 1 for wheat 2 ore 1'")
|
| 847 |
+
print(" Gifts: 't player 2 nothing wheat 1' (receive gift)")
|
| 848 |
+
print(" Gifts: 't player 2 wood 3 nothing' (give gift)")
|
| 849 |
+
print(" 💡 Use 'for' or 'get' to separate offer from request")
|
| 850 |
print()
|
| 851 |
print("🃏 DEVELOPMENT CARDS:")
|
| 852 |
print(" buy / dev - Buy dev card (cost: 1 Ore + 1 Sheep + 1 Wheat)")
|
tests/test_human_user.py
CHANGED
|
@@ -459,4 +459,152 @@ class TestCommandHistory:
|
|
| 459 |
# Check that all commands were added to history
|
| 460 |
assert len(self.user.command_history) == len(commands)
|
| 461 |
for cmd in commands:
|
| 462 |
-
assert cmd in self.user.command_history
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
# Check that all commands were added to history
|
| 460 |
assert len(self.user.command_history) == len(commands)
|
| 461 |
for cmd in commands:
|
| 462 |
+
assert cmd in self.user.command_history
|
| 463 |
+
|
| 464 |
+
|
| 465 |
+
class TestAdvancedTrading:
|
| 466 |
+
"""Test advanced trading functionality with flexible amounts."""
|
| 467 |
+
|
| 468 |
+
def setup_method(self):
|
| 469 |
+
"""Set up test fixtures."""
|
| 470 |
+
self.user = HumanUser("TestUser", 0)
|
| 471 |
+
self.game_state = GameState()
|
| 472 |
+
|
| 473 |
+
def test_simple_one_to_one_trade(self):
|
| 474 |
+
"""Test simple 1:1 trade (backward compatibility)."""
|
| 475 |
+
action = self.user._parse_input("trade player 1 wood sheep", self.game_state)
|
| 476 |
+
|
| 477 |
+
assert action.action_type == ActionType.TRADE_PROPOSE
|
| 478 |
+
assert action.parameters['target_player'] == 1
|
| 479 |
+
assert action.parameters['offer'] == {'wood': 1}
|
| 480 |
+
assert action.parameters['request'] == {'sheep': 1}
|
| 481 |
+
|
| 482 |
+
def test_trade_with_amounts(self):
|
| 483 |
+
"""Test trade with explicit amounts."""
|
| 484 |
+
action = self.user._parse_input("trade player 1 wood 2 sheep 1", self.game_state)
|
| 485 |
+
|
| 486 |
+
assert action.action_type == ActionType.TRADE_PROPOSE
|
| 487 |
+
assert action.parameters['target_player'] == 1
|
| 488 |
+
assert action.parameters['offer'] == {'wood': 2}
|
| 489 |
+
assert action.parameters['request'] == {'sheep': 1}
|
| 490 |
+
|
| 491 |
+
def test_trade_multiple_resources(self):
|
| 492 |
+
"""Test trade with multiple resources on both sides."""
|
| 493 |
+
action = self.user._parse_input("trade player 1 wood 2 brick 1 for wheat 2 ore 1", self.game_state)
|
| 494 |
+
|
| 495 |
+
assert action.action_type == ActionType.TRADE_PROPOSE
|
| 496 |
+
assert action.parameters['target_player'] == 1
|
| 497 |
+
assert action.parameters['offer'] == {'wood': 2, 'brick': 1}
|
| 498 |
+
assert action.parameters['request'] == {'wheat': 2, 'ore': 1}
|
| 499 |
+
|
| 500 |
+
def test_trade_with_separator_words(self):
|
| 501 |
+
"""Test trade with various separator words."""
|
| 502 |
+
separators = ['for', 'get', 'want', 'receive']
|
| 503 |
+
|
| 504 |
+
for sep in separators:
|
| 505 |
+
action = self.user._parse_input(f"trade player 1 wood 2 {sep} sheep 1", self.game_state)
|
| 506 |
+
|
| 507 |
+
assert action.action_type == ActionType.TRADE_PROPOSE
|
| 508 |
+
assert action.parameters['offer'] == {'wood': 2}
|
| 509 |
+
assert action.parameters['request'] == {'sheep': 1}
|
| 510 |
+
|
| 511 |
+
def test_gift_receiving(self):
|
| 512 |
+
"""Test receiving a gift (nothing for something)."""
|
| 513 |
+
action = self.user._parse_input("trade player 1 nothing wheat 1", self.game_state)
|
| 514 |
+
|
| 515 |
+
assert action.action_type == ActionType.TRADE_PROPOSE
|
| 516 |
+
assert action.parameters['offer'] == {}
|
| 517 |
+
assert action.parameters['request'] == {'wheat': 1}
|
| 518 |
+
|
| 519 |
+
def test_gift_giving(self):
|
| 520 |
+
"""Test giving a gift (something for nothing)."""
|
| 521 |
+
action = self.user._parse_input("trade player 1 wood 3 nothing", self.game_state)
|
| 522 |
+
|
| 523 |
+
assert action.action_type == ActionType.TRADE_PROPOSE
|
| 524 |
+
assert action.parameters['offer'] == {'wood': 3}
|
| 525 |
+
assert action.parameters['request'] == {}
|
| 526 |
+
|
| 527 |
+
def test_extreme_trade_zero_for_five(self):
|
| 528 |
+
"""Test extreme trade: 0 cards for 5 cards (use separator for clarity)."""
|
| 529 |
+
action = self.user._parse_input("trade player 1 nothing for wheat 2 ore 2 brick 1", self.game_state)
|
| 530 |
+
|
| 531 |
+
assert action.action_type == ActionType.TRADE_PROPOSE
|
| 532 |
+
assert action.parameters['offer'] == {}
|
| 533 |
+
assert action.parameters['request'] == {'wheat': 2, 'ore': 2, 'brick': 1}
|
| 534 |
+
|
| 535 |
+
def test_extreme_trade_five_for_zero(self):
|
| 536 |
+
"""Test extreme trade: 5 cards for 0 cards (use separator for clarity)."""
|
| 537 |
+
action = self.user._parse_input("trade player 1 wood 2 brick 2 sheep 1 for nothing", self.game_state)
|
| 538 |
+
|
| 539 |
+
assert action.action_type == ActionType.TRADE_PROPOSE
|
| 540 |
+
assert action.parameters['offer'] == {'wood': 2, 'brick': 2, 'sheep': 1}
|
| 541 |
+
assert action.parameters['request'] == {}
|
| 542 |
+
|
| 543 |
+
def test_complex_multi_resource_trade(self):
|
| 544 |
+
"""Test complex trade with many resources on both sides."""
|
| 545 |
+
action = self.user._parse_input("trade player 1 wood 3 brick 2 sheep 1 for wheat 4 ore 2", self.game_state)
|
| 546 |
+
|
| 547 |
+
assert action.action_type == ActionType.TRADE_PROPOSE
|
| 548 |
+
assert action.parameters['offer'] == {'wood': 3, 'brick': 2, 'sheep': 1}
|
| 549 |
+
assert action.parameters['request'] == {'wheat': 4, 'ore': 2}
|
| 550 |
+
|
| 551 |
+
def test_trade_with_player_name(self):
|
| 552 |
+
"""Test trading with player name instead of ID."""
|
| 553 |
+
from pycatan.actions import PlayerState
|
| 554 |
+
|
| 555 |
+
# Create PlayerState objects (it's a dataclass, pass all params)
|
| 556 |
+
alice = PlayerState(
|
| 557 |
+
player_id=0,
|
| 558 |
+
name="Alice",
|
| 559 |
+
cards=[],
|
| 560 |
+
dev_cards=[],
|
| 561 |
+
settlements=[],
|
| 562 |
+
cities=[],
|
| 563 |
+
roads=[],
|
| 564 |
+
victory_points=0,
|
| 565 |
+
longest_road_length=0,
|
| 566 |
+
has_longest_road=False,
|
| 567 |
+
has_largest_army=False,
|
| 568 |
+
knights_played=0
|
| 569 |
+
)
|
| 570 |
+
|
| 571 |
+
bob = PlayerState(
|
| 572 |
+
player_id=1,
|
| 573 |
+
name="Bob",
|
| 574 |
+
cards=[],
|
| 575 |
+
dev_cards=[],
|
| 576 |
+
settlements=[],
|
| 577 |
+
cities=[],
|
| 578 |
+
roads=[],
|
| 579 |
+
victory_points=0,
|
| 580 |
+
longest_road_length=0,
|
| 581 |
+
has_longest_road=False,
|
| 582 |
+
has_largest_army=False,
|
| 583 |
+
knights_played=0
|
| 584 |
+
)
|
| 585 |
+
|
| 586 |
+
# Add player states to game_state
|
| 587 |
+
self.game_state.players_state = [alice, bob]
|
| 588 |
+
|
| 589 |
+
action = self.user._parse_input("trade player bob wood 2 for sheep 1", self.game_state)
|
| 590 |
+
|
| 591 |
+
assert action.action_type == ActionType.TRADE_PROPOSE
|
| 592 |
+
assert action.parameters['target_player'] == 1 # Bob's ID
|
| 593 |
+
assert action.parameters['offer'] == {'wood': 2}
|
| 594 |
+
assert action.parameters['request'] == {'sheep': 1}
|
| 595 |
+
|
| 596 |
+
def test_trade_accumulates_duplicate_resources(self):
|
| 597 |
+
"""Test that duplicate resources in same offer accumulate."""
|
| 598 |
+
action = self.user._parse_input("trade player 1 wood 2 wood 1 for sheep 1", self.game_state)
|
| 599 |
+
|
| 600 |
+
assert action.action_type == ActionType.TRADE_PROPOSE
|
| 601 |
+
assert action.parameters['offer'] == {'wood': 3} # 2 + 1 = 3
|
| 602 |
+
assert action.parameters['request'] == {'sheep': 1}
|
| 603 |
+
|
| 604 |
+
def test_short_trade_command(self):
|
| 605 |
+
"""Test short 't' command for trading."""
|
| 606 |
+
action = self.user._parse_input("t player 1 wood 2 sheep 1", self.game_state)
|
| 607 |
+
|
| 608 |
+
assert action.action_type == ActionType.TRADE_PROPOSE
|
| 609 |
+
assert action.parameters['offer'] == {'wood': 2}
|
| 610 |
+
assert action.parameters['request'] == {'sheep': 1}
|