├── gambling_games
└── __init__.py
├── board_games
└── __init__.py
├── card_games
├── __init__.py
└── solitaire_games
│ ├── __init__.py
│ ├── gargantua_game.py
│ ├── strategy_game.py
│ ├── yukon_game.py
│ ├── monte_carlo_game.py
│ ├── spider_game.py
│ ├── bisley_game.py
│ ├── freecell_game.py
│ ├── forty_thieves_game.py
│ ├── quadrille_game.py
│ ├── canfield_game.py
│ ├── thoughtful_game.py
│ └── pyramid_game.py
├── dice_games
├── __init__.py
└── solitaire_dice_game.py
├── other_games
├── __init__.py
├── country_data.txt
├── dollar_game.py
├── number_guess_game.py
├── rps_game.py
├── hangman_game.py
└── slider_game.py
├── adventure_games
└── __init__.py
├── simulation_games
└── __init__.py
├── __init__.py
├── test.py
├── full_credits.py
├── README.md
├── play.py
├── other_cmd.py
└── utility.py
/gambling_games/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | __init__.py
3 |
4 | dummy init file for gambing_games
5 | """
--------------------------------------------------------------------------------
/board_games/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | __init__.py
3 |
4 | Dummy package maker for tgames.board_games.
5 | """
6 |
--------------------------------------------------------------------------------
/card_games/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | __init__.py
3 |
4 | Dummy package maker for tgames.board_games.
5 | """
6 |
--------------------------------------------------------------------------------
/dice_games/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | __init__.py
3 |
4 | This is the dummy init file for dice_games
5 | """
6 |
--------------------------------------------------------------------------------
/other_games/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | __init__.py
3 |
4 | Dummy package maker for tgames.other_games.
5 | """
6 |
--------------------------------------------------------------------------------
/adventure_games/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | __init__.py
3 |
4 | Dummy package maker for tgames.adventure_games.
5 | """
6 |
--------------------------------------------------------------------------------
/simulation_games/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | __init__.py
3 |
4 | Dummy package maker for tgames.simulation_games.
5 | """
6 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | __init__.py
3 |
4 | Dummy package maker for tgames.board_games.
5 | """
6 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | __init__.py
3 |
4 | Package initializer for t_games.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 |
8 | This program is free software: you can redistribute it and/or modify it under
9 | the terms of the GNU General Public License as published by the Free Software
10 | Foundation, either version 3 of the License, or (at your option) any later
11 | version.
12 |
13 | This program is distributed in the hope that it will be useful, but WITHOUT
14 | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15 | FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
16 | details.
17 |
18 | See for details on this license (GPLv3).
19 | """
20 |
21 |
22 | from . import board
23 | from . import cards
24 | from . import dice
25 |
26 | from .interface import Interface
27 | from .play import play, test
28 |
29 |
30 | if __name__ == '__main__':
31 | play()
32 |
--------------------------------------------------------------------------------
/test.py:
--------------------------------------------------------------------------------
1 | """
2 | test.py
3 |
4 | Testing t_games.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Classes:
10 | Test: An object for saving state when testing. (object)
11 |
12 | Functions:
13 | test: Test some text games. (None)
14 | """
15 |
16 |
17 | from __future__ import print_function
18 |
19 | try:
20 | # Standard imports.
21 | from . import interface
22 | from . import player
23 | except (ValueError, ImportError):
24 | try:
25 | # Imports for running play.py independently.
26 | from t_games import interface
27 | from t_games import player
28 | except ImportError:
29 | # Imports for running play.py from the t_games folder in 2.7.
30 | import os
31 | import sys
32 | here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
33 | sys.path.insert(0, here)
34 | from t_games import interface
35 | from t_games import player
36 |
37 |
38 | class Test(object):
39 | """
40 | An object for saving state when testing. (object)
41 |
42 | Attributes:
43 | human: The test player. (player.Tester)
44 | menu: The t_games interface object. (interface.Interface)
45 |
46 | Overridden Methods:
47 | __init__
48 | __call__
49 | """
50 |
51 | def __init__(self):
52 | """Set up the saved state attributes. (None)"""
53 | self.human = player.Tester()
54 | self.menu = interface.Interface(self.human)
55 |
56 | def __call__(self, held_inputs = []):
57 | """
58 | Test t_games when called. (list of lists)
59 |
60 | The return value is a list of results from the games played.
61 |
62 | Parameters:
63 | held_inputs: The commands for the player to start with. (list of str)
64 | """
65 | self.human.held_inputs = held_inputs
66 | self.menu.menu()
67 | return self.human.results[self.human.session_index:]
68 |
69 |
70 | # Test some text games. (None)
71 | test = Test()
72 |
73 |
74 | if __name__ == '__main__':
75 | test()
76 |
--------------------------------------------------------------------------------
/other_games/country_data.txt:
--------------------------------------------------------------------------------
1 | 9
2 | United States
3 | 1600, 800, .8, 100
4 | United Kingdom, Ireland, Germany, Spain, Portugal, Saudi Arabia, Mexico, Canada, Australia, Japan, Thailand, Israel, Jordan, Egypt, Kenya, Colombia, Pakistan, South Korea, Turkey, Greece, Estonia, Latvia, Lithuania, Poland, Czechia, Ukraine, Libya, Taiwan
5 | Russia, Cuba, China, Venezuela, Iran, Syria, Moldova, Grenada, Lebanon, Myanmar, North Korea
6 | Russia
7 | 1600, 800, .85, 95
8 | India, Syria, Brazil, Venezuela, Cuba, Italy, Germany, Azerbaijan, Belarus, Kazakhstan, Kyrgyzstan, Armenia, Moldova, Uzbekistan, Turkmenistan, China, Indonesia, Iran, Botswana, Mozambique, Namibia, Sudan, Zimbabwe, Bolivia, Grenada, Uruguay, Armenia, Indonesia, Lebanon, Mongolia, Myanmar, North Korea, Sri Lanka
9 | Turkey, Ukraine, United States, Afganistan, Estonia, Latvia, Lithuania, Georgia, Poland, Czechia, Japan, Libya, Pakistan, Chad
10 | United Kingdom
11 | 120, 700, 9, 0
12 | United States, Ireland, Canada, Australia, Chile, Colombia, Panama, Brunei, Israel, Japan, Kazakhstan, Oman, South Korea, Turkey, Finland, Poland, Germany, New Zealand, Nigeria, Barbados, Estonia, Latvia, Lithuania, Libya, India
13 | Russia, Argentina, Iran
14 | France
15 | 180, 600, 0, 0
16 | Turkey, Libya, Egypt, Germany, Democratic Republic of the Congo, Chad, Niger, Brazil, Canada, India, Indonesia, Japan, South Korea, Qatar, Bosnia and Herzegovina, Greece, Ireland, Latvia
17 | Madagascar, Comoros, Mauritius, China, North Korea
18 | China
19 | 140, 900, .8, 25
20 | Russia, Myanmar, North Korea, Pakistan, France, Italy, Algeria, Sudan, Democratic Republic of the Congo, Nigeria, Egypt, Zimbabwe, Venezuela, Barbados, Cuba, Australia, Samoa
21 | Taiwan, United Kingdom, Turkey, Libya, Mongolia, France, Japan, Vietnam, Italy, Germany, India, Poland, United States, Jordan, Bangladesh
22 | India
23 | 70, 500, .7, 15
24 | Russia, Israel, Afganistan, France, Bhutan, Bangladesh, South Africa, Brazil, Mexico, Japan, Germany, Indonesia, Brazil, Mongolia, Singapore, United Arab Emirates, Ghana, Kenya, Lesotho, Mauritius, Morocco, Namibia, Nigeria, South Africa
25 | Pakistan, United Kingdom, Turkey
26 | Pakistan
27 | 70, 400, 0, 0
28 | United States, Turkey, United Kingdom, China, Indonesia, Algeria, Tunisia, Morocco, Eritrea, Saudi Arabia, France
29 | India, Afganistan, Israel
30 | North Korea
31 | 7, 0, 0, 0
32 | China, Russia
33 | South Korea, United States, Turkey, Portugal, Iraq, Chile, Argentina
34 | Israel
35 | 40, 900, .75, 10
36 | United States, Mexico, Canada, China
37 | Turkey, Algeria, Bahrain, Comoros, Djibouti, Iraq, Kuwait, Lebanon, Libya, Morocco, Qatar, Saudi Arabia, Somalia, Sudan, Syria, Tunisia, United Arab Emirates, Yemen, Afganistan, Bangladesh, Brunei, Indonesia, Iran, Malaysia, Mali, Niger, Pakistan
--------------------------------------------------------------------------------
/full_credits.py:
--------------------------------------------------------------------------------
1 | """
2 | full_credits.py
3 |
4 | Generate the full credits for tgames.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS_DATA: The sections and people who worked on those sections. (list)
11 | FULL_CREDITS: The formatted text of CREDITS_DATA. (str)
12 |
13 | Functions:
14 | data_to_text: Convert CREDITS_DATA to formatted text. (str)
15 | """
16 |
17 |
18 | CREDITS_DATA = [('Interface Programming', """Craig O'Brien, Micah Page"""),
19 | ('Game Design', """Humans; Paul Alfille, C. Graham Baker, Elwood T. Baker, Matt Baker,
20 | Richard A. Canfield, Melvin Dresher, Doug Dyment, Charles Fey, Merrill Flood, Michael Keller,
21 | Bernard de Marigny, Albert Morehead, Geoffrey Mott-Smith, Craig O'Brien, Ned Strongin, Sid Sackson,
22 | John Suckling, Howard Wexler, Gregory Yob"""),
23 | ('Game Programming', """Thomas Ahle, Craig O'Brien, David B. Wilson"""),
24 | ('Bot Design', """Robert Axelrod, B. Beaufils, R. Boyd, S. Braver, K. Deb, J. Delahaye, James Friedman,
25 | Roger Johnson, Reiner Knizia, S. Komorita, David Kraines, Vivian Kraines, J. P. Lorberbaum, J. Li,
26 | S. Mittal, J. Nachbar, Todd Neller, Craig O'Brien, Mathieu Our, Clifton Presser, Anatol Rapoport,
27 | J. Sheposh, R. Sugden, Gerry Tesauro"""),
28 | ('Bot Programming', """Craig "Ichabod" O'Brien"""),
29 | ('Documentation', """wma, Craig O'Brien"""),
30 | ('Play Testing', """Doug Edmunds, Craig O'Brien, Micah Page, Dustin Roberts"""),
31 | ('Special Thanks', """Guido van Rossum; python-forum.io, github.com, Wikipedia, pagat.com;
32 | Alan Beale, Bill Budge, Kris Burm, George Carlin, Matt Groening, Lawrence Lasker, Walter F. Parks,
33 | David Parlett""")]
34 |
35 |
36 | def data_to_text():
37 | """Convert CREDITS_DATA to formatted text. (str)"""
38 | credits_text = ''
39 | # Loop through the sections.
40 | for section, people in CREDITS_DATA:
41 | # Show the section title.
42 | credits_text += '\n{:^79}\n{:^79}\n'.format(section, '-' * len(section))
43 | for line in people.split(';'):
44 | # Loop through the names.
45 | names = line.strip().split(',')
46 | while names:
47 | # Display the names four at a time.
48 | quad, names = names[:4], names[4:]
49 | quad_text = ''.join(['{:^20}'.format(name.strip()) for name in quad])
50 | credits_text += '{:^79}\n'.format(quad_text)
51 | return credits_text
52 |
53 |
54 | # The formatted text of CREDITS_DATA.
55 | FULL_CREDITS = data_to_text()
56 |
57 |
58 | if __name__ == '__main__':
59 | print(FULL_CREDITS)
60 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## t_games
2 |
3 | The t_games project is a suite of text games to play on a command-line interface (CLI). It contains board games, card games, dice games, and other miscellany. It is also a framework of useful objects for writing your own CLI games. You can easily write your own game as a subclass of the main Game class. If you include the game in the t_games folders, it will be detected by the system and included in the t_games interface. See [Game Objects](https://github.com/ichabod801/t_games/wiki/Game-Objects) in the wiki for more details.
4 |
5 | ### Getting Started
6 |
7 | Simply download the files and use the play function:
8 |
9 | ```python
10 | import t_games
11 | t_games.play()
12 | ```
13 |
14 | If you download the files to a folder in your Python path, you should be able to do this from any location. Otherwise you will need to be in the parent folder of the t_games folder (2.7 or 3.0+) or the t_games folder itself (3.0+ only).
15 |
16 | Alternatively, you can go to the folder you downloaded them into and run `python play.py`.
17 |
18 | The system will ask you three questions to uniquely identify you, and then it will set up a player profile so you can play games. It's a standard, old-fashioned menu system, but you can type `help` for ways to get around that.
19 |
20 | #### Prerequisites
21 |
22 | None. The t_games system is designed to run in base Pyton, in either 2.7 or the latest 3.x.
23 |
24 | ### Contributing
25 |
26 | The t_games project is idling at the moment. I wanted to get the project to a point I could call "finished," and it's there. Now I want to move on. If you would like to do some development on the project or add some games to it, please email me at t_admin at xenomind dot com.
27 |
28 | I will certainly work on any bug reports I get. Either make an issue and add it to the Weevilwood project, or email me at t_admin at xenomind dot com. Please include as much information as possible. Best is a copy and paste from the console including the last game state, your last action, and the full text of the traceback (or a description of why the output was wrong). If you were playing the game with any options, please include those as well.
29 |
30 | ### Versioning
31 |
32 | The t_games project uses a three number versioning system compatible with [PEP 440](https://www.python.org/dev/peps/pep-0440/). The first number is the major release cycle, the second number is the number of games contained in the release, and the third number is the number of changes since the last game was released. Alpha and beta releases will be identified as per GitHub conventions.
33 |
34 | ### Authors
35 |
36 | * **Craig "Ichabod" O'Brien** :skull: wrote the original interface and the initial games.
37 |
38 | Type `credits` from the main menu to see a list of contributors and acknowledgements.
39 |
40 | ### License
41 |
42 | This project is licensed under the GPLv3 license. See the LICENSE.md file for details.
--------------------------------------------------------------------------------
/play.py:
--------------------------------------------------------------------------------
1 | """
2 | play.py
3 |
4 | Playing t_games.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Classes:
10 | Play: An object for saving state when playing. (object)
11 | Test: An object for saving state when play testing. (object)
12 |
13 | Functions:
14 | play: Play t_games. (list of lists)
15 | test: Play t_games with a default player. (list of lists)
16 | """
17 |
18 |
19 | from __future__ import print_function
20 |
21 | import getopt
22 | import sys
23 |
24 | try:
25 | # Standard imports.
26 | from . import interface
27 | from . import player
28 | except (ValueError, ImportError):
29 | try:
30 | # Imports for running play.py independently.
31 | from t_games import interface
32 | from t_games import player
33 | except ImportError:
34 | # Imports for running play.py from the t_games folder in 2.7.
35 | import os
36 | import sys
37 | here = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
38 | sys.path.insert(0, here)
39 | from t_games import interface
40 | from t_games import player
41 |
42 |
43 | class Play(object):
44 | """
45 | An object for saving state when playing. (object)
46 |
47 | Attributes:
48 | human: The player. (player.Human)
49 | menu: The t_games interface object. (interface.Interface)
50 |
51 | Methods:
52 | reset: Set the player and the interface. (None)
53 |
54 | Overridden Methods:
55 | __init__
56 | __call__
57 | """
58 |
59 | def __init__(self):
60 | """Set up the saved state attributes. (None)"""
61 | self.human = None
62 | self.menu = None
63 |
64 | def __call__(self, held_inputs = [], action = 'play'):
65 | """
66 | Play t_games. (list of lists)
67 |
68 | The action can be 'play' (play t_games), 'test' (play t_games with a default
69 | player), or 'auto' (run the automatic testing). For the 'auto' action,
70 | held_inputs can specify the test files to run.
71 |
72 | The return value is a list of results from the games played.
73 |
74 | Parameters:
75 | held_inputs: The commands for the player to start with. (list of str)
76 | action: The action to take. (str)
77 | """
78 | if self.human is None or self.menu is None:
79 | self.reset()
80 | # Play the requested game.
81 | self.human.held_inputs = held_inputs
82 | self.menu.menu()
83 | # Handle the results
84 | results = self.human.results[self.human.session_index:]
85 | self.human.session_index = len(self.human.results)
86 | return results
87 |
88 | def reset(self):
89 | """Set the player and the interface. (None)"""
90 | print()
91 | self.human = player.Human()
92 | self.menu = interface.Interface(self.human)
93 |
94 |
95 | # Play some text games.
96 | play = Play()
97 |
98 |
99 | class Test(Play):
100 | """
101 | An object for saving state when play testing. (object)
102 |
103 | Overridden Methods:
104 | reset
105 | """
106 |
107 | def reset(self):
108 | """Set the player and the interface. (None)"""
109 | self.human = player.Tester()
110 | self.menu = interface.Interface(self.human)
111 |
112 |
113 | # Test some games
114 | test = Test()
115 |
116 |
117 | if __name__ == '__main__':
118 | # Get any options.
119 | try:
120 | opts, args = getopt.getopt(sys.argv[1:], 't')
121 | except getopt.GetoptError as err:
122 | print(err)
123 | sys.exit(2)
124 | # Check for testing.
125 | if ('-t', '') in opts:
126 | play_function = test
127 | else:
128 | play_function = play
129 | play_function(args)
130 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/gargantua_game.py:
--------------------------------------------------------------------------------
1 | """
2 | gargantua_game.py
3 |
4 | A game of Gargantua (two-deck Klondike).
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: Credits for Gargantua. (str)
11 | OPTIONS: Options for Gargantua. (str)
12 | RULES: Rules for Gargantua. (str)
13 |
14 | Classes:
15 | Gargantua: A game of Gargantua. (solitaire.Solitaire)
16 | """
17 |
18 |
19 | from . import solitaire_game as solitaire
20 |
21 |
22 | CREDITS = """
23 | Game Design: Traditional
24 | Game Programming: Craig "Ichabod" O'Brien
25 | """
26 |
27 | RULES = """
28 | Gargantua is basically two-deck Klondike. The cards are dealt in a triangle of
29 | nine piles, the first pile having one card, and each pile to the right having
30 | one more card.
31 |
32 | Cards on the tableau are built down in rank and alternating in color. Cards
33 | are sorted to the foundations up in suit from the ace. Empty tableau piles may
34 | be filled with a king or a stack starting with a king.
35 |
36 | Cards from the stock are turned over one at a time. The stock may only be gone
37 | through twice.
38 | """
39 |
40 | OPTIONS = """
41 | harp (h): Equivalent to max-passes = 4.
42 | gonzo (gz): Equivalent to max-passes = -1 piles = 10.
43 | max-passes= (mp=): How many times you can go through the stock.
44 | piles= (p=): How many tableau piles there should be.
45 | """
46 |
47 |
48 | class Gargantua(solitaire.MultiSolitaire):
49 | """
50 | A game of Gargantua. (Solitaire)
51 |
52 | Overridden Methods:
53 | set_checkers
54 | """
55 |
56 | aka = ['Double Klondike', 'Jumbo', 'Garg']
57 | categories = ['Card Games', 'Solitaire Games', 'Finding Games']
58 | credits = CREDITS
59 | name = 'Gargantua'
60 | num_options = 2
61 | options = OPTIONS
62 | rules = RULES
63 |
64 | def do_gipf(self, arguments):
65 | """
66 | Mate turns all of the aces face up.
67 |
68 | Calvin Cards gives you another pass through the deck.
69 | """
70 | game, losses = self.gipf_check(arguments, ('mate', 'calvin cards'))
71 | # Mate turns all of the aces face up.
72 | if game == 'mate':
73 | if not losses:
74 | for card in self.deck.in_play:
75 | if card.rank == 'A':
76 | card.up = True
77 | elif game == 'calvin cards':
78 | if not losses:
79 | self.max_passes += 1
80 | # Otherwise I'm confused.
81 | else:
82 | self.human.tell("Gargantua decidedly dislikes miniscule linguistic particulates.")
83 | return True
84 |
85 | def set_checkers(self):
86 | """Set up the game specific rules. (None)"""
87 | super(Gargantua, self).set_checkers()
88 | # Set the game specific rules checkers.
89 | self.lane_checkers = [solitaire.lane_king]
90 | self.pair_checkers = [solitaire.pair_down, solitaire.pair_alt_color]
91 | self.sort_checkers = [solitaire.sort_ace, solitaire.sort_up]
92 | # Set the dealers
93 | self.dealers = [solitaire.deal_klondike, solitaire.deal_stock_all]
94 |
95 | def set_options(self):
96 | """Define the options for the game. (None)"""
97 | self.options = {'max-passes': 2, 'num-foundations': 8, 'turn-count': 1}
98 | self.option_set.add_option('piles', ['p'], action = 'key=num-tableau', target = self.options,
99 | default = 9, converter = int, question = 'How many tableau piles should their be? ')
100 | self.option_set.add_option('max-passes', ['mp'], action = 'key=max-passes', target = self.options,
101 | default = 2, converter = int, question = 'How many times can you go through the stock? ')
102 | self.option_set.add_group('gonzo', ['gz'], 'max-passes = -1 piles = 10')
103 | self.option_set.add_group('harp', ['h'], 'max-passes=4')
104 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/strategy_game.py:
--------------------------------------------------------------------------------
1 | """
2 | strategy_game.py
3 |
4 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
5 | See the top level __init__.py file for details on the t_games license.
6 |
7 | Constants:
8 | CREDITS: The credits for Strategy. (str)
9 | OPTIONS: The options for Strategy. (str)
10 | RULES: The rules for Strategy. (str)
11 |
12 | Classes:
13 | Strategy: A game of Strategy. (solitaire.Solitaire)
14 | """
15 |
16 |
17 | from . import solitaire_game as solitaire
18 |
19 |
20 | CREDITS = """
21 | Game Design: Albert Morehead and Geoffrey Mott-Smith
22 | Game Programming: Craig "Ichabod" O'Brien
23 | """
24 |
25 | RULES = """
26 | All of the cards are dealt to the reserve. You may move the top card of the
27 | reserve onto any of the eight tableau piles. Aces may be sorted as they
28 | appear, but no other card may be sorted until the reserve is empty.
29 |
30 | Parlett suggests that each time you win, play again with one less tableau
31 | pile.
32 | """
33 |
34 | OPTIONS = """
35 | piles= (p=): The number of tableau piles (1-8).
36 | """
37 |
38 |
39 | class Strategy(solitaire.Solitaire):
40 | """
41 | A game of Strategy. (solitaire.Solitaire)
42 |
43 | Overridden Methods:
44 | set_checkers
45 | set_options
46 | """
47 |
48 | aka = ['Stra']
49 | credits = CREDITS
50 | categories = ['Card Games', 'Solitaire Games', 'Revealing Games']
51 | name = 'Strategy'
52 | num_options = 1
53 | options = OPTIONS
54 | rules = RULES
55 |
56 | def do_gipf(self, arguments):
57 | """
58 | Monte Carlo allows reverses one pile.
59 |
60 | Roulette lets you swap two adjacent cards.
61 | """
62 | game, losses = self.gipf_check(arguments, ('monte carlo', 'roulette'))
63 | go = True
64 | # A Monte Carlo win lets you reverse one pile.
65 | if game == 'monte carlo':
66 | if not losses:
67 | # Remind the human.
68 | self.human.tell(self)
69 | # Get a foundation pile.
70 | while True:
71 | card_text = self.human.ask('Pick a card on the tableau: ')
72 | if self.deck.card_re.match(card_text):
73 | card = self.deck.find(card_text)
74 | if card.game_location in self.tableau:
75 | break
76 | else:
77 | self.human.error('That card is not in the tableau.')
78 | else:
79 | self.human.error('I do not recognize that card.')
80 | # Reverse the pile.
81 | card.game_location.reverse()
82 | go = False
83 | # A Roulette win lets you swap (spin) two adjacent cards.
84 | elif game == 'roulette':
85 | if not losses:
86 | # Remind the human.
87 | self.human.tell(self)
88 | while True:
89 | # Get two cards from the human.
90 | card_text = self.human.ask('\nWhich two adjacent cards would you like to spin? ')
91 | cards = self.deck.card_re.findall(card_text)
92 | cards = [self.deck.find(card) for card in cards]
93 | # Check that they are next to each other.
94 | if len(cards) != 2:
95 | self.human.tell('Please pick two cards.')
96 | elif cards[0].game_location != cards[1].game_location:
97 | self.human.tell('The two cards must be in the same tableau pile.')
98 | else:
99 | pile = cards[0].game_location
100 | indexes = [pile.index(card) for card in cards]
101 | if abs(indexes[0] - indexes[1]) == 1:
102 | # Swap (spin) the two cards.
103 | pile[indexes[0]], pile[indexes[1]] = pile[indexes[1]], pile[indexes[0]]
104 | break
105 | else:
106 | self.human.tell('Those cards are not next to each other.')
107 | go = False
108 | # Otherwise I'm confused.
109 | else:
110 | self.human.tell('That does not compute.')
111 | return go
112 |
113 | def set_checkers(self):
114 | """Set up the game specific rules. (None)"""
115 | super(Strategy, self).set_checkers()
116 | # Set the rule checkers.
117 | self.build_checkers = [solitaire.build_reserve]
118 | self.lane_checkers = [solitaire.lane_reserve]
119 | self.pair_checkers = []
120 | self.sort_checkers = [solitaire.sort_ace, solitaire.sort_up, solitaire.sort_no_reserve]
121 | # Set the dealer.
122 | self.dealers = [solitaire.deal_reserve_n(52)]
123 |
124 | def set_options(self):
125 | """Set the game options. (None)"""
126 | self.options = {'num-tableau': 8, 'num-reserve': 1}
127 | self.option_set.add_option('piles', ['p'], action = 'key=num-tableau', converter = int,
128 | default = 8, valid = range(1, 9), target = self.options,
129 | question = 'How many tableau piles (1-8, return for 8)? ')
130 | self.option_set.add_group('gonzo', ['gz'], 'piles = 6')
131 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/yukon_game.py:
--------------------------------------------------------------------------------
1 | """
2 | yukon_game.py
3 |
4 | A game of Yukon.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: Credits for Yukon. (str)
11 | OPTIONS: Options for Yukon. (str)
12 | RULES: Rules for Yukon. (str)
13 |
14 | Class:
15 | Yukon: A game of Yukon.
16 | """
17 |
18 |
19 | import random
20 |
21 | from . import solitaire_game as solitaire
22 |
23 |
24 | CREDITS = """
25 | Game Design: Traditional.
26 | Game Programming: Craig "Ichabod" O'Brien.
27 | """
28 |
29 | RULES = """
30 | The cards are dealt in a triangle of seven piles, the first pile having one
31 | card, and each pile to the right having one more card. Then the rest of the
32 | cards are deal face up, left to right, on all stacks but the first.
33 |
34 | Any stack of cards may be moved, as long as the bottom card in the stack is one
35 | rank below and a different color than the card it is being moved onto. Empty
36 | tableau piles may be filled with a king or any stack starting with a king.
37 | """
38 |
39 | OPTIONS = """
40 | gonzo (gz): Equivalent to 'piles = 8'.
41 | piles= (p=): How many tableau piles there should be.
42 | suits (s, russian): Cards must be matched by suit, not alternating color.
43 | """
44 |
45 |
46 | class Yukon(solitaire.Solitaire):
47 | """
48 | A game of Yukon. (Solitaire)
49 |
50 | Overridden Methods:
51 | set_checkers
52 | set_options
53 | """
54 |
55 | aka = []
56 | categories = ['Card Games', 'Solitaire Games', 'Building Games']
57 | credits = CREDITS
58 | name = 'Yukon'
59 | num_options = 2
60 | options = OPTIONS
61 | rules = RULES
62 |
63 | def do_gipf(self, arguments):
64 | """
65 | Klondike allows you to move the top three cards of any pile onto any open card.
66 |
67 | Slot Machines lets you randomly rotate the face up cards of any one tableau
68 | pile.
69 | """
70 | game, losses = self.gipf_check(arguments, ('klondike', 'slot machines'))
71 | # Klondike lets you move any three onto anything.
72 | if game == 'klondike':
73 | if not losses:
74 | player = self.players[self.player_index]
75 | player.tell(self)
76 | player.tell()
77 | # Get the mover
78 | while True:
79 | mover = player.ask('Pick a tableau card to move, with two cards on top of it: ')
80 | mover = self.deck.find(mover)
81 | location = mover.game_location
82 | if location in self.tableau and mover == location[-3] and mover.up:
83 | moving_stack = location[-3:]
84 | break
85 | error = 'The {} is not on the tableau with two cards on top of it.'
86 | player.error(error.format(mover.name))
87 | # Get the target.
88 | while True:
89 | target = player.ask('Pick a tableau card to move those three cards to: ')
90 | target = self.deck.find(target)
91 | location = target.game_location
92 | if location in self.tableau and target == location[-1]:
93 | break
94 | player.error('The {} is not on top of a tableau stack.'.format(target.name))
95 | # Make the move.
96 | self.transfer(moving_stack, location)
97 | # Slot Machines randomly rotates a pile.
98 | elif game == 'slot machines':
99 | if not losses:
100 | player = self.players[self.player_index]
101 | # Show the current state.
102 | player.tell(self)
103 | player.tell()
104 | # Get the pile to rotate.
105 | query = 'Pick a tableau pile (1-7, left to right) to rotate: '
106 | pile_index = player.ask_int(query, low = 1, high = 7) - 1
107 | pile = self.tableau[pile_index]
108 | # Get the size of the face up stack.
109 | for up_index, card in enumerate(pile):
110 | if card.up:
111 | break
112 | # Rotate the pile.
113 | rotation = random.randint(up_index + 1, len(pile) - 1)
114 | moving_stack = pile[up_index:rotation]
115 | undo_stack = pile[rotation:]
116 | self.transfer(moving_stack, pile)
117 | # Fix the undo.
118 | self.moves[-1] = (undo_stack, pile, pile, 0, False)
119 | # Otherwise I'm confused.
120 | else:
121 | self.human.tell("That is not one of the eleven words for snow.")
122 | return True
123 |
124 | def set_checkers(self):
125 | """Set up the game specific rules. (None)"""
126 | super(Yukon, self).set_checkers()
127 | # Set the game specific rules checkers.
128 | self.lane_checkers = [solitaire.lane_king]
129 | if self.suits:
130 | self.build_checkers = [solitaire.build_down_one, solitaire.build_suit_one]
131 | else:
132 | self.build_checkers = [solitaire.build_down_one, solitaire.build_alt_color_one]
133 | self.sort_checkers = [solitaire.sort_ace, solitaire.sort_up]
134 | # Set the dealers
135 | self.dealers = [solitaire.deal_klondike, solitaire.deal_yukon]
136 |
137 | def set_options(self):
138 | """Define the options for the game. (None)"""
139 | self.options = {}
140 | # Set the deal options.
141 | self.option_set.add_option('piles', ['p'], action = 'key=num-tableau', target = self.options,
142 | default = 7, converter = int, question = 'How many tableau piles should their be? ')
143 | self.option_set.add_option('suits', ['s', 'russian'],
144 | question = 'Should building be done by suits instead of alternating colors? bool')
145 | # Set the option groups.
146 | self.option_set.add_group('gonzo', ['gz'], 'piles = 8')
147 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/monte_carlo_game.py:
--------------------------------------------------------------------------------
1 | """
2 | monte_carlo_game.py
3 |
4 | A game of Monte Carlo.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: The credits for Monte Carlo. (str)
11 | OPTIONS: The options for Monte Carlo. (str)
12 | RULES: The rules for Monte Carlo. (str)
13 |
14 | Classes:
15 | MonteCarlo: A game of Monte Carlo. (solitaire.Solitaire)
16 | """
17 |
18 |
19 | import random
20 |
21 | from . import solitaire_game as solitaire
22 |
23 |
24 | # The credits for Monte Carlo.
25 | CREDITS = """
26 | Game Design: Traditional
27 | Game Programming: Craig "Ichabod" O'Brien
28 | """
29 |
30 | # The rules for Monte Carlo.
31 | RULES = """
32 | The tableau is a layout of five cards by five cards. Any pair of the same rank
33 | that is adjacent orthogonally or diagonally may be removed to the single
34 | foundation pile. At any time (using the turn command), you can consolidate
35 | cards to the right and up, so that all empty spots are on the bottom right.
36 | Then cards will be added from the stock to fill in the blanks.
37 |
38 | Use the match command to pair two cards and sort them to the foundation.
39 | """
40 |
41 | OPTIONS = """
42 | gonzo (gz): Equivalent to rows=3.
43 | thirteen (13): Pairs adding to thirteen can be matched, kings can be sorted to the
44 | foundation.
45 | rows= (r=): The number of rows dealt (defaults to 5).
46 | """
47 |
48 |
49 | class MonteCarlo(solitaire.Solitaire):
50 | """
51 | A game of Monte Carlo. (solitaire.Solitaire)
52 |
53 | Attributes:
54 | thirteen: A flag for matching pairs that add to thirteen. (bool)
55 |
56 | Overridden Methods:
57 | do_match
58 | do_turn
59 | find_foundation
60 | set_checkers
61 | set_options
62 | tableau_text
63 | """
64 |
65 | aka = ['Weddings', 'MoCa']
66 | categories = ['Card Games', 'Solitaire Games', 'Matching Games']
67 | credits = CREDITS
68 | name = 'Monte Carlo'
69 | num_options = 2
70 | options = OPTIONS
71 | rules = RULES
72 |
73 | def do_gipf(self, arguments):
74 | """
75 | Quadrille allows you to match non-adjacent cards.
76 |
77 | Craps randomizes the tableau.
78 | """
79 | # Run the edge, if possible.
80 | game, losses = self.gipf_check(arguments, ('quadrille', 'craps'))
81 | # Winning Quadrille allows you to match non-adjacent cards.
82 | if game == 'quadrille':
83 | if not losses:
84 | self.human.tell('\nYour next match does not have to be adjacent.')
85 | del self.match_checkers[1]
86 | # Wunning craps shuffles the tableau.
87 | elif game == 'craps':
88 | if not losses:
89 | random.shuffle(self.tableau)
90 | # Otherwise I'm confused.
91 | else:
92 | self.human.tell('But reality is just a simulation, so does gipfing really matter?')
93 | return True
94 |
95 | def do_match(self, cards):
96 | """
97 | Match two cards and discard them. (m)
98 |
99 | The two cards specified by the arguments can be listed in any order.
100 | """
101 | # Unset non-adjacent matching on successful match.
102 | go = super(MonteCarlo, self).do_match(cards)
103 | if not go and len(self.match_checkers) == 2:
104 | self.match_checkers.append(solitaire.match_adjacent)
105 | return go
106 |
107 | def do_turn(self, arguments):
108 | """
109 | Refill the tableau from the stock. (t)
110 |
111 | In Monte Carlo, the turn command first shifts all cards to the right,
112 | and up to the next level if there is space. Then any empty spots are
113 | filled from the stock.
114 | """
115 | # Shift everything over.
116 | undo_index = 0
117 | empty_indexes = []
118 | for pile_index, pile in enumerate(self.tableau):
119 | # Move full piles to empty piles, if you have any.
120 | if pile and empty_indexes:
121 | self.transfer(pile[:], self.tableau[empty_indexes.pop(0)], undo_ndx = undo_index)
122 | undo_index += 1
123 | # Note that the current pile is now empty.
124 | empty_indexes.append(pile_index)
125 | # Note empty piles for later filling.
126 | elif not pile:
127 | empty_indexes.append(pile_index)
128 | # Fill any remaining empty piles from the stock.
129 | for pile_index in empty_indexes:
130 | if not self.stock:
131 | break
132 | self.transfer(self.stock[-1:], self.tableau[pile_index], undo_ndx = undo_index)
133 | undo_index += 1
134 |
135 | def find_foundation(self, card):
136 | """
137 | Find the foundation a card can be sorted to. (list of TrackingCard)
138 |
139 | Parameters:
140 | card: The card to sort. (str)
141 | """
142 | return self.foundations[0]
143 |
144 | def set_checkers(self):
145 | """Set up the game specific rules. (None)"""
146 | # Set the default checkers.
147 | super(MonteCarlo, self).set_checkers()
148 | # Set the dealers.
149 | self.dealers = [solitaire.deal_n(self.options['num-tableau']), solitaire.deal_stock_all]
150 | # Set the rules checkers.
151 | self.build_checkers = [solitaire.build_none]
152 | self.lane_checkers = [solitaire.lane_none]
153 | self.match_checkers = [solitaire.match_tableau, solitaire.match_adjacent, solitaire.match_pairs]
154 | # Account for the thirteen option.
155 | if self.thirteen:
156 | self.match_checkers[-1] = solitaire.match_thirteen
157 | self.sort_checkers = [solitaire.sort_kings_only]
158 | else:
159 | self.sort_checkers = [solitaire.sort_none]
160 |
161 | def set_options(self):
162 | """Set the options for the game. (None)"""
163 | self.options = {'num-foundations': 1}
164 | self.option_set.add_option('thirteen', ['13'], question = 'Do you want to match sums of 13? bool')
165 | self.option_set.add_option('rows', ['r'], action = "key=num-tableau",
166 | converter = lambda x: int(x) * 5, default = 25, valid = (15, 20, 25, 30), target = self.options,
167 | question = 'How many rows should be dealt (4-6, return for 5)? ')
168 | self.option_set.add_group('gonzo', ['gz'], 'rows=3')
169 |
170 | def tableau_text(self):
171 | """Generate the text representation of the tableau piles. (str)"""
172 | lines = []
173 | for pile_index, pile in enumerate(self.tableau):
174 | # Add a new line every five piles.
175 | if not pile_index % 5:
176 | lines.append('')
177 | # Show the card or a blank spot for each pile.
178 | if pile:
179 | lines[-1] += str(pile[0]) + ' '
180 | else:
181 | lines[-1] += ' '
182 | return '\n'.join(lines)
183 |
--------------------------------------------------------------------------------
/other_cmd.py:
--------------------------------------------------------------------------------
1 | """
2 | other_cmd.py
3 |
4 | Basically, a cmd-style command processor without a cmdloop. This is just for
5 | side handling of other commands during another text input handling loop.
6 |
7 | Note that the return values for command handling methods are different in
8 | OtherCmd compared to Cmd. To match game.Game processing, True means keep
9 | processing without moving to the next turn.
10 |
11 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
12 | See the top level __init__.py file for details on the t_games license.
13 |
14 | Classes:
15 | OtherCmd: An object for handing text commands. (object)
16 | """
17 |
18 |
19 | import textwrap
20 | import traceback
21 |
22 |
23 | class OtherCmd(object):
24 | """
25 | An object for handing text commands. (object)
26 |
27 | Class Attributes:
28 | aliases: Other names for commands. (dict of str: str)
29 | help_text: Text for other help topics. (dict of str: str)
30 |
31 | Attributes:
32 | human: The user of the interface. (player.Player)
33 |
34 | Methods:
35 | default: Handle unrecognized commands. (bool)
36 | do_debug: Evaluate Python code. (bool)
37 | do_help: Process help requests. (bool)
38 | do_set: Set shortcuts. (bool)
39 | handle_cmd: Check text input for a valid command. (bool)
40 |
41 | Overridden Methods:
42 | __init__
43 | __repr__
44 | """
45 |
46 | aliases = {'&': 'debug', '?': 'help'}
47 | help_text = {'help': '\nResistance is futile.'}
48 |
49 | def __init__(self, human):
50 | """
51 | Set up the user. (None)
52 |
53 | Parameters:
54 | human: The (ostensibly) human user of the interface. (player.Player)
55 | """
56 | self.human = human
57 |
58 | def __repr__(self):
59 | """Generate a debugging text representation. (str)"""
60 | return '<{} for {!r}>'.format(self.__class__.__name__, self.human)
61 |
62 | def default(self, text):
63 | """
64 | Handle unrecognized commands. (bool)
65 |
66 | Parameters:
67 | text: The raw text input by the user. (str)
68 | """
69 | self.human.error('\nI do not recognize the command {!r}.'.format(text))
70 |
71 | def do_debug(self, arguments):
72 | """
73 | I can't help you with that.
74 | """
75 | try:
76 | # Run the code.
77 | result = eval(arguments)
78 | except (Exception, AttributeError, ImportError, NameError, TypeError, ValueError):
79 | # Catch most exceptions and inform the user.
80 | self.human.error('\nThere was an exception raised while processing that command:')
81 | self.human.error(traceback.format_exc(), end = '')
82 | else:
83 | # Show the results of valid code.
84 | self.human.tell(repr(result))
85 | self.human.tell()
86 | return True
87 |
88 | def do_help(self, arguments):
89 | """
90 | Handle help requests. (bool)
91 |
92 | Parameters:
93 | arguments: What to provide help for. (str)
94 | """
95 | topic = arguments.lower()
96 | # check for aliases
97 | topic = self.aliases.get(topic, topic)
98 | # The help_text dictionary takes priority.
99 | if topic in self.help_text:
100 | self.human.tell(self.help_text[topic].rstrip())
101 | # General help is given with no arguments.
102 | elif not topic:
103 | # Show the base help text.
104 | self.human.tell(self.help_text['help'].rstrip())
105 | # Get the names of other help topics.
106 | names = [name[3:] for name in dir(self.__class__) if name.startswith('do_')]
107 | names.extend([name[5:] for name in dir(self.__class__) if name.startswith('help_')])
108 | names.extend(self.help_text.keys())
109 | # Clean up the names.
110 | names = list(set(names) - set(('debug', 'help', 'text')))
111 | names.sort()
112 | # Convert the names to cleanly wrapped text and output.
113 | name_lines = textwrap.wrap(', '.join(names), width = 79)
114 | if name_lines:
115 | self.human.tell()
116 | self.human.tell("Additional help topics available with 'help ':")
117 | self.human.tell('\n'.join(name_lines))
118 | # help_foo methods take priority over do_foo docstrings.
119 | elif hasattr(self, 'help_' + topic):
120 | help_method = getattr(self, 'help_' + topic)
121 | # Exit without pausing if requested by the help_foo method.
122 | if help_method():
123 | return True
124 | # Method docstrings are given for recognized commands.
125 | elif hasattr(self, 'do_' + topic):
126 | help_text = getattr(self, 'do_' + topic).__doc__
127 | help_text = textwrap.dedent(help_text).rstrip()
128 | self.human.tell(help_text)
129 | # Display default text for unknown arguments.
130 | else:
131 | self.human.tell("\nI can't help you with that.")
132 | # Don't let the next menu interfere with reading the help text.
133 | self.human.ask('\nPress Enter to continue: ')
134 | return True
135 |
136 | def do_set(self, arguments):
137 | """
138 | Set a shortcut.
139 |
140 | The first word provided as an argument to the set command will be the
141 | shortcut. The rest of text of the argument will be what the shortcut is
142 | expanded into. Any time that shortcut is used as a command, it is replaced
143 | with the expanded text.
144 |
145 | For example, if you prefer playing freecell with three cells, you could type
146 | 'set fc play freecell / cells = 3'. Then any time you typed 'fc', it would
147 | play the game freecell with the option 'cells = 3'. You could even type 'fc
148 | challenge' to play with three cells and the challenge option.
149 | """
150 | shortcut, space, text = arguments.strip().partition(' ')
151 | if shortcut and text:
152 | self.human.store_shortcut(shortcut, text)
153 | elif shortcut:
154 | self.human.error('\nNo expansion text was provided, the shortcut was not set.')
155 | else:
156 | self.human.error('\nNo shortcut was provided, nothing was set.')
157 |
158 | def handle_cmd(self, text):
159 | """
160 | Check text input for a valid command. (bool)
161 |
162 | The return value is a flag indicating a valid command.
163 |
164 | Parameters:
165 | text: The raw text input by the user. (str)
166 | """
167 | # Parse the input into a command and arguments.
168 | command, space, arguments = text.strip().partition(' ')
169 | command = command.lower()
170 | command = self.aliases.get(command, command)
171 | # Check for a method to handle the command.
172 | method = 'do_' + command
173 | if hasattr(self, method):
174 | return getattr(self, method)(arguments.strip())
175 | else:
176 | # Use default if there is no available method.
177 | return self.default(text)
178 |
179 |
180 | if __name__ == '__main__':
181 | # Run the unit testing.
182 | from t_tests.other_cmd_test import *
183 | unittest.main()
184 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/spider_game.py:
--------------------------------------------------------------------------------
1 | """
2 | spider_game.py
3 |
4 | A game of Spider.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: The credits for Spider. (str)
11 | RULES: The rules of Spider. (str)
12 |
13 | Classes:
14 | Spider: A game of Spider. (solitaire.MultiSolitaire)
15 | """
16 |
17 |
18 | from ... import cards
19 | from . import solitaire_game as solitaire
20 |
21 |
22 | # # The credits for Spider.
23 | CREDITS = """
24 | Game Design: Traditional
25 | Game Programming: Craig "Ichabod" O'Brien
26 | """
27 |
28 | # The rules of Spider.
29 | RULES = """
30 | Spider is two deck game, with ten tableau piles. Four of the tableau piles
31 | start with six cards, the rest start with five cards.
32 |
33 | Cards on the tableau can be built down in rank regardless of suit. However,
34 | only stacks of a single suit can be moved as a unit. Otherwise cards must be
35 | built one at a time.
36 |
37 | If you ever build a stack that goes from king to ace in the same suit, the
38 | whole stack will automatically be sorted.
39 |
40 | Turning cards over from the stock deals one face up card to the top of each
41 | tableau pile. You may not turn over cards from the stock if you have any empty
42 | tableau piles.
43 | """
44 |
45 | OPTIONS = """
46 | gonzo (gz): Equivalent to 'open relaxed-turn'.
47 | one-suit (1s): The deck is all one suit (spades).
48 | open (o): All tableau cards are dealt face up.
49 | relaxed-turn (relaxed, rt): You may turn over cards from the deck when you
50 | have empty tableau piles.
51 | two-suit (2s): The deck has only two suits: hearts and spades.
52 | """
53 |
54 |
55 | class Spider(solitaire.MultiSolitaire):
56 | """
57 | A game of Spider. (solitaire.MultiSolitaire)
58 |
59 | Attributes:
60 | open: A flag for the tableau being totally face up. (bool)
61 | relaxed_turn: A flag for being able to turn with empty lanes. (bool)
62 |
63 | Methods:
64 | auto_sort_check: Check if the stack just made is sortable. (None)
65 |
66 | Overridden Methods:
67 | do_alternate
68 | do_build
69 | do_turn
70 | set_checkers
71 | set_options
72 | """
73 |
74 | aka = ['Spid']
75 | categories = ['Card Games', 'Solitaire Games', 'Building Games']
76 | credits = CREDITS
77 | name = 'Spider'
78 | num_options = 4
79 | options = OPTIONS
80 | rules = RULES
81 |
82 | def auto_sort_check(self):
83 | """Check if the stack just made is sortable. (None)"""
84 | # If there are thirteen cards in the new location
85 | moving_stack, old_location, new_location, undo_index, turn = self.moves[-1]
86 | stack = new_location[-13:]
87 | if len(stack) == 13:
88 | # Check those thirteen for validity.
89 | for checker in self.lane_checkers:
90 | if checker(self, stack[0], stack):
91 | break
92 | else:
93 | # Sort any valid stacks as a whole.
94 | foundations = self.find_foundation(stack[0])
95 | foundation = [foundation for foundation in foundations if not foundation][0]
96 | self.transfer(stack, foundation, undo_ndx = 1)
97 |
98 | def do_alternate(self, arguments):
99 | """
100 | Redo the last command with different but matching cards. (alt)
101 |
102 | This is for when there are two cards of the same rank and suit that
103 | can make the same move, and the game makes the wrong one.
104 | """
105 | # Do the building
106 | go = super(Spider, self).do_alternate(arguments)
107 | # If there was building, check for a sortable stack.
108 | if not go:
109 | self.auto_sort_check()
110 | return go
111 |
112 | def do_build(self, arguments):
113 | """
114 | Build card(s) into stacks on the tableau. (b)
115 |
116 | Two cards must be given to this command: the card to move and the card to
117 | build it onto. If you are moving a stack of cards, specify the bottom card of
118 | the stack as the card to move.
119 | """
120 | # Do the building
121 | go = super(Spider, self).do_build(arguments)
122 | # If there was building, check for a sortable stack.
123 | if not go:
124 | self.auto_sort_check()
125 | # Reset changes to the rule checkers.
126 | self.pair_checkers = [solitaire.pair_down]
127 | if len(self.build_checkers) == 1:
128 | self.build_checkers.append(solitaire.build_suit)
129 | return go
130 |
131 | def do_gipf(self, arguments):
132 | """
133 | Bisley allows your next build to be up or down one rank.
134 |
135 | Freecell lets you move a stack regardless of suit.
136 | """
137 | game, losses = self.gipf_check(arguments, ('bisley', 'freecell'))
138 | # Winning Bisley gets you an up or down build.
139 | if game == 'bisley':
140 | if not losses:
141 | self.human.tell('\nYour next build may be up or down one rank.')
142 | self.pair_checkers = [solitaire.pair_up_down]
143 | # Winning Freecell lets you move a stack ignoring suit.
144 | elif game == 'freecell':
145 | if not losses:
146 | self.human.tell('\nYour next build may move a stack regardless of suit.')
147 | self.build_checkers = [solitaire.build_down]
148 | # Otherwise I'm confused.
149 | else:
150 | self.human.tell('Only the spider crawls the web.')
151 | return True
152 |
153 | def do_turn(self, arguments):
154 | """
155 | Turn over cards from the stock. (t)
156 |
157 | In Spider there is no waste pile. Cards from the stock are dealt on top of the
158 | tableau piles, one for each pile.
159 | """
160 | # Check for no stock.
161 | if not self.stock:
162 | self.human.error('There are no more cards to turn over.')
163 | # Check for empty piles (or relaxed-turn option) !! This should be a rule checker.
164 | elif not all(self.tableau) and not self.relaxed_turn:
165 | self.human.error('You cannot turn over cards from the stock if you have empty tableau piles.')
166 | else:
167 | # Deal the cards to the tableau.
168 | for pile_index, pile in enumerate(self.tableau):
169 | self.transfer([self.stock[-1]], pile, up = True, undo_ndx = pile_index)
170 | if not self.stock:
171 | break
172 |
173 | def set_checkers(self):
174 | """Set the game specific rule checkers. (None)"""
175 | super(Spider, self).set_checkers()
176 | # Set the dealers.
177 | self.dealers = [solitaire.deal_n(54, up = self.open), solitaire.deal_stock_all]
178 | # Set the rule checkers.
179 | self.build_checkers = [solitaire.build_suit, solitaire.build_down]
180 | self.lane_checkers = [solitaire.lane_suit, solitaire.lane_down]
181 | self.pair_checkers = [solitaire.pair_down]
182 | self.sort_checkers = [solitaire.sort_none]
183 |
184 | def set_options(self):
185 | """Set up the game specific options. (None)"""
186 | # Set the base solitaire options.
187 | self.options = {'num-foundations': 8, 'num-tableau': 10}
188 | # Set the deal options.
189 | self.option_set.add_option('one-suit', ['1s'], action = 'key=deck-specs', target = self.options,
190 | value = (0, 8, cards.STANDARD_RANKS, cards.ONE_SUIT), default = None,
191 | question = 'Should the deck only have one suit? bool')
192 | self.option_set.add_option('two-suit', ['2s'], action = 'key=deck-specs', target = self.options,
193 | value = (0, 4, cards.STANDARD_RANKS, cards.TWO_SUITS), default = None,
194 | question = 'Should the deck only have two suits? bool')
195 | self.option_set.add_option('open', ['o'], question = 'Should the tableau be dealt face up? bool')
196 | # Set the play options.
197 | self.option_set.add_option('relaxed-turn', ['relaxed', 'rt'],
198 | question = 'Should you be able to turn over cards with empty lanes? bool')
199 | # Set the option groups.
200 | self.option_set.add_group('gonzo', ['gz'], 'open relaxed-turn')
201 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/bisley_game.py:
--------------------------------------------------------------------------------
1 | """
2 | bisley_game.py
3 |
4 | A game of Bisley.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: The credits for Bisley. (str)
11 | RULES: The rules for Bisley. (str)
12 |
13 | Classes:
14 | Bisley: A game of Bisley. (solitaire.Solitaire)
15 | """
16 |
17 |
18 | import random
19 |
20 | from . import solitaire_game as solitaire
21 |
22 |
23 | CREDITS = """
24 | Game Design: Traditional
25 | Game Programming: Craig "Ichabod" O'Brien
26 | """
27 |
28 | OPTIONS = """
29 | gonzo (gz): equivalent to reserved.
30 | reserved (r): One pile is used as a reserve pile.
31 | """
32 |
33 | RULES = """
34 | Four aces are dealt as four of the eight foundations. Thirteen columns of
35 | cards are dealt as the tableau: four columns of three cards under the ace
36 | foundations, and nine columns of four card to the right of the ace
37 | foundations.
38 |
39 | Cards may be built on the tableau one at time by suit, in either ascending or
40 | descending rank. Kings may be sorted to foundations above the ace foundations.
41 | Cards may be sorted down in suit from the kings, or up in suit from the aces.
42 | Empty foundation columns may not be filled with any card.
43 | """
44 |
45 |
46 | class Bisley(solitaire.Solitaire):
47 | """
48 | A game of Bisley. (solitaire.Solitaire)
49 |
50 | Overridden Methods:
51 | __str__
52 | do_lane
53 | find_foundation
54 | foundation_text
55 | set_checkers
56 | set_options
57 | tableau_text
58 | """
59 |
60 | aka = ['Bisl']
61 | categories = ['Card Games', 'Solitaire Games', 'Open Games']
62 | credits = CREDITS
63 | name = 'Bisley'
64 | num_options = 1
65 | options = OPTIONS
66 | rules = RULES
67 |
68 | def __str__(self):
69 | """Human readable text representation. (str)"""
70 | # Mix the foundation text in with the tableau text.
71 | text = '{}\n\n'.format(self.cell_text()) if self.num_cells else ''
72 | text = '\n{}{}{}'.format(text, self.foundation_text(), self.tableau_text())
73 | reserve_text = '\n\n{}'.format(self.reserve_text()) if self.reserve else ''
74 | waste_text = '\n\n{}'.format(self.stock_text()) if (self.stock or self.waste) else ''
75 | return '{}{}{}'.format(text, reserve_text, waste_text)
76 |
77 | def do_gipf(self, arguments):
78 | """
79 | Liar's Dice shuffles one tableau pile.
80 |
81 | Strategy lets you move one stack into an empty lane.
82 | """
83 | # Run the edge, if possible.
84 | game, losses = self.gipf_check(arguments, ("liar's dice", 'strategy'))
85 | # Winning Liar's Dice randomly shuffles one tableau pile.
86 | if game == "liar's dice":
87 | if not losses:
88 | self.human.tell(self)
89 | while True:
90 | card_text = self.human.ask('Pick a card on the tableau: ')
91 | if self.deck.card_re.match(card_text):
92 | card = self.deck.find(card_text)
93 | if card.game_location in self.tableau:
94 | break
95 | else:
96 | self.human.error('That card is not in the tableau.')
97 | else:
98 | self.human.error('I do not recognize that card.')
99 | random.shuffle(card.game_location)
100 | # Winning Strategy lets you lane one stack.
101 | elif game == 'strategy':
102 | if not losses:
103 | self.human.tell('\nYou may lane any one stack.')
104 | self.lane_checkers = []
105 | # Otherwise I'm confused.
106 | else:
107 | self.human.tell('Non-sequitur, one-love.')
108 | return True
109 |
110 | def do_lane(self, card):
111 | """
112 | Move a card into an empty lane. (l)
113 |
114 | This command takes one argument: the card to move.
115 | """
116 | # Lane the card.
117 | go = super(Bisley, self).do_lane(card)
118 | # Reset the lane checkers.
119 | if not go and not self.lane_checkers:
120 | self.lane_checkers = [solitaire.lane_none]
121 | return go
122 |
123 | def find_foundation(self, card):
124 | """
125 | Determine which foundation a card should sort to. (list of TrackingCard)
126 |
127 | Parameters:
128 | card: The card to sort to a foundation. (cards.TrackingCard)
129 | """
130 | # Start with the king foundation.
131 | sort_index = self.deck.suits.index(card.suit)
132 | possible = self.foundations[sort_index + 4]
133 | # Switch to the ace foundation if possible.
134 | if (possible and card.above(possible[-1])) or card.rank == 'A':
135 | sort_index += 4
136 | return self.foundations[sort_index]
137 |
138 | def foundation_text(self):
139 | """Generate the text representation of the foundations."""
140 | # Put the foundations in two rows, kings over aces.
141 | words = []
142 | for index, foundation in enumerate(self.foundations):
143 | # Get the text for the foundation card (or not).
144 | if foundation:
145 | words.append(str(foundation[-1]))
146 | else:
147 | words.append('--')
148 | # Get the text between the foundation cards.
149 | if index == 3:
150 | words.append('\n')
151 | else:
152 | words.append(' ')
153 | return ''.join(words)
154 |
155 | def handle_options(self):
156 | """Handle the options for the game. (None)"""
157 | super(Bisley, self).handle_options()
158 | if self.reserved:
159 | self.options['num-reserve'] = 1
160 | self.options['num-tableau'] = 12
161 |
162 | def set_checkers(self):
163 | """Set the game specific rules. (None)"""
164 | super(Bisley, self).set_checkers()
165 | # Set up the dealers.
166 | if self.reserved:
167 | self.dealers = [solitaire.deal_aces_up, solitaire.deal_reserve_n(4), solitaire.deal_bisley]
168 | else:
169 | self.dealers = [solitaire.deal_aces_up, solitaire.deal_bisley]
170 | # Set up the rule checkers.
171 | self.build_checkers = [solitaire.build_one]
172 | self.lane_checkers = [solitaire.lane_none]
173 | self.pair_checkers = [solitaire.pair_suit, solitaire.pair_up_down]
174 | self.sort_checkers = [solitaire.sort_up_down, solitaire.sort_kings]
175 |
176 | def set_options(self):
177 | """Set up the possible options for the game. (None)"""
178 | self.options = {'num-foundations': 8, 'num-tableau': 13}
179 | self.option_set.add_group('gonzo', ['gz'], 'reserved')
180 | self.option_set.add_option('reserved', ['r'],
181 | question = 'Should one tableau pile be made into a reserve pile? bool')
182 |
183 | def tableau_text(self):
184 | """Generate the text representation of the foundations."""
185 | # Get the tallest row, account for the ace foundations.
186 | row_heights = [len(pile) for pile in self.tableau]
187 | for pushed in range(4):
188 | row_heights[pushed] += 1
189 | row_max = max(row_heights)
190 | # Loop through the rows.
191 | rows = []
192 | for row_index in range(row_max):
193 | # Add a row and loop through the columns.
194 | rows.append([])
195 | for column_index in range(len(self.tableau)):
196 | # Shift the first four columns under the ace foundations
197 | if row_index == 0 and column_index < 4:
198 | continue
199 | if column_index < 4:
200 | card_index = row_index - 1
201 | else:
202 | card_index = row_index
203 | # Add a card or a blank spot to the row as neccessary.
204 | if card_index < len(self.tableau[column_index]):
205 | rows[-1].append(str(self.tableau[column_index][card_index]))
206 | elif not row_index or (row_index == 1 and column_index < 4):
207 | rows[-1].append('--')
208 | else:
209 | rows[-1].append(' ')
210 | # Return the text generated from the rows.
211 | return '\n'.join([' '.join(row) for row in rows])
212 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/freecell_game.py:
--------------------------------------------------------------------------------
1 | """
2 | freecell_game.py
3 |
4 | FreeCell and related games.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: Credits for FreeCell. (str)
11 | OPTIONS: Options for FreeCell. (str)
12 | RULES: Rules for FreeCell. (str)
13 | STACK_HELP: Help on how many cards can be moved at one time. (str)
14 |
15 | Classes:
16 | FreeCell: A game of FreeCell. (Solitaire)
17 | """
18 |
19 |
20 | import random
21 |
22 | from ... import utility
23 | from . import solitaire_game as solitaire
24 |
25 |
26 | CREDITS = """
27 | Game Design/Original Programming: Paul Alfille
28 | Python Implementation: Craig "Ichabod" O'Brien
29 | """
30 |
31 | RULES = """
32 | Cards on the tableau build down in rank and alternating in color. Cards are
33 | sorted to the foundation by suit in ascending rank order. Any card at the top
34 | of a tableau pile may be moved to one of the free cells. Empty tableau piles
35 | may be filled with any card from the top of another tableau pile or one of the
36 | free cells.
37 |
38 | Technically, cards may only be moved one at a time. However, the computer
39 | keeps track of how large a stack you could move one card at a time, and allows
40 | you to move a stack that size as one move. For example, if you have two free
41 | cells, you can move a stack of three cards one at a time: one each to a free
42 | cell, then third to the destination card, then the two cards back off the free
43 | cells. So if you have two empty free cells, the game lets you move three cards
44 | as one.
45 | """
46 |
47 | OPTIONS = """
48 | baker (b): Building is done by suit (Baker's Game).
49 | cells= (c=): The number of free cells available. 1-10, defaults to 4.
50 | challenge (ch): The twos then the aces are dealt on the bottom row.
51 | egnellahc (eg): The aces then the twos are dealt on the bottom row.
52 | fill-free (ff): The free cells are filled with the last four cards from the
53 | deck.
54 | gonzo (gz): Equivalent to 'cells = 2 fill-free piles = 10 supercell'
55 | kings-only (ko): Only kings can be used to fill free cells.
56 | piles= (p=): The number of tableau piles. 4-10, defaults to 8.
57 | supercell (sc): One random card in each pile is turned face down.
58 | """
59 |
60 | STACK_HELP = """
61 | The number of cards you can move at one time depends on the number of empty
62 | free cells and the number of empty lanes. The formula for how many you can
63 | move is (1 + C) * 2 ^ L, where C is the number of empty cells and L is the
64 | number of empty lanes*.
65 |
66 | For the mathphobic:
67 |
68 | Cells
69 | Lanes 0 1 2 3 4
70 | 0 1 2 3 4 5
71 | 1 2 4 6 8 10
72 | 2 4 8 12 16 20
73 | 3 8 16 24 32 40
74 |
75 | If you are moving the cards to a lane, don't count that lane.
76 |
77 | * The formula using the rpn command would be: 1 C + 2 L ^ *
78 | """
79 |
80 |
81 | class FreeCell(solitaire.Solitaire):
82 | """
83 | A game of FreeCell. (Solitaire)
84 |
85 | Attributes:
86 | baker: A flag for building being by suit. (bool)
87 | challenge: A flag for dealing the twos and aces first. (bool)
88 | egnellahc: A flag for dealing the aces and twos first. (bool)
89 | fill_free: A flag for filling the free cells with the last four cards. (bool)
90 | kings_only: A flag for only allowing kings in empty lanes. (bool)
91 | supercell: A flag for flipping tableau cards over randomly. (bool)
92 |
93 | Overridden Methods:
94 | set_checkers
95 | set_options
96 | """
97 |
98 | aka = ['Free']
99 | categories = ['Card Games', 'Solitaire Games', 'Open Games']
100 | credits = CREDITS
101 | help_text = {'moving-stacks': STACK_HELP}
102 | name = 'FreeCell'
103 | num_options = 8
104 | options = OPTIONS
105 | rules = RULES
106 |
107 | def do_gipf(self, arguments):
108 | """
109 | Hamurabi allows building a free cell card on any tableau pile.
110 | """
111 | game, losses = self.gipf_check(arguments, ('hamurabi',))
112 | # Hamurabi allows building a free cell card on any tableau pile.
113 | if game == 'hamurabi':
114 | if not losses:
115 | # Get the state of the game.
116 | self.human.tell(self)
117 | cell_check = self.cell_text()
118 | tableau_check = [str(stack[-1]) for stack in self.tableau if stack]
119 | # Relax the rules.
120 | pair_hold = self.pair_checkers
121 | self.pair_checkers = []
122 | # Get the cards to move.
123 | while True:
124 | cards_raw = self.human.ask('\nEnter a free cell card and a card to build it on: ')
125 | cards = cards_raw.upper().split()
126 | if cards[0] not in cell_check:
127 | self.human.error('You must build with a free cell card.')
128 | elif cards[1] not in tableau_check:
129 | self.human.error('You must build to the top of a tableau pile.')
130 | # Stop asking for cards when there's a valid move.
131 | elif not self.do_build(cards_raw):
132 | break
133 | # Reset the rules.
134 | self.pair_checkers = pair_hold
135 | # Otherwise I'm confused.
136 | else:
137 | self.human.tell('There are no valid moves for the gipf of spades.')
138 |
139 | def set_checkers(self):
140 | """Set up the game specific rules. (None)"""
141 | super(FreeCell, self).set_checkers()
142 | # Set the game specific rules checkers.
143 | self.build_checkers = [solitaire.build_one]
144 | self.lane_checkers = [solitaire.lane_one]
145 | if self.kings_only:
146 | self.lane_checkers.append(solitaire.lane_king)
147 | self.pair_checkers = [solitaire.pair_down, solitaire.pair_alt_color]
148 | if self.baker:
149 | self.pair_checkers[1] = solitaire.pair_suit
150 | self.sort_checkers = [solitaire.sort_ace, solitaire.sort_up]
151 | # Set the dealers
152 | max_cards = (52 - self.num_cells) if self.fill_free else 52
153 | if self.challenge:
154 | self.dealers = [solitaire.deal_twos, solitaire.deal_aces]
155 | self.dealers.append(solitaire.deal_n(max_cards - 8, up = True, start = 8))
156 | elif self.egnellahc:
157 | self.dealers = [solitaire.deal_aces, solitaire.deal_twos]
158 | self.dealers.append(solitaire.deal_n(max_cards - 8, up = True, start = 8))
159 | else:
160 | self.dealers = [solitaire.deal_n(max_cards)]
161 | if self.fill_free:
162 | self.dealers.append(solitaire.deal_free)
163 | if self.supercell:
164 | self.dealers.append(solitaire.deal_flip_random)
165 |
166 | def set_options(self):
167 | """Set the game options. (None)"""
168 | self.options = {}
169 | # Set the tableau dimensions.
170 | self.option_set.add_option('cells', ['c'], action = 'key=num-cells', converter = int,
171 | default = 4, valid = range(1, 15), target = self.options,
172 | question = 'How many free cells (1-10, return for 4)? ')
173 | self.option_set.add_option('piles', ['p'], action = 'key=num-tableau', converter = int,
174 | default = 8, valid = range(4, 14), target = self.options,
175 | question = 'How many tableau piles (4-10, return for 8)? ')
176 | # Set the deal options.
177 | self.option_set.add_option('challenge', ['ch'],
178 | question = 'Should the twos and aces be dealt first? bool')
179 | self.option_set.add_option('egnellahc', ['eg'],
180 | question = 'Should the aces and twos be dealt first? bool')
181 | self.option_set.add_option('supercell', ['sc'],
182 | question = 'Should random cards be flipped face down? bool')
183 | self.option_set.add_option('fill-free', ['ff'],
184 | question = 'Should the free cells be filled with the last four cards dealt? bool')
185 | # Set the play options.
186 | self.option_set.add_option('kings-only', ['ko'],
187 | question = 'Should the kings be the only card playable to empty lanes? bool')
188 | self.option_set.add_option('baker', ['b'],
189 | question = "Should tableau cards be built by suit (Baker's Game)? bool")
190 | # Set the option groups.
191 | self.option_set.add_group('gonzo', ['gz'], 'cells = 2 fill-free piles = 10 supercell')
192 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/forty_thieves_game.py:
--------------------------------------------------------------------------------
1 | """
2 | forty_thieves_game.py
3 |
4 | A game of Forty Thieves.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: Credits for Forty Thieves. (str)
11 | OPTIONS: Options for Forty Thieves. (str)
12 | RULES: Rules for Forty Thieves. (str)
13 |
14 | Classes:
15 | FortyThieves: A game of Forty Thieves. (solitaire.Solitaire)
16 | """
17 |
18 |
19 | from . import solitaire_game as solitaire
20 |
21 |
22 | CREDITS = """
23 | Game Design: Traditional
24 | Game Programming: Craig "Ichabod" O'Brien
25 | """
26 |
27 | RULES = """
28 | This is a two deck solitaire game. Ten columns of four cards each are dealt
29 | for the tableau. There are eight foundations to be built up, ace to king in
30 | suit. You may only move one card at a time. Building on the tableau is down
31 | in rank by suit. You may turn over one card from the stock at a time, and
32 | place it in a waste pile. The top card of the waste pile is available for
33 | building or sorting. You may only go through the stock once.
34 | """
35 |
36 | OPTIONS = """
37 | alt-color (streets, ac): The tableau is built down in rank by alternating
38 | color.
39 | columns= (c=): The number of tableau columns (stacks) dealt.
40 | down-rows= (dr=): The number of tabelau rows that are dealt face down.
41 | dress-parade (rank-and-file, dp, rf) Equivalent to 'alt-color down-rows=3
42 | move-seq'.
43 | emperor (deauville, em, dv): Equivalent to 'alt-color down-rows=3'.
44 | found-aces (fa): Start the game with the aces on the foundations.
45 | gonzo (gz): Equivalent to 'columns=8 down-rows=4 move-seq rows=5'.
46 | indian: Equivalent to 'down-rows=1 c=10 r=3 not-suit'.
47 | limited (ltd): Equivalent to 'c=12 r=3'.
48 | lucas: Equivalent to 'found-aces c=13 r=3'.
49 | maria: Equivalent to 'alt-color c=9 r=4'.
50 | move-seq (ms): Move any built sequence on the tableau.
51 | not-suit (ns): The tableau is built down in rank by anything but suit.
52 | number-ten (10): Equivalent to 'down-rows=2 c=10 r=4 alt-color move-seq'.
53 | rows (r): The number of tableau rows (cards per stack) dealt.
54 | """
55 |
56 |
57 | class FortyThieves(solitaire.MultiSolitaire):
58 | """
59 | A game of Forty Thieves. (solitaire.Solitaire)
60 |
61 | Attributes:
62 | down_rows: How many tableau rows should be face down. (int)
63 | found_aces: A flag for dealing the aces to the foundations. (bool)
64 | move_seq: A flag for being able to move any built stack on the tableau. (bool)
65 | not_suit: A flag for building by different suits. (bool)
66 | rows: How many tableau rows should be dealt. (int)
67 | streets: A flag for building by alternating colors. (bool)
68 |
69 | Overridden Methods:
70 | set_checkers
71 | set_options
72 | stock_text
73 | """
74 |
75 | aka = ['Big Forty', 'Le Cadran', 'Napoleon at St Helena', 'Roosevelt at San Juan', 'FoTh']
76 | categories = ['Card Games', 'Solitaire Games', 'Digging Games']
77 | credits = CREDITS
78 | name = 'Forty Thieves'
79 | num_options = 7
80 | options = OPTIONS
81 | rules = RULES
82 |
83 | def do_gipf(self, arguments):
84 | """
85 | Freecell lets you build the top waste card on any tableau pile.
86 | """
87 | game, losses = self.gipf_check(arguments, ('freecell',))
88 | # Freecell allows building the top waste card on any tableau pile.
89 | if game == 'freecell':
90 | if not losses:
91 | # Remind the human.
92 | self.human.tell(self)
93 | # Get the card to build.
94 | tableau_check = [stack[-1] for stack in self.tableau if stack]
95 | while True:
96 | cards_raw = self.human.ask('Enter a waste card and a card to build it on: ')
97 | cards = cards_raw.upper().split()
98 | if cards[0] not in self.waste:
99 | self.human.error('You must build with a face up waste card.')
100 | elif cards[1] not in tableau_check:
101 | self.human.error('You must build to the top of a tableau pile.')
102 | else:
103 | break
104 | # Make the move.
105 | waste_ndx = self.waste.index(cards[0])
106 | waste_card = self.waste[waste_ndx]
107 | tableau_stack = [stack for stack in self.tableau if stack[-1] == cards[1]][0]
108 | self.transfer([waste_card], tableau_stack)
109 | pass
110 | # Otherwise I'm confused.
111 | else:
112 | self.human.tell("I'm sorry, I quit gipfing for Lent.")
113 |
114 | def set_checkers(self):
115 | """Set up the game specific rules. (None)"""
116 | super(FortyThieves, self).set_checkers()
117 | # Set game specific rules.
118 | if not self.move_seq:
119 | self.build_checkers = [solitaire.build_one]
120 | self.lane_checkers = [solitaire.lane_one]
121 | self.pair_checkers = [solitaire.pair_down, solitaire.pair_suit]
122 | if self.streets:
123 | self.pair_checkers[-1] = solitaire.pair_alt_color
124 | elif self.not_suit:
125 | self.pair_checkers[-1] = solitaire.pair_not_suit
126 | self.sort_checkers = [solitaire.sort_ace, solitaire.sort_up]
127 | # Set the dealers.
128 | self.dealers = []
129 | if self.found_aces:
130 | self.dealers.append(solitaire.deal_aces_multi)
131 | # Deal down rows + 1, since deal_n deals the last row up.
132 | if self.down_rows:
133 | self.down_rows = min(self.down_rows + 1, self.rows)
134 | self.dealers.append(solitaire.deal_n(self.options['num-tableau'] * self.down_rows, False))
135 | # Figure the remaining up rows and deal them.
136 | up_rows = self.rows - self.down_rows
137 | if up_rows:
138 | self.dealers.append(solitaire.deal_n(self.options['num-tableau'] * (up_rows)))
139 | self.dealers.append(solitaire.deal_stock_all)
140 |
141 | def set_options(self):
142 | """Define the options for the game. (None)"""
143 | # Set the standard solitaire options.
144 | self.options = {'max-passes': 1, 'num-foundations': 8, 'num-tableau': 10, 'turn-count': 1}
145 | # Define the option groups.
146 | self.option_set.add_group('emperor', ['deauville', 'dv', 'em'], 'streets down-rows=3')
147 | self.option_set.add_group('dress-parade', ['dp', 'rf', 'rank-and-file'],
148 | 'streets down-rows=3 move-seq')
149 | self.option_set.add_group('gonzo', ['gz'], 'columns=8 down-rows=4 move-seq rows=5')
150 | self.option_set.add_group('lucas', 'found-aces c=13 r=3')
151 | self.option_set.add_group('maria', 'alt-color c=9 r=4')
152 | self.option_set.add_group('limited', ['ltd'], 'c=12 r=3')
153 | self.option_set.add_group('indian', 'down-rows=1 c=10 r=3 not-suit')
154 | self.option_set.add_group('number-ten', ['10'], 'down-rows=2 c=10 r=4 alt-color move-seq')
155 | # Define the build options.
156 | self.option_set.add_option('streets', ['alt-color', 'ac'],
157 | question = 'Should tableau building be down by alternating color (return for by suit)? bool')
158 | self.option_set.add_option('not-suit', ['ns'],
159 | question = 'Should tableau building be down by anything but suit? bool')
160 | query = 'Should you be able to move any stack on the tableau (return for one card at a time)? bool'
161 | self.option_set.add_option('move-seq', ['josephine', 'ms'], question = query)
162 | # Define the deal options.
163 | self.option_set.add_option('columns', ['c'], int, default = 10, action = 'key=num-tableau',
164 | target = self.options,
165 | question = 'How many tableau columns (stacks) should be dealt (return for 10)? ')
166 | self.option_set.add_option('rows', ['r'], int, default = 4,
167 | question = 'How many tableau rows should be dealt (return for 4)? ')
168 | self.option_set.add_option('down-rows', ['d'], int, default = 0,
169 | question = 'How many rows of the tableau should be dealt face down (return for none)? ')
170 | self.option_set.add_option('found-aces', ['fa'],
171 | question = 'Should the aces be dealt to start the foundations? bool')
172 |
173 | def stock_text(self):
174 | """Generate text for the stock and waste. (str)"""
175 | # Generate the stock text.
176 | if self.stock:
177 | stock_text = '?? '
178 | else:
179 | stock_text = '-- '
180 | # Generate the waste text.
181 | stock_text += ' '.join(str(card) for card in self.waste)
182 | return stock_text
183 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/quadrille_game.py:
--------------------------------------------------------------------------------
1 | """
2 | quadrille_game.py
3 |
4 | A game of Quadrille.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: The credits for Qualdrille. (str)
11 | OPTIONS: The options for Quadrille. (str)
12 | RULES: The rules for Quadrille. (str)
13 |
14 | Classes:
15 | Quadrille: A game of Quadrille. (solitaire.Solitaire)
16 | """
17 |
18 |
19 | import time
20 |
21 | from ... import cards
22 | from . import solitaire_game as solitaire
23 |
24 |
25 | # The credits for Quadrille.
26 | CREDITS = """
27 | Game Design: Traditional
28 | Game Programming: Craig "Ichabod" O'Brien
29 | """
30 |
31 | # The options for Quadrille.
32 | OPTIONS = """
33 | cells= (c=): The number of free cells available. One to four, defaults to zero.
34 | gonzo (gz): Equivalent to 'max-passes=2 cells=1'
35 | max-passes= (mp=): The number of times you can go through the stock. Defaults
36 | to 3, -1 counts as infinite.
37 | """
38 |
39 | # The rules for Quadrille.
40 | RULES = """
41 | The queens are layed out in the center. The fives and sixes are dealt around
42 | them as foundations. The fives build down to the kings, and the sixes build up
43 | to the jacks.
44 |
45 | You can flip cards from the stock one at a time into the waste, and sort the
46 | top card of the waste. You get three passes through the deck.
47 |
48 | You may use the command 'auto full' (or just 'auto f') to have the computer
49 | play the game for you. You can add a number from 0 to 10 to adjust the speed
50 | at which it plays.
51 | """
52 |
53 |
54 | class Quadrille(solitaire.Solitaire):
55 | """
56 | A game of Quadrille. (solitaire.Solitaire)
57 |
58 | Methods:
59 | full_auto: Automatically play the game for the user. (bool)
60 |
61 | Overridden Methods:
62 | __str__
63 | do_auto
64 | find_foundation
65 | set_checkers
66 | set_options
67 | """
68 |
69 | aka = ['Captive Queens', 'La Francaise', 'Partners', 'Quad']
70 | categories = ['Card Games', 'Solitaire Games', 'Revealing Games']
71 | credits = CREDITS
72 | name = 'Quadrille'
73 | num_options = 2
74 | options = OPTIONS
75 | rules = RULES
76 |
77 | def __str__(self):
78 | """Generate a human readable text representation. (str)"""
79 | piles = self.foundations
80 | lines = ['']
81 | if self.options['num-cells']:
82 | cards = [str(card) for card in self.cells]
83 | blanks = ['--' for blank in range(self.options['num-cells'] - len(self.cells))]
84 | lines.append(' '.join(cards + blanks))
85 | lines.append('')
86 | lines.append(' {}'.format(piles[2][-1]))
87 | lines.append(' {} {}'.format(piles[5][-1], piles[6][-1]))
88 | lines.append(' QH')
89 | lines.append('{} QD QS {}'.format(piles[1][-1], piles[3][-1]))
90 | lines.append(' QC')
91 | lines.append(' {} {}'.format(piles[4][-1], piles[7][-1]))
92 | lines.append(' {}'.format(piles[0][-1]))
93 | lines.extend(('', self.stock_text()))
94 | return '\n'.join(lines)
95 |
96 | def do_auto(self, arguments):
97 | """
98 | Automatically play cards into the foundations. (a)
99 |
100 | If full (or f) is passed as the first argument to the auto command (in
101 | Quadrille), the game is played for you by the computer. If a number from zero
102 | to ten is passed as a second argument, it controls the speed at which the
103 | computer plays from 0 (one move per second) to 10 (as fast as possible).
104 | """
105 | # Check for full auto.
106 | if arguments and arguments.lower().split()[0] in ('f', 'full'):
107 | self.full_auto(arguments)
108 | return False
109 | # Otherwise handle normally.
110 | else:
111 | return super(Quadrille, self).do_auto(arguments)
112 |
113 | def do_gipf(self, arguments):
114 | """
115 | Yacht gives you an extra pass through the deck.
116 |
117 | Hearts forces the next high and low hearts to the waste.
118 | """
119 | # Run the edge, if possible.
120 | game, losses = self.gipf_check(arguments, ('yacht', 'hearts'))
121 | # Winning Yacht gives you an extra pass through the deck.
122 | if game == 'yacht':
123 | if not losses:
124 | self.max_passes += 1
125 | self.human.tell('\nYou have gained an extra pass through the deck.')
126 | # Winning Hearts forces the next high and low hearts to the waste.
127 | elif game == 'hearts':
128 | if not losses:
129 | bottom_heart = self.foundations[2][-1]
130 | lower_heart = self.deck.find(self.deck.ranks[bottom_heart.rank_num - 1] + 'H')
131 | self.transfer([lower_heart], self.waste)
132 | top_heart = self.foundations[6][-1]
133 | higher_heart = self.deck.find(self.deck.ranks[top_heart.rank_num + 1] + 'H')
134 | self.transfer([higher_heart], self.waste)
135 | # Otherwise I'm confused.
136 | else:
137 | self.human.tell("I don't know that dance.")
138 | return True
139 |
140 | def find_foundation(self, card):
141 | """
142 | Determine which foudations a card could be sorted to. (list of list)
143 |
144 | Parameters:
145 | card: The card to find foundations for. (card.TrackingCard)
146 | """
147 | # Find the base foundation.
148 | foundation_index = self.deck.suits.index(card.suit)
149 | # Switch foundations for cards building up.
150 | if card.rank in '789TJ':
151 | foundation_index += 4
152 | return self.foundations[foundation_index]
153 |
154 | def full_auto(self, arguments):
155 | """
156 | Automatically play the game for the user. (bool)
157 |
158 | Parameters:
159 | arguments: The arguments to the sort command. (str)
160 | """
161 | # Strip out non-digits from the front of the argument.
162 | arguments = ''.join([char for char in arguments if char.isdigit()])
163 | # Convert the arguments to a 0-10 digit.
164 | if arguments:
165 | speed = 10 - min(10, max(0, int(arguments)))
166 | else:
167 | speed = 5
168 | # Make moves while there are cards to move.
169 | passes = self.stock_passes
170 | while self.stock or self.waste:
171 | # Get a card to sort.
172 | if not self.waste:
173 | self.do_turn('')
174 | else:
175 | # Check the card for sorting.
176 | foundation = self.find_foundation(self.waste[-1])
177 | if self.sort_check(self.waste[-1], foundation, False):
178 | self.do_sort(str(self.waste[-1]))
179 | # If you cant sort, get another card.
180 | elif self.stock:
181 | self.do_turn('')
182 | else:
183 | # Keep track of passes through the deck, and exit when limit reached.
184 | passes += 1
185 | if passes == self.options['max-passes']:
186 | break
187 | else:
188 | self.do_turn('')
189 | # Update tracking and pause.
190 | print(self)
191 | self.turns += 1
192 | time.sleep(0.1 * speed)
193 |
194 | def set_checkers(self):
195 | """Set the game specific rule checking functions. (None)"""
196 | super(Quadrille, self).set_checkers()
197 | # Set the rule checking functions.
198 | self.build_checkers = [solitaire.build_none]
199 | self.lane_checkers = [solitaire.lane_none]
200 | self.sort_checkers = [solitaire.sort_up_down]
201 | # Set the dealers.
202 | self.dealers = [solitaire.deal_queens_out, solitaire.deal_five_six, solitaire.deal_stock_all]
203 |
204 | def set_options(self):
205 | """Set the available game options."""
206 | self.options = {'num-foundations': 8, 'num-reserve': 4, 'turn-count': 1, 'max-passes': 3,
207 | 'deck-specs': (0, cards.STANDARD_WRAP_RANKS)}
208 | self.option_set.add_option('cells', ['c'], action = 'key=num-cells', converter = int,
209 | default = 0, valid = range(1, 5), target = self.options,
210 | question = 'How many free cells (1-4, return for 4)? ')
211 | self.option_set.add_option('max-passes', ['mp'], action = 'key=max-passes', converter = int,
212 | default = 3, valid = range(-1, 11), target = self.options,
213 | question = 'How many free cells (-1(infinite)-10, return for 3)? ')
214 | self.option_set.add_group('gonzo', ['gz'], 'cells=1 max-passes=2')
215 |
--------------------------------------------------------------------------------
/dice_games/solitaire_dice_game.py:
--------------------------------------------------------------------------------
1 | """
2 | solitaire_dice_game.py
3 |
4 | A game of solitaire dice.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: The credits for the game and the programming. (str)
11 | RULES: The rules of Solitaire Dice. (str)
12 | SUM_LEADS: Text used in displaying the scores. (list of str)
13 | SUM_VALUES: The scoring for each possible sum. (list of int)
14 |
15 | Classes:
16 | SolitaireDice: A game of Solitaire Dice. (game.Game)
17 | """
18 |
19 |
20 | import collections
21 | import re
22 |
23 | from .. import dice
24 | from .. import game
25 |
26 |
27 | CREDITS = """
28 | Game Design: Sid Sackson
29 | Game Programming: Craig O'Brien
30 | """
31 |
32 | RULES = """
33 | Each turn you roll five dice. You discard one die, and split the other four
34 | into two pairs. You sum the two pairs, and keep a record of how many times
35 | each total is rolled.
36 |
37 | You can only discard three different numbers. Once you discard three different
38 | numbers, you cannot discard any other numbers unless you didn't roll any of
39 | the three numbers you had already discarded. Once you have discarded any
40 | single number eight times, the game is over.
41 |
42 | If a total has been rolled zero OR five times, it scores nothing. If it has
43 | been rolled one to four times, it scores -200 points. If it has been rolled
44 | six to ten times, it scores the number of times it's been rolled over five
45 | times a value based on the total: 2 and 12 score 100, 3 and 11 score 70, 4
46 | and 10 score 60, 5 and 9 score 50, 6 and 8 score 40, and 7 scores 30. Rolling a
47 | total eleven or more times results in the same score as rolling it ten times.
48 |
49 | Any score under 0 is recorded as a loss, any non-negative score up to 500 is
50 | recorded as a draw, and any score over 500 is recorded as a win.
51 | """
52 |
53 | SUM_LEADS = ['Pts Sum Count', '--- --- -----', '(100) 2:', ' (70) 3:',
54 | ' (60) 4:', ' (50) 5:', ' (40) 6:', ' (30) 7:', ' (40) 8:',
55 | ' (50) 9:', ' (60) 10:', ' (70) 11:', '(100) 12:']
56 |
57 | SUM_VALUES = [0, 0, 100, 70, 60, 50, 40, 30, 40, 50, 60, 70, 100]
58 |
59 |
60 | class SolitaireDice(game.Game):
61 | """
62 | A game of Solitaire Dice. (game.Game)
63 |
64 | Attributes:
65 | dice: The dice that are rolled. (dice.Pool)
66 | discards: The numbers discarded and how many times. (dict of int: int)
67 | free_free: A flag for a 'free' free ride. (bool)
68 | message: A message to show the user in show_status. (str)
69 | mode: Where we are in the player's turn. (str)
70 | roll: The current roll. (list of int)
71 | totals: The number of times each total has been rolled. (list of int)
72 |
73 | Methods:
74 | discard_mode: Discard a die. (bool)
75 | roll_mode: Roll the dice. (bool)
76 | show_status: Show the current game state. (None)
77 | split_mode: Choose a pair of dice. (bool)
78 | update_score: Update the game score. (None)
79 |
80 | Overridden Methods:
81 | game_over
82 | player_action
83 | set_up
84 | """
85 |
86 | aka = ['SoDi']
87 | categories = ['Dice Games']
88 | credits = CREDITS
89 | name = 'Solitaire Dice'
90 | rules = RULES
91 |
92 | def discard_mode(self, player):
93 | """
94 | Discard a die. (bool)
95 |
96 | Parameters:
97 | player: The player whose turn it is. (Player)
98 | """
99 | # Determine what can be discarded.
100 | if len(self.discards) == 3:
101 | allowed_discards = [d for d in self.discards if d in self.dice]
102 | # Check for a free ride.
103 | if not allowed_discards or self.free_free:
104 | player.tell('Free ride! You may discard any die you want.')
105 | allowed_discards = self.dice
106 | self.free_free = False
107 | else:
108 | allowed_discards = self.dice
109 | # Get the required/requested discard.
110 | if len(allowed_discards) == 1:
111 | discard = allowed_discards.pop()
112 | self.message += '\nYou must discard a {}.'.format(discard)
113 | else:
114 | discard = player.ask_int('Which number would you like to discard? ', valid = allowed_discards)
115 | if isinstance(discard, int):
116 | # Process valid discards (don't store free rides).
117 | if len(self.discards) < 3 or discard in self.discards:
118 | self.discards[discard] += 1
119 | self.dice.hold(discard)
120 | self.mode = 'split'
121 | else:
122 | return self.handle_cmd(discard)
123 |
124 | def do_gipf(self, arguments):
125 | """
126 | Freecell gives you a free ride.
127 |
128 | Gargantua lets you change one die into a six.
129 | """
130 | game, losses = self.gipf_check(arguments, ('freecell', 'gargantua'))
131 | # Freecell gives you a free ride no matter what the roll is.
132 | if game == 'freecell':
133 | if not losses:
134 | self.free_free = True
135 | # Gargantua lets you change one die to a six.
136 | elif game == 'gargantua':
137 | if not losses:
138 | self.human.tell('\nYour roll is: {}.'.format(self.dice))
139 | query = 'Which value would you like to change to a six? '
140 | to_six = self.human.ask_int(query, valid = self.dice)
141 | to_change = self.dice.index(to_six)
142 | self.dice[to_change].value = 6
143 | return True
144 | # Otherwise I'm confused.
145 | else:
146 | self.human.tell("I don't understand.")
147 |
148 | def game_over(self):
149 | """Check for any number being discarded 8 times. (bool)"""
150 | if self.mode == 'roll' and max(self.discards.values()) == 8:
151 | score = self.scores[self.human]
152 | # Win
153 | if score < 0:
154 | self.human.tell('You lost with {} points. :('.format(score))
155 | self.win_loss_draw[1] = 1
156 | # Loss
157 | elif score <= 500:
158 | self.human.tell('You drew with {} points. :|'.format(score))
159 | self.win_loss_draw[2] = 1
160 | # Draw
161 | else:
162 | self.human.tell('You won with {} points! :)'.format(score))
163 | self.win_loss_draw[0] = 1
164 | return True
165 |
166 | def player_action(self, player):
167 | """
168 | Roll, discard, and choose two pairs. (bool)
169 |
170 | Parameters:
171 | player: The player whose turn it is. (Player)
172 | """
173 | # Roll five dice.
174 | if self.mode == 'roll':
175 | self.roll_mode(player)
176 | self.show_status(player)
177 | # Discard one die.
178 | if self.mode == 'discard':
179 | return self.discard_mode(player)
180 | # Split into pairs.
181 | if self.mode == 'split':
182 | return self.split_mode(player)
183 |
184 | def roll_mode(self, player):
185 | """
186 | Roll the dice. (bool)
187 |
188 | Parameters:
189 | player: The player whose turn it is. (Player)
190 | """
191 | # Roll the dice.
192 | self.dice.release()
193 | self.dice.roll()
194 | self.dice.sort()
195 | # Set tracking variables.
196 | self.mode = 'discard'
197 | self.message = ''
198 |
199 | def set_options(self):
200 | """Set the possible options for the game. (None)"""
201 | # Add a dummy option group.
202 | self.option_set.add_group('gonzo', ['gz'], '')
203 |
204 | def set_up(self):
205 | """Set up the game. (None)"""
206 | self.dice = dice.Pool([6] * 5)
207 | self.totals = [0] * 13
208 | self.discards = collections.defaultdict(int)
209 | self.free_free = False
210 | self.mode = 'roll'
211 |
212 | def show_status(self, player):
213 | """
214 | Show the current game state. (None)
215 |
216 | Parameters:
217 | player: The player whose turn it is. (Player)
218 | """
219 | # show sums
220 | player.tell()
221 | player.tell('SUMS:')
222 | for line in range(13):
223 | player.tell(SUM_LEADS[line], end = ' ')
224 | if SUM_VALUES[line] and self.totals[line]:
225 | player.tell(self.totals[line])
226 | else:
227 | player.tell()
228 | # show discards
229 | player.tell('\nDISCARDS:')
230 | player.tell('# Count')
231 | player.tell('-- -----')
232 | for value in sorted(self.discards.items()):
233 | player.tell('{}: {}'.format(*value))
234 | # show score
235 | player.tell('\nYour current score is {}.'.format(self.scores[player]))
236 | # show message
237 | if self.message:
238 | player.tell(self.message.strip())
239 | self.message = ''
240 | # show roll
241 | player.tell('Your roll is: {}.'.format(self.dice))
242 |
243 | def split_mode(self, player):
244 | """
245 | Choose two pairs of dice. (bool)
246 |
247 | Parameters:
248 | player: The player whose turn it is. (Player)
249 | """
250 | # Get the split
251 | prompt = 'Choose two numbers to make a pair: '
252 | split = player.ask_int_list(prompt, valid = self.dice, valid_lens = [2])
253 | if isinstance(split, list):
254 | # Handle a valid split
255 | self.totals[sum(split)] += 1
256 | self.dice.hold(split[0])
257 | self.dice.hold(split[1])
258 | self.totals[sum(self.dice.get_free())] += 1
259 | self.update_score(player, split)
260 | self.mode = 'roll'
261 | else:
262 | # Handle other commands
263 | return self.handle_cmd(split)
264 |
265 | def update_score(self, player, split):
266 | """
267 | Update the game score. (None)
268 |
269 | Parameters:
270 | player: The player whose turn it is. (Player)
271 | split: One pair from the recent split. (list of int)
272 | """
273 | # Update the whole score from scratch, total by total.
274 | score = 0
275 | for total, value in zip(self.totals, SUM_VALUES):
276 | if 0 < total < 5:
277 | score -= 200
278 | elif total:
279 | score += (min(total, 10) - 5) * value
280 | self.scores[player] = score
281 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/canfield_game.py:
--------------------------------------------------------------------------------
1 | """
2 | canfield_game.py
3 |
4 | A game of Canfield.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: Credits for Canfield. (str)
11 | OPTIONS: Options for Canfield. (str)
12 | RULES: Rules for Canfield. (str)
13 |
14 | Classes:
15 | Canfield: A game of Canfield. (Solitaire)
16 | """
17 |
18 |
19 | from ... import cards
20 | from ... import options
21 | from . import solitaire_game as solitaire
22 |
23 |
24 | CREDITS = """
25 | Game Design: Richard A. Canfield
26 | Game Programming: Craig "Ichabod" O'Brien
27 | """
28 |
29 | RULES = """
30 | The deal is four cards to four tableau piles, one card to start one of the
31 | foundations, thirteen cards to a reserve, and the rest of the cards to the
32 | stock.
33 |
34 | Foundation piles are built up in rank by suit from whatever rank was put in
35 | the first foundation pile, going from king to ace if necessary. Tableau piles
36 | are built down in rank by alternating color. The top card of the reserve is
37 | available for building, and you may turn over the stock to the waste three
38 | cards at a time and use the top card of the waste. Empty piles on the
39 | tableau may only be filled from the reserve. If the reserve is empty, cards
40 | from the waste may be used to fill empty spots on the tableau.
41 |
42 | Stacks on the tableau may be moved, but only if the whole stack is moved.
43 | """
44 |
45 | OPTIONS = """
46 | build= (b): How tableau piles are built by suit. (alt-color, suit, or any)
47 | chameleon (ch): Equivalent to 'build=any max-passes=1 partial-move reserve=12
48 | tableau=3 turn-count=1'
49 | foundation= (f): The rank to start the foundations with.
50 | free-lane (fl): Empty tableau piles may be filled by any card.
51 | gonzo (gz): Equivalent to 'max-passes=2 partial-move selective turn-count=1
52 | visible-reserve'
53 | max-passes= (mp): How many passes you get through the deck, -1 for infinite.
54 | partial-move (pm): Parts of piles may be moved on the tableau.
55 | rainbow (rb): Equivalent to 'build=any'.
56 | rainbow-one (rb1): Equivalent to 'build=any max-passes=2 turn-count=1'.
57 | reserve-size= (rs): How many cards go into the reserve. (10-15)
58 | selective (s): Deal five cards, choose which goes on a foundation.
59 | storehouse (sh): Equivalent to 'build=suit foundation=2 max-passes=2 turn-count=1'.
60 | superior (sup): Equivalent to 'visible-reserve free-lane'.
61 | tableau= (t): How many tableau piles there are. (3-5)
62 | turn-count= (tc): How many cards get turned over from the stock at a time.
63 | two-by-one (2x1): Equivalent to 'max-passes=2 turn-count=1'
64 | visible-reserve (vr): Deal the reserve face up.
65 | """
66 |
67 |
68 | class Canfield(solitaire.Solitaire):
69 | """
70 | A game of Canfield. (Solitaire)
71 |
72 | Attributes:
73 | build: The type of suit matching needed for building. (str)
74 | foundation: The card rank to fill the foundations with. (str)
75 | free_lane: A flag to allow filling empty piles from the waste. (bool)
76 | partial_move: A flag for allowing moving partial stacks. (bool)
77 | reserve_size: How many cards should be dealt to the reserve. (int)
78 | selective: A flag for a deal of five, player chooses foundation. (bool)
79 | visible_reserve: A flag for dealing the reserve face up. (bool)
80 |
81 | Methods:
82 | superior_text: Generate text for the reserve in the superior variant. (str)
83 |
84 | Overridden Methods:
85 | handle_options
86 | set_checkers
87 | set_options
88 | """
89 |
90 | aka = ['Demon', 'Canf']
91 | categories = ['Card Games', 'Solitaire Games', 'Digging Games']
92 | credits = CREDITS
93 | name = 'Canfield'
94 | num_options = 10
95 | options = OPTIONS
96 | rules = RULES
97 |
98 | def do_gipf(self, arguments):
99 | """
100 | Blackjack allows you to build a jack onto anything.
101 |
102 | Prisoner's Dilemma allows you to lane any card once.
103 | """
104 | game, losses = self.gipf_check(arguments, ('blackjack', "prisoner's dilemma"))
105 | # Blackjack allows building a jack on anything.
106 | if game == 'blackjack':
107 | if not losses:
108 | pair_hold = self.pair_checkers
109 | self.pair_checkers = []
110 | go = True
111 | while go:
112 | self.human.tell(self)
113 | cards = self.human.ask('\nEnter a jack and anything to build it on: ')
114 | if cards.strip().upper()[0] != 'J':
115 | self.human.error('The first card must be a jack.')
116 | continue
117 | go = self.do_build(cards)
118 | self.pair_checkers = pair_hold
119 | # Prisoner's Dilemma allows you to lane any card once.
120 | if game == "prisoner's dilemma":
121 | if not losses:
122 | self.lane_checkers = []
123 | self.human.tell('\nThe next card you lane can be any otherwise playable card.')
124 | # Otherwise I'm confused.
125 | else:
126 | self.human.tell("I'm sorry, I don't speak Flemish.")
127 | return True
128 |
129 | def do_lane(self, card):
130 | """
131 | Move a card into an empty lane. (l)
132 |
133 | This command takes one argument: the card to move.
134 | """
135 | go = super(Canfield, self).do_lane(card)
136 | if not go and not self.free_lane:
137 | self.lane_checkers = [solitaire.lane_reserve_waste]
138 | return go
139 |
140 | def handle_options(self):
141 | """Handle the option settings for this game. (None)"""
142 | super(Canfield, self).handle_options()
143 | # Make the reserve visible, if necessary.
144 | if self.visible_reserve:
145 | self.reserve_text = self.superior_text
146 |
147 | def set_checkers(self):
148 | """Set up the game specific rules. (None)"""
149 | # Set the default checkers.
150 | super(Canfield, self).set_checkers()
151 | # Set the rules.
152 | self.build_checkers = [solitaire.build_whole]
153 | self.lane_checkers = [solitaire.lane_reserve_waste]
154 | self.pair_checkers = [solitaire.pair_down, solitaire.pair_alt_color]
155 | self.sort_checkers = [solitaire.sort_rank, solitaire.sort_up]
156 | # Set the dealers.
157 | reserve_dealer = solitaire.deal_reserve_n(self.reserve_size, self.visible_reserve)
158 | self.dealers = [reserve_dealer, solitaire.deal_start_foundation, solitaire.deal_one_row,
159 | solitaire.deal_stock_all]
160 | # Handle the deal options.
161 | if self.foundation:
162 | self.dealers.insert(0, solitaire.deal_rank_foundations(self.foundation))
163 | del self.dealers[2]
164 | elif self.selective:
165 | self.dealers = [reserve_dealer, solitaire.deal_selective, solitaire.deal_stock_all]
166 | if self.partial_move:
167 | self.build_checkers = []
168 | # Handle the tableau options.
169 | if self.build == 'suit':
170 | self.pair_checkers[1] = solitaire.pair_suit
171 | elif self.build == 'any':
172 | del self.pair_checkers[1]
173 | if self.partial_move:
174 | self.build_checkers = []
175 | if self.free_lane:
176 | self.lane_checkers = []
177 |
178 | def set_options(self):
179 | """Define the game options. (None)"""
180 | self.options = {'num-reserve': 1, 'deck-specs': (0, cards.STANDARD_WRAP_RANKS)}
181 | # Set up the deal options.
182 | self.option_set.add_option('foundation', ['f'], options.upper, default = '',
183 | valid = 'A23456789TJQK',
184 | question = 'What rank should the foundations be filled with (return for none)? ')
185 | self.option_set.add_option('reserve-size', ['rs'], int, default = 13, valid = range(10, 16),
186 | question = 'How many cards should be dealt to the reserve (10-15, return for 13)? ')
187 | self.option_set.add_option('selective', ['s'],
188 | question = 'Should you be able to choose which starting card goes on the foundations? bool')
189 | self.option_set.add_option('tableau', ['t'], int, action = 'key=num-tableau', default = 4,
190 | valid = (3, 4, 5), target = self.options,
191 | question = 'How many tableau piles should there be (3 to 5, return for 4)? ')
192 | self.option_set.add_option('visible-reserve', ['vr'],
193 | question = 'Should the reserve be visible? bool')
194 | # Set up the stock options.
195 | self.option_set.add_option('max-passes', ['mp'], int, action = 'key=max-passes', default = -1,
196 | valid = (-1, 1, 2, 3), target = self.options,
197 | question = 'Allow how many passes through the stock (1 to 3, -1 or return for no limit)? ')
198 | self.option_set.add_option('turn-count', ['tc'], int, action = 'key=turn-count', default = 3,
199 | valid = (1, 2, 3), target = self.options,
200 | question = 'Turn over how many cards from the stock (1 to 3, return for 3)? ')
201 | # Set up the tableau options.
202 | self.option_set.add_option('build', ['b'], options.lower, default = 'alt-color',
203 | valid = ('alt-color', 'suit', 'any'),
204 | question = 'How should cards be built on the tableau (alt-color [default], suit, or any)? ')
205 | self.option_set.add_option('free-lane', ['fl'],
206 | question = 'Should you be able to fill empty piles with any card? bool')
207 | self.option_set.add_option('partial-move', ['pm'],
208 | question = 'Should you be able to move partial stacks? bool')
209 | # Set the option groups.
210 | self.option_set.add_group('chameleon', ['ch'],
211 | 'build=any max-passes=1 partial-move reserve-size=12 tableau=3 turn-count=1')
212 | self.option_set.add_group('gonzo', ['gz'],
213 | 'max-passes=2 partial-move selective turn-count=1 visible-reserve')
214 | self.option_set.add_group('rainbow', ['rb'], 'build=any')
215 | self.option_set.add_group('rainbow-one', ['rb1'], 'build=any max-passes=2 turn-count=1')
216 | self.option_set.add_group('storehouse', ['sh'], 'build=suit foundation=2 max-passes=2 turn-count=1')
217 | self.option_set.add_group('superior', ['sup'], 'visible-reserve free-lane')
218 | self.option_set.add_group('two-by-one', ['2x1'], 'max-passes=2 turn-count=1')
219 |
220 | def superior_text(self):
221 | """Generate text for the reserve in the superior variant. (str)"""
222 | return ' '.join([str(card) for card in self.reserve[0]])
223 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/thoughtful_game.py:
--------------------------------------------------------------------------------
1 | """
2 | thoughtful_game.py
3 |
4 | A game of Thoughtful Solitaire (open Klondike).
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: The credits for Thoughtful Solitaire. (str)
11 | OPTIONS: The options for Thoughtful Solitaire. (str)
12 | RULES: The rules of Thoughtful Solitaire. (str)
13 |
14 | Classes:
15 | Thoughtful: A game of Thoughtful Solitaire. (solitaire.Solitaire)
16 | """
17 |
18 |
19 | import random
20 |
21 | from . import solitaire_game as solitaire
22 |
23 |
24 | CREDITS = """
25 | Game Design: Traditional
26 | Game Programming: Craig "Ichabod" O'Brien
27 | """
28 |
29 | RULES = """
30 | Thoughtful Solitaire is an open version of Klondike, so all card are dealt face
31 | up. As in Klondike, you can build any card on the top of a tableau pile onto
32 | another tableau card this the opposite color and one rank higher. You can move
33 | complete stacks built in that way as well. The top card of any tableau pile may
34 | also sorted to the foundations going up and in suit. Empty lanes may be filled
35 | with a king.
36 |
37 | In addition there are eight reserve piles. The top card from any reserve pile
38 | may be built to the tableau or sorted to the foundations as stated above.
39 | However, once you pull a card from a reserve pile, all the reserve piles to the
40 | left of that reserve pile will be blocked (as indicated by an XX at the bottom
41 | of the pile). They can be unblocked with the turn command. This simulates going
42 | through the stock three cards at a time as in normal Klondike.
43 | """
44 |
45 | OPTIONS = """
46 | piles= (p=): The number of tablue piles (4-9, defaults to 7).
47 | unblocked (u): There are no blocked reserve piles.
48 | """
49 |
50 |
51 | class Thoughtful(solitaire.Solitaire):
52 | """
53 | A game of Thoughtful Solitaire. (Solitaire)
54 |
55 | Attributes:
56 | blocked_history: The value of blocked_index after each move. (list of int)
57 | blocked_index: The index of the rightmost blocked reserve pile. (int)
58 | piles: The number of tableau piles in the game. (int)
59 | unblocked: A flag for not blocking piles. (bool)
60 |
61 | Methods:
62 | card_shift: Shift a card to the top of it's pile. (None)
63 | turn_transfer: Move a card while undoing a turn move. (None)
64 |
65 | Overridden Methods:
66 | do_turn
67 | do_undo
68 | reserve_text
69 | set_checkers
70 | set_options
71 | set_up
72 | transfer
73 | """
74 |
75 | aka = ['Thoughtful', 'ThSo']
76 | categories = ['Card Games', 'Solitaire Games', 'Open Games']
77 | credits = CREDITS
78 | name = 'Thoughtful Solitaire'
79 | num_options = 2
80 | options = OPTIONS
81 | rules = RULES
82 |
83 | def card_shift(self, piles, pile_type):
84 | """
85 | Shift a card to the top of it's pile. (None)
86 |
87 | Parameters:
88 | piles: The list of piles the card must be in. (list of list)
89 | pile_type: The name of the piles being searched. (str)
90 | """
91 | # Get the card from the user.
92 | print(self)
93 | query = '\nWhich {} card would you like to move to the top of its stack? '.format(pile_type)
94 | while True:
95 | card = self.human.ask(query)
96 | for pile in piles:
97 | if card in pile:
98 | break
99 | else:
100 | # Warn the user if you can't find the card.
101 | self.human.error('That card is not in the {}.'.format(pile_type))
102 | continue
103 | break
104 | # Move the card to the top of the pile.
105 | pile.append(pile.pop(pile.index(card)))
106 | # Block undo past this point.
107 | self.moves = []
108 |
109 | def do_gipf(self, arguments):
110 | """
111 | Chess lets you move a tableau card to the top of its pile.
112 |
113 | Klondike lets you move a reserve card to the top of its pile.
114 | """
115 | game, losses = self.gipf_check(arguments, ('chess', 'klondike'))
116 | go = True
117 | if game == 'chess':
118 | if not losses:
119 | self.card_shift(self.tableau, 'tableau')
120 | elif game == 'klondike':
121 | if not losses:
122 | self.card_shift(self.reserve, 'reserve')
123 | else:
124 | self.human.tell("That's exactly what I was thinking!")
125 | return go
126 |
127 | def do_turn(self, arguments):
128 | """
129 | Turn cards from the stock into the waste. (t)
130 |
131 | This command takes no arguments. The cards in the reserve are moved left from
132 | the bottom up until all piles (except maybe the last one) have three cards.
133 | """
134 | # Track if any moves are actually made.
135 | start_moves = len(self.moves)
136 | # Get the first pile needing cards.
137 | for pile_index, pile in enumerate(self.reserve):
138 | if len(pile) < 3:
139 | start_pile = pile_index
140 | break
141 | else:
142 | start_pile = len(self.reserve) - 1
143 | # Get the first pile to the right with cards.
144 | end_pile = start_pile + 1
145 | for pile_index, pile in enumerate(self.reserve[end_pile:], start = end_pile):
146 | if pile:
147 | end_pile = pile_index
148 | break
149 | else:
150 | # Catch no cards to move.
151 | end_pile = 0
152 | # Loop through the remaining cards.
153 | undo = 0
154 | while end_pile and end_pile < self.options['num-reserve']:
155 | # Move the next card to the stack needing one.
156 | self.transfer([self.reserve[end_pile][0]], self.reserve[start_pile], undo_ndx = undo)
157 | # Update the undo count so it's treated as one move.
158 | undo += 1
159 | # Update the end pile.
160 | while end_pile < self.options['num-reserve'] and not self.reserve[end_pile]:
161 | end_pile += 1
162 | # Update the start pile if necessary.
163 | if len(self.reserve[start_pile]) == 3:
164 | start_pile += 1
165 | # Start pile and end pile can't be the same pile, it will reverse itself infinitely.
166 | if start_pile == end_pile:
167 | end_pile += 1
168 | while end_pile < self.options['num-reserve'] and not self.reserve[end_pile]:
169 | end_pile += 1
170 | # Check for no cards to move.
171 | if start_moves == len(self.moves):
172 | # Find the rightmost pile.
173 | pile_index = -1
174 | try:
175 | while not self.reserve[pile_index]:
176 | pile_index -= 1
177 | # Watch out for an empty reserve.
178 | except IndexError:
179 | self.human.error('There is nothing to turn.')
180 | return True
181 | # Move that card onto itself (it goes to turn_transfer, so gets put on the bottom).
182 | #self.transfer([self.reserve[pile_index][0]], self.reserve[pile_index])
183 | # Reset to no blocked piles.
184 | if self.blocked and self.blocked_history:
185 | self.blocked_index = -1
186 | self.blocked_history[-1] = -1
187 |
188 | def do_undo(self, arguments):
189 | """
190 | Undo one or more previous moves. (u)
191 |
192 | If this command is called with no arguments, one move is undone. If an integer
193 | argument is given, that many moves are undone.
194 | """
195 | super(Thoughtful, self).do_undo(arguments)
196 | if self.blocked:
197 | # Reset the blocked history and the blocked pile.
198 | self.blocked_history = self.blocked_history[:len(self.moves)]
199 | if self.blocked_history:
200 | self.blocked_index = self.blocked_history[-1]
201 | else:
202 | self.blocked_index = -1
203 |
204 | def handle_options(self):
205 | """Handle the option settings for the game. (None)"""
206 | super(Thoughtful, self).handle_options()
207 | reserve_cards = 52 - sum(range(1, self.options['num-tableau'] + 1))
208 | self.options['num-reserve'] = reserve_cards // 3 + (reserve_cards % 3 != 0)
209 |
210 | def reserve_text(self):
211 | """Generate text for the reserve piles. (str)"""
212 | # Set up a blank reserve.
213 | max_reserve = max([len(pile) for pile in self.reserve])
214 | reserve_lines = [[' ' for pile in self.reserve] for row in range(max_reserve)]
215 | # Fill in the cards.
216 | for pile_index, pile in enumerate(self.reserve):
217 | for card_index, card in enumerate(pile):
218 | reserve_lines[card_index][pile_index] = str(card)
219 | if self.blocked and self.blocked_index != -1:
220 | reserve_lines.append(['XX'] * (self.blocked_index + 1))
221 | # Format and return as a string.
222 | return '\n'.join(['{}'.format(' '.join(line)) for line in reserve_lines])
223 |
224 | def set_checkers(self):
225 | """Set up the game specific rules. (None)"""
226 | self.free_checkers = []
227 | self.match_checkers = [solitaire.match_none]
228 | # Set the game specific rules checkers.
229 | self.build_checkers = [solitaire.build_unblocked]
230 | self.lane_checkers = [solitaire.lane_king, solitaire.lane_unblocked]
231 | self.pair_checkers = [solitaire.pair_down, solitaire.pair_alt_color]
232 | self.sort_checkers = [solitaire.sort_ace, solitaire.sort_up, solitaire.sort_unblocked]
233 | # Set the dealers.
234 | self.dealers = [solitaire.deal_klondike, solitaire.deal_reserve_by_n(3, True), solitaire.deal_open]
235 |
236 | def set_options(self):
237 | """Define the options for the game. (None)"""
238 | self.options = {}
239 | self.option_set.add_option('unblocked', ['u'], default = True, value = False, target = 'blocked',
240 | question = 'Should indexes to the left of that reserve pile used be blocked? bool')
241 | self.option_set.add_option('piles', ['p'], action = 'key=num-tableau', converter = int,
242 | default = 7, valid = range(4, 10), target = self.options,
243 | question = 'How many tableau piles (4-9, return for 7)? ')
244 | self.option_set.add_group('gonzo', ['gz'], 'piles = 6')
245 |
246 | def set_up(self):
247 | """Set up the game. (None)"""
248 | super(Thoughtful, self).set_up()
249 | # Set up tracking for the blocked pile.
250 | self.blocked_index = -1
251 | self.blocked_history = []
252 |
253 | def transfer(self, move_stack, new_location, track = True, up = True, undo_ndx = 0):
254 | """
255 | Move a stack of cards from one game location to another. (None)
256 |
257 | This handles the card's knowledge of where it is and tracking game moves.
258 |
259 | Parameters:
260 | move_stack: The stack of cards to move. (list of Card)
261 | new_location: The new game location for the cards. (list of Card)
262 | track: A flag for tracking the move. (bool)
263 | up: A flag for the cards being face up. (bool)
264 | undo_ndx: Nominally how many undos there are to do. (int)
265 | """
266 | # Check for undoing turns.
267 | old_location = move_stack[0].game_location
268 | if new_location in self.reserve and old_location in self.reserve and not track:
269 | self.turn_transfer(move_stack, new_location)
270 | else:
271 | super(Thoughtful, self).transfer(move_stack, new_location, track, up, undo_ndx)
272 | # Update and record the blocked pile for this turn.
273 | if track and self.blocked:
274 | # Check by id() to avoid matching empty lists that aren't the same pile.
275 | pile_ids = [id(pile) for pile in self.reserve]
276 | if not undo_ndx and id(old_location) in pile_ids:
277 | self.blocked_index = pile_ids.index(id(old_location)) - 1
278 | # Move the block back after emptying a reserve pile.
279 | while self.blocked_index > -1 and not self.reserve[self.blocked_index + 1]:
280 | self.blocked_index -= 1
281 | self.blocked_history.append(self.blocked_index)
282 |
283 | def turn_transfer(self, move_stack, new_location):
284 | """
285 | Move a card while undoing a turn move. (None)
286 |
287 | This version prepends the card to the new_location rather than appending it.
288 |
289 | Parameters:
290 | move_stack: The stack of cards to move. (list of Card)
291 | new_location: The new game location for the cards. (list of Card)
292 | """
293 | # Record the move.
294 | old_location = move_stack[0].game_location
295 | # Move the cards.
296 | for card in move_stack:
297 | old_location.remove(card)
298 | new_location.insert(0, card)
299 | # Reset location tracking.
300 | for card in move_stack:
301 | card.game_location = new_location
302 |
--------------------------------------------------------------------------------
/card_games/solitaire_games/pyramid_game.py:
--------------------------------------------------------------------------------
1 | """
2 | pyramid_game.py
3 |
4 | A game of Pyramid.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: The credits for Pyramid. (str)
11 | OPTIONS: The options for Pyramid. (str)
12 | RULES: The rules for Pyramid. (str)
13 |
14 | Classes:
15 | Pyramid: A game of Pyramid. (solitaire.Solitaire)
16 | """
17 |
18 |
19 | from . import solitaire_game as solitaire
20 |
21 |
22 | # The credits for Pyramid.
23 | CREDITS = """
24 | Game Design: Traditional
25 | Giza option designed by Michael Keller
26 | Game Programming: Craig "Ichabod" O'Brien
27 | """
28 |
29 | # The options for Pyramid.
30 | OPTIONS = """
31 | cells= (c): The number of free cells available. 0 to 10, defaults to 0.
32 | giza (g): Fully open game with 8 reserve piles. Equivalent to 'reserve=8
33 | reserve-rows=3'.
34 | gonzo (gz): Equivalent to 'passes=2 turn-count=3'.
35 | klondike (k): Klondike style stock and waste. Equivalent to 'passes=-1
36 | turn-count=3'.
37 | passes= (p): The number of passes through the stock you get. -1 gives
38 | unlimited passes. If this is not one, the standard-turn option is in
39 | effect. Defaults to 1.
40 | relaxed-match (rm): You may match cards even if one is blocking the other.
41 | relaxed-win (rw): If the pyramid is clear, you can win even if there are
42 | cards in the stock or waste.
43 | reserve= (r): The number of reserve piles. 0 to 8, defaults to 0.
44 | reserve-rows= (rr): The number of reserve rows. 0 to 3, defaults to 1.
45 | standard-turn (st): Cards are not sorted from the waste when turning cards
46 | from the stock.
47 | turn-count= (tc): How many cards are turned over from the stock at a time.
48 | Defaults to 1.
49 | """
50 |
51 | # The rules for Pyramid.
52 | RULES = """
53 | A pyramid of cards is dealt out with one card on the top row, two cards on the
54 | second row, and so on for seven rows. Cards are open on the tableau if both of
55 | the card under that card have been removed. For example, in this layout:
56 |
57 | AC
58 | JD KD
59 | 5S
60 |
61 | The five of spades and the king of diamonds are available for play, but the
62 | jack of diamonds is blocked by the five of spades, and the ace of clubs is
63 | blocked by the jack and king of diamonds.
64 |
65 | Any pair that totals thirteen may be matched to the foundation. Jacks count as
66 | 11, queens as 12, and kings as 13 (kings may just be sorted to the
67 | foundation). The stock may be turned over one at a time to be matched with
68 | cards on the tableau. However, any unused waste cards are sorted to the
69 | foundation.
70 | """
71 |
72 |
73 | class Pyramid(solitaire.Solitaire):
74 | """
75 | A game of Pyramid (solitaire.Solitaire)
76 |
77 | Attributes:
78 | relaxed_match: A flag for relaxing the matching rules. (bool)
79 | relaxed_wins: A flag for winning just by clearing the pyramid. (bool)
80 | reserve_rows: The number of cards in each reserve pile. (int)
81 | standard_turn: A flag for using the default turning rules. (bool)
82 |
83 | Methods:
84 | is_empty: Check that there are no face up cards not in the foundation. (bool)
85 |
86 | Overridden Methods:
87 | do_turn
88 | find_foundation
89 | game_over
90 | handle_options
91 | reserve_text
92 | set_checkers
93 | set_options
94 | tableau_text
95 | """
96 |
97 | aka = ['Pyra']
98 | categories = ['Card Games', 'Solitaire Games', 'Matching Games']
99 | credits = CREDITS
100 | name = 'Pyramid'
101 | num_options = 8
102 | options = OPTIONS
103 | rules = RULES
104 |
105 | def do_auto(self, max_rank):
106 | """
107 | Automatically play cards to the foundations. (a)
108 |
109 | If no argument is given, auto will play cards as long as it can. If a card
110 | rank is given as an argument, auto will on play cards up to and including that
111 | rank. If the pyramid and waste are cleared (and any reserve or free cells),
112 | auto will sort all of the cards in the stock (except with the standard-turn
113 | option).
114 | """
115 | # Do the normal auto sort.
116 | super(Pyramid, self).do_auto(max_rank)
117 | # Check for sorting the stock.
118 | if not self.standard_turn and self.is_empty():
119 | self.sort_stock()
120 | return False
121 |
122 | def do_gipf(self, arguments):
123 | """
124 | Monte Carlo slides all the cards to the left to fill in any gaps.
125 |
126 | Spider (hah!) sorts any unblocked cards.
127 | """
128 | # Run the edge, if possible.
129 | game, losses = self.gipf_check(arguments, ('monte carlo', 'spider'))
130 | # Winning Monte Carlo slides all cards to the left.
131 | if game == 'monte carlo':
132 | if not losses:
133 | # Move cards over repeatedly.
134 | while True:
135 | undo_index = 0
136 | # Loop through pairs of tabelau piles.
137 | for left, right in zip(self.tableau, self.tableau[1:]):
138 | # Move the appropriate cards if there are any.
139 | stack = right[(len(left) - 1):]
140 | if stack:
141 | self.transfer(stack, left, undo_ndx = undo_index)
142 | undo_index += 1
143 | # If no cards were moved, stop trying to move cards.
144 | if not undo_index:
145 | break
146 | # Winning Spider (hah!) sorts any unblocked cards.
147 | elif game == 'spider':
148 | if not losses and self.stock:
149 | # Sort all of the unblocked cards.
150 | for pile_index, pile in enumerate(self.tableau):
151 | if pile and not solitaire.sort_pyramid(self, pile[-1], self.foundations[0]):
152 | self.transfer(pile[-1:], self.foundations[0], undo_ndx = pile_index)
153 | # Otherwise I'm confused.
154 | else:
155 | self.human.tell("No, it's Giza. Gee-zah.")
156 | return True
157 |
158 | def do_turn(self, arguments):
159 | """
160 | Turn cards from the stock into the waste. (t)
161 |
162 | In Pyramid, cards in the waste are sorted to the foundation before the next
163 | card is turned over from the stock. If the only cards left are in the stock,
164 | they will all be sorted instead of turned over into the waste (except with
165 | the standard-turn option).
166 | """
167 | # Store current move tracking.
168 | count_hold = self.move_count
169 | # Move the current waste card to the foundation.
170 | if self.waste:
171 | self.transfer(self.waste[:], self.foundations[0])
172 | # Check for autosorting the stock.
173 | if not self.standard_turn and self.is_empty():
174 | self.sort_stock()
175 | # Otherwise, do the turn as normal.
176 | else:
177 | super(Pyramid, self).do_turn(arguments)
178 | # Update the undo and move tracking.
179 | for move in self.moves[-self.options['turn-count']:]:
180 | move[-2] += 1
181 | self.move_count = count_hold + 1
182 |
183 | def find_foundation(self, card):
184 | """
185 | Find the foundation a card can be sorted to. (list of TrackingCard)
186 |
187 | Parameters:
188 | card: The card to sort. (str)
189 | """
190 | return self.foundations[0]
191 |
192 | def game_over(self):
193 | """Check for the end of the game."""
194 | # Check for relaxed win and empty pyramid.
195 | if self.relaxed_win and not self.tableau[0] and not self.cells and not any(self.reserve):
196 | # Transfer the stock and the waste to the foundation for a win.
197 | if self.waste:
198 | self.transfer(self.waste[:], self.foundations[0])
199 | if self.stock:
200 | self.transfer(self.stock[:], self.foundations[0])
201 | # Return the normal check.
202 | return super(Pyramid, self).game_over()
203 |
204 | def handle_options(self):
205 | """Handle the particular option settings. (None)"""
206 | super(Pyramid, self).handle_options()
207 | # Multiple passes through the stock requires standard turn rules.
208 | if self.options['max-passes'] > 1 or self.options['max-passes'] == -1:
209 | self.standard_turn = True
210 | # Apply the standard turn rules.
211 | if self.standard_turn:
212 | self.do_turn = super(Pyramid, self).do_turn
213 |
214 | def is_empty(self):
215 | """Check that there are no face up cards not in the foundation. (bool)"""
216 | return not any(self.tableau) and not any(self.reserve) and not self.cells and not self.waste
217 |
218 | def reserve_text(self):
219 | """Generate text for the reserve piles. (str)"""
220 | # Set up a blank reserve.
221 | max_reserve = max([len(pile) for pile in self.reserve])
222 | reserve_lines = [[' ' for pile in self.reserve] for row in range(max_reserve)]
223 | # Fill in the cards.
224 | for pile_index, pile in enumerate(self.reserve):
225 | for card_index, card in enumerate(pile):
226 | reserve_lines[card_index][pile_index] = str(card)
227 | # Format and return as a string.
228 | padding = ' ' * (7 - self.options['num-reserve'])
229 | return '\n'.join(['{}{}'.format(padding, ' '.join(line)) for line in reserve_lines])
230 |
231 | def set_checkers(self):
232 | """Set up the game specific rules. (None)"""
233 | super(Pyramid, self).set_checkers()
234 | # Set the dealers.
235 | self.dealers = [solitaire.deal_pyramid]
236 | if self.options['num-reserve']:
237 | reserve_cards = self.options['num-reserve'] * self.reserve_rows
238 | self.dealers.append(solitaire.deal_reserve_n(reserve_cards, up = True))
239 | self.dealers.append(solitaire.deal_stock_all)
240 | # Set the rule checkers.
241 | self.build_checkers = [solitaire.build_none]
242 | self.lane_checkers = [solitaire.lane_none]
243 | if self.relaxed_match:
244 | self.match_checkers = [solitaire.match_top_two, solitaire.match_pyramid_relax]
245 | else:
246 | self.match_checkers = [solitaire.match_top, solitaire.match_pyramid]
247 | self.match_checkers.append(solitaire.match_thirteen)
248 | self.sort_checkers = [solitaire.sort_kings_only, solitaire.sort_pyramid]
249 | # Add free cell rules.
250 | if self.options['num-cells']:
251 | self.free_checkers = [solitaire.free_pyramid]
252 |
253 | def set_options(self):
254 | """Set up the game specific options. (None)"""
255 | # Set the solitaire options.
256 | self.options = {'max-passes': 1, 'num-foundations': 1, 'num-tableau': 7, 'turn-count': 1}
257 | # Set the option groups.
258 | self.option_set.add_group('giza', ['g'], 'reserve=8 reserve-rows=3')
259 | self.option_set.add_group('gonzo', ['gz'], 'cells=1 passes=2 turn-count=3')
260 | self.option_set.add_group('klondike', ['k'], 'passes=-1 turn-count=3')
261 | # Set the game options.
262 | # Set the stock and waste options.
263 | self.option_set.add_option('passes', ['p'], int, action = "key=max-passes", default = 1,
264 | check = lambda passes: passes > 1 or passes == -1, target = self.options,
265 | question = 'How many passes through the stock (-1 for infinite, return for 1)? ')
266 | self.option_set.add_option('standard-turn', ['st'],
267 | question = 'Should cards stay in the waste when new ones are turned from the stock? bool')
268 | self.option_set.add_option('turn-count', ['tc'], int, action = "key=turn-count", default = 1,
269 | valid = (1, 2, 3), target = self.options,
270 | question = 'How many cards turned from the stock at a time (1-3, return for 1)? ')
271 | # Set the relaxed rules options.
272 | self.option_set.add_option('relaxed-match', ['rm'],
273 | question = 'Should you be able to match cards that are blocking each other? bool')
274 | self.option_set.add_option('relaxed-win', ['rw'],
275 | question = 'Should you be able to win just by clearing the pyramid? bool')
276 | # Set options for additional piles.
277 | self.option_set.add_option('cells', ['c'], int, action = "key=num-cells", default = 0,
278 | valid = range(11), target = self.options,
279 | question = 'How many free cells should be available (0-10, return for 0)? ')
280 | self.option_set.add_option('reserve', ['r'], int, action = "key=num-reserve", default = 0,
281 | valid = range(9), target = self.options,
282 | question = 'How reserve piles should there be (0-8, return for 0)? ')
283 | self.option_set.add_option('reserve-rows', ['rr'], int, default = 1, valid = range(4),
284 | question = 'How many reserve rows should there be (0-3, return for 1)? ')
285 |
286 | def sort_stock(self):
287 | """Sort the stock. (self)"""
288 | for card in self.stock[:]:
289 | self.transfer([card], self.foundations[0])
290 |
291 | def tableau_text(self):
292 | """Generate the text representation of the tableau piles. (str)"""
293 | lines = []
294 | for pile_count in range(len(self.tableau)):
295 | # Pad each row to make a triantle shape.
296 | lines.append(' ' * (6 - pile_count + (self.options['num-reserve'] == 8)))
297 | for pile_index in range(pile_count + 1):
298 | # Show each row diagonally (down to the left) in the triangle shape.
299 | if len(self.tableau[pile_index]) > (pile_count - pile_index):
300 | card_text = str(self.tableau[pile_index][pile_count - pile_index])
301 | else:
302 | card_text = ' '
303 | lines[-1] = '{}{} '.format(lines[-1], card_text)
304 | lines = filter(lambda text: text.strip(), lines)
305 | return '\n'.join(lines)
306 |
--------------------------------------------------------------------------------
/other_games/dollar_game.py:
--------------------------------------------------------------------------------
1 | """
2 | dollar_game.py
3 |
4 | The Dollar Game, as specified in this Numberphile video:
5 | https://www.youtube.com/watch?v=U33dsEcKgeQ&t=3s
6 |
7 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
8 | See the top level __init__.py file for details on the t_games license.
9 |
10 | Constants:
11 | CREDITS: The credits for the Dollar Game. (str)
12 | OPTIONS: The options for the Dollar Game. (str)
13 | RULES: The rules of the Dollar Game. (str)
14 |
15 | Classes:
16 | DollarGame: A game of the Dollar Game. (game.Game)
17 | DollarGraph: A graph with a dollar value for each node. (object)
18 | """
19 |
20 |
21 | import random
22 | import string
23 |
24 | from .. import game
25 | from .. import utility
26 |
27 |
28 | CREDITS = """
29 | Game Design: Matt Baker
30 | Game Programming: Craig O'Brien, David B. Wilson
31 | """
32 |
33 | OPTIONS = """
34 | ease= (e=): How easy the graph is to solve (1-5, defaults to 2).
35 | from-zero (fz, f0): Calculates the inital values from 0 (see rules).
36 | genus= (g=): The genus of the graph (#edges - #nodes + 1, defaults to 3).
37 | gonzo (gz): Equivalent to 'genus=8 nodes=23 ease=1'.
38 | nodes= (n=): The number of nodes in the graph. Defaults to 5-10 at random.
39 | """
40 |
41 | RULES = """
42 | The game is made up of a bunch of nodes, each of which is a neighbor to one or
43 | more of the other nodes. Each node has a dollar value to it, which may be
44 | negative. Nodes with a negative dollar value are considered to be in debt.
45 |
46 | Two moves are available in the game. You can have a node donate one dollar to
47 | each of it's neighbors, or you can have a node take a dollar from each of its
48 | neighbors. The aliases for the donate move are d and -, the aliases for the
49 | take move are t and +.
50 |
51 | The game is won when all nodes are out of debt (that is, all nodes have a
52 | value of zero or more).
53 |
54 | See the Numberphile video (https://www.youtube.com/watch?v=U33dsEcKgeQ&t=3s)
55 | for more details.
56 |
57 | The initial values of the graph are normally calculated by assigned random
58 | values from -n to n (where n is the number of nodes) to each node, and then
59 | randomly normalizing the values based on the genus and the ease options. The
60 | from-zero option starts all the values at 0, and adds or subtracts from node
61 | values randomly until the total based on the genus and the ease is reached.
62 | """
63 |
64 |
65 | class DollarGame(game.Game):
66 | """
67 | A game of the Dollar Game. (game.Game)
68 |
69 | The ease option minus one is added to the genus of the graph to determine
70 | the total value of the graph.
71 |
72 | Attributes:
73 | auto_cap: A flag for automatically capitalizing user input. (bool)
74 | ease: The ease of solving the graph. (int)
75 | edges: The number of edges in the graph. (int)
76 | from_zero: A flag for seeding node values starting at 0. (bool)
77 | genus: The genus of the graph. (int)
78 | graph: The graph the game is played on. (DollarGraph)
79 | nodes: The number of nodes in the graph. (int)
80 | total_value: The total of the values of the nodes in the graph. (int)
81 |
82 | Methods:
83 | do_donate: Donate one dollar from a node to each of it's neighbors. (bool)
84 | do_take: Take one dollar from each of a node's neighbors. (bool)
85 |
86 | Overridden Methods:
87 | __str__
88 | handle_options
89 | player_action
90 | set_options
91 | set_up
92 | """
93 |
94 | aka = ['Dollar Game', 'Dollar', 'DoGa']
95 | aliases = {'-': 'donate', '+': 'take', 'd': 'donate', 't': 'take'}
96 | categories = ['Other Games', 'Theoretical Games']
97 | credits = CREDITS
98 | name = 'The Dollar Game'
99 | num_options = 4
100 | options = OPTIONS
101 | rules = RULES
102 |
103 | def __str__(self):
104 | """Human readable text representation. (str)"""
105 | return str(self.graph)
106 |
107 | def do_donate(self, arguments):
108 | """
109 | Donate one dollar from a node to each of it's neighbors.
110 |
111 | Aliases: d, -
112 | """
113 | if self.auto_cap:
114 | arguments = arguments.upper()
115 | try:
116 | self.graph.donate(arguments)
117 | except KeyError:
118 | self.human.error('{} is not a node in the graph.'.format(arguments))
119 | return True
120 |
121 | def do_gipf(self, arguments):
122 | """
123 | Thoughtful Solitaire lets you move one dollar from one node to another.
124 | """
125 | game, losses = self.gipf_check(arguments, ('thoughtful solitaire',))
126 | go = True
127 | if game == 'thoughtful solitaire':
128 | if not losses:
129 | print(self.graph)
130 | while True:
131 | take = self.human.ask('\nWhat node would you like to take a dollar from? ')
132 | if self.auto_cap:
133 | take = take.upper()
134 | if take not in self.graph.nodes:
135 | self.human.tell('That is not a valid node.')
136 | continue
137 | give = self.human.ask('What node would you like to give a dollar to? ')
138 | if self.auto_cap:
139 | give = give.upper()
140 | if take not in self.graph.nodes:
141 | self.human.tell('That is not a valid node.')
142 | continue
143 | break
144 | self.graph.values[take] -= 1
145 | self.graph.values[give] += 1
146 | go = True
147 | else:
148 | beg = self.human.ask("\nCould you gipf a dollar to a fellow American who's down on his luck? ")
149 | if beg in utility.YES:
150 | self.human.tell('Thank you.')
151 | go = False
152 | return go
153 |
154 | def do_take(self, arguments):
155 | """
156 | Take one dollar from each of a node's neighbors.
157 |
158 | Aliases: t, +
159 | """
160 | if self.auto_cap:
161 | arguments = arguments.upper()
162 | try:
163 | self.graph.take(arguments)
164 | except KeyError:
165 | self.human.error('{} is not a node in the graph.'.format(arguments))
166 | return True
167 |
168 | def game_over(self):
169 | """Determine if the game has been won. (bool)"""
170 | if min(self.graph.values.values()) >= 0:
171 | self.human.tell('You won in {} turns!'.format(self.turns))
172 | self.win_loss_draw = [1, 0, 0]
173 | self.scores[self.human] = self.genus - self.ease
174 | return True
175 | else:
176 | return False
177 |
178 | def handle_options(self):
179 | """Handle the option settings for this game. (None)"""
180 | super(DollarGame, self).handle_options()
181 | if not self.nodes:
182 | self.nodes = random.randint(5, 10)
183 | self.edges = self.genus + self.nodes - 1
184 | self.total_value = self.genus + self.ease - 1
185 | self.auto_cap = (self.nodes < 27)
186 |
187 | def set_options(self):
188 | """Set up the game options. (None)"""
189 | self.option_set.add_option('nodes', ['n'], int, 0, valid = range(2, 53),
190 | question = 'How many nodes should be in the graph (return for 5-10 at random)? ')
191 | self.option_set.add_option('genus', ['g'], int, 3, check = lambda x: x > 0,
192 | question = 'What should the genus of the graph be (return for 3)? ')
193 | self.option_set.add_option('ease', ['e'], int, 2, valid = (1, 2, 3, 4, 5),
194 | question = 'How easy should the graph be (return for 3)? ')
195 | self.option_set.add_option('from-zero', ['fz', 'f0'])
196 | self.option_set.add_group('gonzo', ['gz'], 'genus=8 nodes=23 ease=1')
197 |
198 | def set_up(self):
199 | """Set up the game. (None)"""
200 | method = 'from-zero' if self.from_zero else 'normalize'
201 | self.graph = DollarGraph(self.nodes, self.edges, self.total_value, method)
202 |
203 | class DollarGraph(object):
204 | """
205 | A graph with a dollar value for each node. (object)
206 |
207 | Attributes:
208 | edges: The neighbors of the nodes. (dict of str: list of str)
209 | nodes: The letter names of the nodes. (str)
210 | values: The dollar values of the nodes. (dict of str: int)
211 |
212 | Methods:
213 | donate: Donate money from a node to its neighbors. (None)
214 | random_graph: Generate a random set of edges. (None)
215 | random_values: Populate the values of the graph. (None)
216 | take: Take money from a node's neighbors. (None)
217 |
218 | Overridden Methods:
219 | __init__
220 | __str__
221 | """
222 |
223 | def __init__(self, nodes, edges, total_value, value_method = 'normalize'):
224 | """
225 | Set up the graph. (None)
226 |
227 | Parameters:
228 | nodes: The number of nodes in the graph. (int)
229 | edges: The number of edges in the graph. (int)
230 | total_value: The total of the values in the graph. (int)
231 | value_method: The method for calculating the random initial values. (str)
232 | """
233 | # Set the base attributes.
234 | self.nodes = (string.ascii_uppercase + string.ascii_lowercase)[:nodes]
235 | self.values = {char: 0 for char in self.nodes}
236 | self.edges = {char: [] for char in self.nodes}
237 | # Fill in the attributes.
238 | self.random_graph(nodes, edges)
239 | self.random_values(total_value, value_method)
240 |
241 | def __str__(self):
242 | """Human readable text representation. (str)"""
243 | text = ''
244 | for node in self.nodes:
245 | neighbors = []
246 | for neighbor in self.edges[node]:
247 | neighbors.append('{}: {}'.format(neighbor, self.values[neighbor]))
248 | text = '{}\n{}: {} ({})'.format(text, node, self.values[node], ', '.join(neighbors))
249 | return text
250 |
251 | def donate(self, node):
252 | """
253 | Donate money from a node to its neighbors. (None)
254 |
255 | Parameters:
256 | node: The node to donate from. (str)
257 | """
258 | self.values[node] -= len(self.edges[node])
259 | for neighbor in self.edges[node]:
260 | self.values[neighbor] += 1
261 |
262 | def random_graph(self, nodes, edges):
263 | """
264 | Generate a random set of edges. (None)
265 |
266 | Algorithm from David Bruce Wilson. It should generate every possible tree with
267 | equal probability.
268 |
269 | Parameters:
270 | nodes: The number of nodes in the graph. (int)
271 | edges: The number of edges in the graph. (int)
272 | """
273 | # Set up the edge detection loop.
274 | current = random.choice(self.nodes)
275 | found = set(current)
276 | found_edges = set()
277 | # Randomly walk the (fully connected) graph.
278 | # This generates a random spanning graph.
279 | while len(found) < len(self.nodes):
280 | new = random.choice(self.nodes)
281 | if new not in found:
282 | # When a node is visited the first time, add that edge.
283 | found.add(new)
284 | found_edges.add((current, new))
285 | found_edges.add((new, current))
286 | current = new
287 | # Add edges to the spanning graph until it has the required number of edges.
288 | while len(found_edges) < edges * 2:
289 | start, end = random.sample(self.nodes, 2)
290 | found_edges.add((start, end))
291 | found_edges.add((end, start))
292 | # Add the randomly calculated edges to the stored graph data.
293 | for start, end in found_edges:
294 | self.edges[start].append(end)
295 | # Sort the edges for clearer output.
296 | for node in self.nodes:
297 | self.edges[node].sort()
298 |
299 | def random_values(self, total_value, value_method):
300 | """
301 | Populate the values of the graph. (None)
302 |
303 | I assume there is bias to this method. I should look into a non-biassed
304 | method for generating the values, given the constraints of at least one
305 | negative value and no absolute values greater than the total value.
306 |
307 | Parameters:
308 | total_value: The defined total of the values in the graph. (int)
309 | value_method: The method for calculating the random initial values. (str)
310 | """
311 | if value_method == 'normalize':
312 | while True:
313 | # Generate random values in the range.
314 | values = [random.randint(-total_value, total_value) for node in self.nodes]
315 | # Adjust random values until the correct total value is reached.
316 | mod = 1 if sum(values) < total_value else -1
317 | while sum(values) != total_value:
318 | value_index = random.randrange(len(self.nodes))
319 | if abs(values[value_index]) < total_value:
320 | values[value_index] += mod
321 | # Exit if you have an unsolved position.
322 | if min(values) < 0:
323 | break
324 | elif value_method == 'from-zero':
325 | while True:
326 | # Start with all zeros.
327 | values = [0] * len(self.nodes)
328 | total = 0
329 | # Randomly add or subtract from a random node.
330 | while total != total_value:
331 | value_index = random.randrange(len(self.nodes))
332 | mod = random.choice((-1, 1, 1))
333 | values[value_index] += mod
334 | total += mod
335 | # Exit if you have an unsolved position.
336 | if min(values) < 0:
337 | break
338 | # Set the values.
339 | self.values = {char: value for char, value in zip(self.nodes, values)}
340 |
341 | def take(self, node):
342 | """
343 | Take money from a node's neighbors. (None)
344 |
345 | Parameters:
346 | node: The node that take the money. (str)
347 | """
348 | self.values[node] += len(self.edges[node])
349 | for neighbor in self.edges[node]:
350 | self.values[neighbor] -= 1
351 |
352 |
--------------------------------------------------------------------------------
/utility.py:
--------------------------------------------------------------------------------
1 | """
2 | Utility functions for tgames.
3 |
4 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
5 | See the top level __init__.py file for details on the t_games license.
6 |
7 | Constants:
8 | FIBONACCI: Fibonacci numbers under 200. (list of int)
9 | LOC: tgames location. (str)
10 | MAX_INT: The largest allowed integer. (int)
11 | NINETEEN: English words for 1-19. (list of str)
12 | ORDINAL_ENDS: Endings for numeric ordinals. (dict of int: str)
13 | ORDINALS: Conversion of cardinal numbers to ordinal numbers. (dict of str: str)
14 | PRIMES: All primes under 200. (list of int)
15 | TENS: English words for multiples of 10. (list of str)
16 | THOUSAND_UP: English words for powers of one thousand. (list of str)
17 | YES: Synonyms for 'yes'. (set of str)
18 |
19 | Functions:
20 | choose: Combinations [n choose r]. (int)
21 | flip: Returns a random bit. (int)
22 | hundred_word: Give the word form of a number less than 100. (str)
23 | levenshtein: Determine the Levenshtein distance between two strings. (int)
24 | mean: Calculate the mean of a list of values. (float)
25 | median: Calculate the median of a list of values. (float)
26 | num_text: Handle text instances of 'n foo'. (str)
27 | number_plural: Convert a number and word to two words with the plural. (str)
28 | number_word: Give the word form of a number. (str)
29 | oxford: Convert a sequence to a word list with an Oxford comma. (str)
30 | permutations: The number of permutations of n out r objects. (int)
31 | plural: Match the plural/singular form of the word to the number. (str)
32 | pow: Expnonentiation. (number)
33 | streaks: Calculates longest streaks for a sequence. (dict of float: int)
34 | thousand_word: Give the word form of a number less than 100. (str)
35 | """
36 |
37 |
38 | import collections
39 | import math
40 | import os
41 | import random
42 | import sys
43 |
44 |
45 | FIBONACCI = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144]
46 |
47 | LOC = os.path.dirname(os.path.abspath(__file__))
48 |
49 | try:
50 | MAX_INT = sys.maxint
51 | except AttributeError:
52 | MAX_INT = sys.maxsize
53 |
54 | NINETEEN = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
55 | 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen',
56 | 'nineteen']
57 |
58 | ORDINAL_ENDS = {1: 'st', 2: 'nd', 3: 'rd'} # usage: ORDINAL_ENDS.get(n % 10, 'th')
59 |
60 | ORDINALS = {'zero': 'zeroth', 'one': 'first', 'two': 'second', 'three': 'third', 'four': 'fourth',
61 | 'five': 'fifth', 'six': 'sixth', 'seven': 'seventh', 'eight': 'eighth', 'nine': 'ninth', 'ten': 'tenth',
62 | 'eleven': 'eleventh', 'twelve': 'twelfth', 'thirteen': 'thirteenth', 'fourteen': 'fourteenth',
63 | 'fifteen': 'fifteenth', 'sixteen': 'sixteenth', 'seventeen': 'seventeenth', 'eighteen': 'eighteenth',
64 | 'nineteen': 'nineteenth', 'twenty': 'twentieth', 'thirty': 'thirtieth', 'forty': 'fortieth',
65 | 'fifty': 'fiftieth', 'sixty': 'sixtieth', 'seventy': 'seventieth', 'eighty': 'eightieth',
66 | 'ninety': 'ninetieth', 'hundred': 'hundredth', 'thousand': 'thousandth', 'million': 'millionth',
67 | 'billion': 'billionth', 'trillion': 'trillionth', 'quadrillion': 'quadrillionth',
68 | 'quintillion': 'quintillionth', 'sextillion': 'sextillionth', 'septillion': 'septillionth',
69 | 'octillion': 'octillionth', 'nonillion': 'nonillionth', 'decillion': 'decillionth',
70 | 'undecillion': 'undecillionth', 'duodecillion': 'duodecillionth', 'tredecillion': 'tredecillionth',
71 | 'quatturodecillion': 'quatturodecillionth', 'quindecillion': 'quindecillionth',
72 | 'sexdecillion': 'sexdecillionth', 'octodecillion': 'octodecillionth', 'novemdecillion':
73 | 'novemdecillionth', 'vigintillion': 'vigintillionth'}
74 |
75 | PRIMES = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97,
76 | 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197,
77 | 199]
78 |
79 | TENS = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety']
80 |
81 | THOUSAND_UP = ['', 'thousand', 'million', 'billion', 'trillion', 'quadrillion', 'quintillion',
82 | 'sextillion', 'septillion', 'octillion', 'nonillion', 'decillion', 'undecillion', 'duodecillion',
83 | 'tredecillion', 'quatturodecillion', 'quindecillion', 'sexdecillion', 'octodecillion',
84 | 'novemdecillion', 'vigintillion']
85 |
86 | YES = set(['yes', 'y', '1', 'yup', 'sure', 'affirmative', 'yeah', 'indubitably', 'yep', 'aye', 'ok'])
87 | YES.update(['okay', 'darn tootin', 'roger', 'da', 'si'])
88 |
89 |
90 | def choose(n, r):
91 | """
92 | Combinations [n choose r]. (int)
93 |
94 | Parameters:
95 | n: The number of items to choose from. (int)
96 | r: The number of items to choose. (int)
97 | """
98 | return int(math.factorial(n) / (math.factorial(r) * math.factorial(n - r)))
99 |
100 |
101 | def flip():
102 | """Return a random bit. (int)"""
103 | return random.choice([1, 0])
104 |
105 |
106 | def hundred_word(n):
107 | """
108 | Give the word form of a number less than 100. (str)
109 |
110 | Parameter:
111 | n: A number to give the word form of. (int)
112 | """
113 | n %= 100
114 | # Don't use zero for compound words.
115 | if not n:
116 | return ''
117 | # Numbers under nineteen are predefined.
118 | elif n < 20:
119 | return NINETEEN[n]
120 | # Number over nineteen must be combined with tens place numbers.
121 | else:
122 | word = TENS[n // 10]
123 | if n % 10:
124 | word = '{}-{}'.format(word, NINETEEN[n % 10])
125 | return word
126 |
127 |
128 | def levenshtein(text_a, text_b):
129 | """
130 | Determine the Levenshtein distance between two strings. (int)
131 |
132 | Parameters:
133 | text_a: The first string. (str)
134 | text_b: The second string. (str)
135 | """
136 | # Initialize the matrix.
137 | matrix = [[0] * (len(text_a) + 1) for row in range(len(text_b) + 1)]
138 | matrix[0] = list(range(len(text_a) + 1))
139 | for y in range(len(text_b) + 1):
140 | matrix[y][0] = y
141 | # Fill the matrix of edits.
142 | for x in range(1, len(text_b) + 1):
143 | for y in range(1, len(text_a) + 1):
144 | base = [matrix[x - 1][y] + 1, matrix[x][y - 1] + 1]
145 | if text_b[x - 1] == text_a[y - 1]:
146 | base.append(matrix[x - 1][y - 1])
147 | else:
148 | base.append(matrix[x - 1][y - 1] + 1)
149 | matrix[x][y] = min(base)
150 | # Return the final value.
151 | return matrix[-1][-1]
152 |
153 |
154 | def mean(values):
155 | """
156 | Calculate the mean of a list. (float)
157 |
158 | Parameters:
159 | values: The list of values. (seq of float)
160 | """
161 | return sum(values) / float(len(values))
162 |
163 |
164 | def median(values):
165 | """
166 | Calculate the median of a list. (float)
167 |
168 | Parameters:
169 | values: The list of values. (seq of float)
170 | """
171 | if len(values) % 2:
172 | return sorted(values)[len(values) // 2]
173 | else:
174 | mid_point = len(values) // 2
175 | values = sorted(values)
176 | return sum(values[(mid_point - 1):(mid_point + 1)]) / 2.0
177 |
178 |
179 | def num_text(number, word = '', *args):
180 | """
181 | Handle text instances of 'n foo'. (str)
182 |
183 | If the word is empty, then only the number word is returned.
184 |
185 | The args parameters can be up to two modifiers. If a modifier does not start
186 | with a colon (:), it is the plural of word. If a plural is not specified this
187 | way, an s is just added to the end. If a modifier does start with a colon, it
188 | is a format type:
189 |
190 | :e -> add 'es' instead of 's' for the plural.
191 | :n -> force numbers to be numerals (numbers < 11 are converted to words by
192 | default).
193 | :o -> numbers are given as ordinals (ordinals default to words, use :no for
194 | '22nd'.
195 | :w -> force numbers to words (numbers > 10 are left as numerals by
196 | default).
197 |
198 | The format type can have more than one character after the colon. Note that
199 | ordinals do not make words plural. If word is an empty string, the word is
200 | converted and returned by itself.
201 |
202 | Parameters:
203 | number: The number of things. (int)
204 | word: The word for the things. (str)
205 | *args: Modifiers to the conversion as specified above. (str)
206 | """
207 | # Parse the modifiers.
208 | format_type = ''
209 | plural = '{}s'.format(word)
210 | for arg in args:
211 | if arg.startswith(':'):
212 | format_type = arg[1:].lower()
213 | else:
214 | plural = arg
215 | # Check for odd plurals.
216 | if 'e' in format_type:
217 | plural = '{}es'.format(word)
218 | if not word:
219 | plural = ''
220 | # Check for singleton.
221 | if number == 1:
222 | plural = word
223 | # Convert the number.
224 | wordify = (number < 11 or 'w' in format_type) and 'n' not in format_type
225 | if wordify:
226 | worded = number_word(number, ordinal = 'o' in format_type)
227 | else:
228 | worded = str(number)
229 | if 'o' in format_type:
230 | worded = '{}{}'.format(worded, ORDINAL_ENDS.get(number % 10, 'th'))
231 | # Undo plurals for ordinals.
232 | if 'o' in format_type:
233 | plural = word
234 | return '{} {}'.format(worded, plural).strip()
235 |
236 |
237 | def number_plural(number, singular, many = ''):
238 | """
239 | Convert the number to a word and get the right form of the word counted. (str)
240 |
241 | Parameters:
242 | number: The number determining plural or singular. (int)
243 | singular: The singular form of the word. (str)
244 | many: The plural form of the word, if not a simple + 's'. (str)
245 | """
246 | return '{} {}'.format(number_word(number), plural(number, singular, many))
247 |
248 |
249 | def number_word(n, ordinal = False):
250 | """
251 | Give the word form of a number. (str)
252 |
253 | Parameter:
254 | n: A number to give the word form of. (int)
255 | ordinal: A flag for returning an ordinal number. (bool)
256 | """
257 | # Handle zero.
258 | if not n:
259 | word = NINETEEN[n]
260 | else:
261 | # Loop thruogh powers of one thousand.
262 | word = ''
263 | level = 0
264 | while n:
265 | # Add the thousand word with the word for the power of one thousand.
266 | word = '{} {} {}'.format(thousand_word(n), THOUSAND_UP[level], word).strip()
267 | n //= 1000
268 | level += 1
269 | # Convert to an ordinal number if requested.
270 | if ordinal:
271 | words = word.split()
272 | if '-' in words[-1]:
273 | parts = words[-1].split('-')
274 | parts[-1] = ORDINALS[parts[-1]]
275 | words[-1] = '-'.join(parts)
276 | else:
277 | words[-1] = ORDINALS[words[-1]]
278 | word = ' '.join(words)
279 | return word
280 |
281 |
282 | def oxford(sequence, conjunction = 'and', word_format = '{}'):
283 | """
284 | Convert a sequence to a word list with an Oxford comma. (str)
285 |
286 | Parameters:
287 | sequence: The items to convert to words. (list)
288 | conjunction: The conjunction at the end of the list. (str)
289 | word_format: The format string syntax for each item. (str)
290 | """
291 | words = [word_format.format(item) for item in sequence]
292 | if not words:
293 | return ''
294 | elif len(words) == 1:
295 | return words[0]
296 | elif len(words) == 2:
297 | return '{1} {0} {2}'.format(conjunction, *words)
298 | else:
299 | return '{}, {} {}'.format(', '.join(words[:-1]), conjunction, words[-1])
300 |
301 |
302 | def permutations(n, r):
303 | """
304 | The number of permutations of r out of n objects. (int)
305 |
306 | Parameters:
307 | n: The number of objects to choose from. (int)
308 | r: The number of objects to permute. (int)
309 | """
310 | return int(math.factorial(n) / math.factorial(n - r))
311 |
312 |
313 | def plural(number, singular, many = ''):
314 | """
315 | Match the plural/singular form of the word to the number. (str)
316 |
317 | Parameters:
318 | number: The number determining plural or singular. (int)
319 | singular: The singular form of the word. (str)
320 | many: The plural form of the word, if not a simple + 's'. (str)
321 | """
322 | if number == 1:
323 | return singular
324 | elif many:
325 | return many
326 | else:
327 | return '{}s'.format(singular)
328 |
329 |
330 | def pow(x, y):
331 | """
332 | Expnonentiation. (number)
333 |
334 | This assumes x and y are literal numbers, and preserves order of operations.
335 |
336 | Parameters:
337 | x: The base. (number)
338 | y: The exponent. (number)
339 | """
340 | result = x ** y
341 | if x < 0:
342 | return -abs(result)
343 | else:
344 | return result
345 |
346 |
347 | def streaks(values):
348 | """
349 | Calculates longest streaks for a sequence. (dict of float: int)
350 |
351 | Parameters:
352 | values: The list of values. (seq of float)
353 | """
354 | if not values:
355 | return 0, 0, [0, 0, 0]
356 | # Prep the loop.
357 | previous = values[0]
358 | lengths = collections.defaultdict(int)
359 | length = 0
360 | # Calculate streaks
361 | for value in values:
362 | if value == previous:
363 | length += 1
364 | else:
365 | lengths[previous] = max(length, lengths[previous])
366 | length = 1
367 | previous = value
368 | # Record the last streak.
369 | lengths[value] = max(length, lengths[value])
370 | return length, value, lengths
371 |
372 |
373 | def thousand_word(n):
374 | """
375 | Give the word form of a number less than 1000. (str)
376 |
377 | Parameter:
378 | n: A number to give the word form of. (int)
379 | """
380 | # Force the word to be less than one thousand.
381 | n %= 1000
382 | # Handle less than one hunded.
383 | if n < 100:
384 | return hundred_word(n)
385 | # Handle above and below one hundred.
386 | elif n % 100:
387 | return '{} hundred {}'.format(NINETEEN[n // 100], hundred_word(n))
388 | # Handle no hundred words.
389 | else:
390 | return '{} hundred'.format(NINETEEN[n // 100])
391 |
392 |
393 | if __name__ == '__main__':
394 | # Run the unit testing.
395 | from t_tests.utility_test import *
396 | unittest.main()
397 |
--------------------------------------------------------------------------------
/other_games/number_guess_game.py:
--------------------------------------------------------------------------------
1 | """
2 | number_guess_game.py
3 |
4 | A classic number guessing game.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: The credits for the Number Guessing Game.
11 | OPTIONS: The options for the Number Guessing Game.
12 | RULES: The rules for the Number Guessing Game.
13 |
14 | Classes:
15 | GuessBot: A number guessing bot using a random strategy. (player.Bot)
16 | GuessBotter: A number guessing bot using a binary search. (GuessBot)
17 | NumberGuess: A classic number guessing game. (game.Game)
18 | """
19 |
20 |
21 | import random
22 |
23 | from .. import game
24 | from .. import player
25 | from .. import utility
26 |
27 |
28 | CREDITS = """
29 | Game Design: Traditional
30 | Game Programming: Craig "Ichabod" O'Brien
31 | """
32 |
33 | OPTIONS = """
34 | easy (e): Play against a random opponent.
35 | gonzo (gz): Equivalent to 'high=1001'.
36 | high= (h=): The highest possible secret number (defaults to 108).
37 | low= (l=): The lowest possible secret number (defaults to 1).
38 | """
39 |
40 | RULES = """
41 | Each turn you guess a number chosen in secret by the computer. Then the
42 | computer tries to guess a number you choose. Whoever guesses correctly with the
43 | least number of guesses wins.
44 | """
45 |
46 |
47 | class GuessBot(player.Bot):
48 | """
49 | A number guessing bot using a random strategy. (player.Bot)
50 |
51 | Attributes:
52 | high_guess: The highest guess the bot should make. (int)
53 | last_guess: The last guess the bot made. (int)
54 | low_guess: The lowest guess the bot should make. (int)
55 |
56 | Methods:
57 | guess: Guess a number.
58 | secret_number: Make a number to be guessed. (int)
59 |
60 | Overridden Methods:
61 | ask
62 | ask_int
63 | set_up
64 | tell
65 | """
66 |
67 | def ask(self, prompt):
68 | """
69 | Get information from the player. (str)
70 |
71 | Parameters:
72 | prompt: The question being asked of the player. (str)
73 | """
74 | if prompt.startswith('Guess a number between'):
75 | self.last_guess = self.guess()
76 | return str(self.last_guess)
77 |
78 | def ask_int(self, prompt, **kwargs):
79 | """
80 | Get an integer response from the human. (int)
81 |
82 | Parameters:
83 | prompt: The question asking for the interger. (str)
84 | kwargs: The standard arguments to ask_int
85 | """
86 | if 'secret number' in prompt:
87 | return self.secret_number(kwargs['low'], kwargs['high'])
88 | else:
89 | return super(GuessBot, self).ask_int(prompt, **kwargs)
90 |
91 | def guess(self):
92 | """Guess a number. (int)"""
93 | return random.randint(self.low_guess, self.high_guess)
94 |
95 | def secret_number(self, low, high):
96 | """
97 | Make a number to be guessed. (int)
98 |
99 | Parameters:
100 | low: The lowest possible secret number. (int)
101 | high: The highest possible secret number. (int)
102 | """
103 | return random.randint(low, high)
104 |
105 | def set_up(self):
106 | """Set up the bot. (None)"""
107 | self.low_guess = self.game.low
108 | self.high_guess = self.game.high
109 | self.last_guess = None
110 |
111 | def tell(self, *args, **kwargs):
112 | """
113 | Give information to the player. (None)
114 |
115 | Parameters:
116 | The parameters are as per the built-in print function.
117 | """
118 | if 'lower' in args[0]:
119 | self.low_guess = self.last_guess + 1
120 | elif 'higher' in args[0]:
121 | self.high_guess = self.last_guess - 1
122 | super(GuessBot, self).tell(*args, **kwargs)
123 |
124 |
125 | class GuessBotter(GuessBot):
126 | """
127 | A number guessing bot using a binary search. (GuessBot)
128 |
129 | Overridden Methods:
130 | guess
131 | """
132 |
133 | def guess(self):
134 | """Guess a number. (int)"""
135 | # Use a binary search.
136 | guess = (self.high_guess - self.low_guess + 1) // 2 + self.low_guess
137 | # Fudge it early on to avoid predictability.
138 | if self.high_guess - self.low_guess > 10:
139 | guess += random.choice((-1, 0, 1))
140 | return guess
141 |
142 | def secret_number(self, low, high):
143 | """
144 | Make a number to be guessed. (int)
145 |
146 | Parameters:
147 | low: The lowest possible secret number. (int)
148 | high: The highest possible secret number. (int)
149 | """
150 | # Start in the middle, with fudging to avoid predictability.
151 | width = high - low + 1
152 | start = width // 2 + random.choice((-1, 0, 1)) + low
153 | base = [low, start, high]
154 | # Run a binary search from there.
155 | while len(base) < width:
156 | next_row = []
157 | for first, second in zip(base, base[1:]):
158 | next_row.append((second - first + 1) // 2 + first)
159 | base = sorted(set(base + next_row))
160 | # Use of the last numbers that a binary search would find.
161 | return random.choice(next_row)
162 |
163 |
164 | class NumberGuess(game.Game):
165 | """
166 | A classic number guessing game. (game.Game)
167 |
168 | Attributes:
169 | easy: A flag for easier game play. (bool)
170 | high: The highest possible secret number. (int)
171 | high_low: A flag for showing high/low information. (bool)
172 | info: The type of information to show the user. (str)
173 | last_guess: The user's last guess. (int or None)
174 | low: The lowest possible secret number. (int)
175 | number: The secret number. (int)
176 | phase: Whether the human is guessing or answering. (str)
177 | warm_cold: A flag for showing warm/cold information. (bool)
178 |
179 | Class Attributes:
180 | info_types: The translation of information given to information flags. (dict)
181 |
182 | Methods:
183 | do_guess: Guess the secret number. (bool)
184 | reset: Reset guess tracking. (None)
185 |
186 | Overridden Methods:
187 | default
188 | handle_options
189 | player_action
190 | set_options
191 | set_up
192 | """
193 |
194 | aka = ['NuGG', 'Guess a Number']
195 | categories = ['Other Games']
196 | credits = CREDITS
197 | info_types = {'high-low': (True, False), 'warm-cold': (False, True), 'both': (True, True)}
198 | name = 'Number Guessing Game'
199 | num_options = 3
200 | options = OPTIONS
201 | rules = RULES
202 |
203 | def default(self, line):
204 | """
205 | Handle unrecognized commands. (bool)
206 |
207 | Parameters:
208 | text: The raw text input by the user. (str)
209 | """
210 | try:
211 | guess = int(line)
212 | except ValueError:
213 | return super(NumberGuess, self).default(line)
214 | else:
215 | return self.do_guess(line)
216 |
217 | def do_gipf(self, arguments):
218 | """
219 | Canfield makes the game 'forget' your last guess.
220 |
221 | Ninety-nine tells you the remainder after dividing the secret number by 9.
222 | """
223 | # Run the edge, if possible.
224 | game, losses = self.gipf_check(arguments, ('canfield', 'ninety-nine'))
225 | # Winning Canfield gets you a free guess.
226 | if game == 'canfield':
227 | if not losses:
228 | self.guesses -= 1
229 | # Winning 99 gets you the secret number mod 9.
230 | elif game == 'ninety-nine':
231 | if not losses:
232 | self.human.tell('\nThe secret number modulo 9 is {}.'.format(self.number % 9))
233 | # Otherwise I'm confused.
234 | else:
235 | self.human.tell("\nGipf is inside the innermost possible secret number.")
236 | return True
237 | self.human.tell('\nYour last guess was {}.'.format(self.last_guess))
238 | if self.last_guess < self.number:
239 | self.human.tell('That was lower than the secret number.')
240 | else:
241 | self.human.tell('That was higher than the secret number.')
242 | return True
243 |
244 | def do_guess(self, arguments):
245 | """
246 | Guess the secret number. (g)
247 | """
248 | player = self.players[self.player_index]
249 | # Check for integer input.
250 | try:
251 | guess = int(arguments)
252 | except ValueError:
253 | player.error('{!r} is not a valid integer.'.format(arguments))
254 | else:
255 | # Check that the input is in range.
256 | if guess < self.low:
257 | player.error('{} is below the lowest possible secret number.'.format(guess))
258 | elif guess > self.high:
259 | player.error('{} is above the highest possible secret number.'.format(guess))
260 | else:
261 | self.guesses += 1
262 | # Check the input against the secret number.
263 | if guess == self.number:
264 | text = '{} is the secret number! You got it in {} guesses.'
265 | player.tell(text.format(guess, self.guesses))
266 | self.scores[player.name] = self.guesses
267 | self.reset()
268 | return False
269 | # Give high/low information, if allowed.
270 | if self.high_low:
271 | if guess < self.number:
272 | player.tell('{} is lower than the secret number.'.format(guess))
273 | elif guess > self.number:
274 | player.tell('{} is higher than the secret number.'.format(guess))
275 | # Give warm/cold information, if allowed.
276 | if self.warm_cold:
277 | if self.last_guess is None:
278 | # Give initial distance as a temperature.
279 | close = abs(guess - self.number) / (self.high - self.low + 1.0)
280 | if close < 0.05:
281 | player.tell('You are hot.')
282 | elif close < 0.25:
283 | player.tell('You are warm.')
284 | elif close < 0.95:
285 | player.tell('You are cold.')
286 | else:
287 | player.tell('You are frozen.')
288 | else:
289 | # Give warmer/colder information.
290 | distance = abs(guess - self.number)
291 | last_distance = abs(self.last_guess - self.number)
292 | if distance < last_distance:
293 | player.tell('Warmer.')
294 | elif distance > last_distance:
295 | player.tell('Colder.')
296 | else:
297 | player.tell('No change in temperature.')
298 | self.last_guess = guess
299 | return True
300 |
301 | def game_over(self):
302 | """Determine if the game is finished or not. (bool)"""
303 | # Finish after each person guesses correctly.
304 | if self.turns == 2:
305 | if self.scores[self.human.name] < self.scores[self.bot.name]:
306 | text = 'You won, {0} guesses to {1}.'
307 | self.win_loss_draw[0] = 1
308 | elif self.scores[self.human.name] > self.scores[self.bot.name]:
309 | text = 'You lost, {0} guesses to {1}.'
310 | self.win_loss_draw[1] = 1
311 | else:
312 | text = 'It was a tie, with {0} guesses each.'
313 | self.win_loss_draw[2] = 1
314 | self.human.tell(text.format(self.scores[self.human.name], self.scores[self.bot.name]))
315 | return True
316 | else:
317 | return False
318 |
319 | def handle_options(self):
320 | """Process the option settings for the game. (None)"""
321 | super(NumberGuess, self).handle_options()
322 | # Check the range settings.
323 | if self.high == self.low:
324 | self.human.tell('\nThe high option must be different than the low option.')
325 | self.option_set.errors.append('Invalid range specified.')
326 | elif self.high < self.low:
327 | self.human.tell('\nThe range was entered backward and has been reversed.')
328 | self.low, self.high = self.high, self.low
329 | # Set the computer opponent.
330 | if self.easy:
331 | self.bot = GuessBot(taken_names = [self.human.name])
332 | else:
333 | self.bot = GuessBotter(taken_names = [self.human.name])
334 | self.players = [self.human, self.bot]
335 | # Set the information flags.
336 | #self.high_low, self.warm_cold = self.info_types[self.info]
337 | self.high_low, self.warm_cold = True, False
338 |
339 | def player_action(self, player):
340 | """
341 | Handle a player's turn or other player actions. (bool)
342 |
343 | Parameters:
344 | player: The player whose turn it is. (Player)
345 | """
346 | # Give the number of guesses.
347 | guess_text = utility.number_plural(self.guesses, 'guess', 'guesses')
348 | player.tell('\nYou have made {} so far.'.format(guess_text))
349 | # Get the secret number, if it hasn't been defined yet.
350 | if self.number is None:
351 | foe = self.players[1 - self.player_index]
352 | query = 'What do you want the secret number to be? '
353 | self.number = foe.ask_int(query, low = self.low, high = self.high, cmd = False)
354 | # Handle the player's move.
355 | query = 'Guess a number between {} and {}: '.format(self.low, self.high)
356 | return self.handle_cmd(player.ask(query))
357 |
358 | def reset(self):
359 | """Reset guess tracking. (None)"""
360 | self.number = None
361 | self.last_guess = None
362 | self.guesses = 0
363 |
364 | def set_options(self):
365 | """Set the options for the game. (None)"""
366 | self.option_set.add_option('easy', ['e'], question = 'Do you want to play in easy mode? bool')
367 | """self.option_set.add_option('info', ['i'], valid = ('high-low', 'warm-cold', 'both'),
368 | question = 'What information should you get (high-low (default), warm-cold, or both)? ')"""
369 | self.option_set.add_option('low', ['l'], int, 1,
370 | question = 'What should the lowest possible number be (return for 1)? ')
371 | self.option_set.add_option('high', ['h'], int, 108,
372 | question = 'What should the highest possible number be (return for 108)? ')
373 | self.option_set.add_group('gonzo', ['gz'], 'high=1001')
374 |
375 | def set_up(self):
376 | """Set up the game. (None)"""
377 | self.reset()
378 |
--------------------------------------------------------------------------------
/other_games/rps_game.py:
--------------------------------------------------------------------------------
1 | """
2 | rps_game.py
3 |
4 | Rock-paper-scissors.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: Credits for Rock-Paper-Scissors. (str)
11 | OPTIONS: Options for Rock-Paper-Scissors. (str)
12 | RULES: Rules for Rock-Paper-Scissors. (str)
13 |
14 | Classes:
15 | Bart: Good old rock. Nothing beats rock. (Bot)
16 | Memor: An RPS bot with a memory. (Bot)
17 | Randy: A random RPS bot. (Bot)
18 | Lisa: An anti-Bart bot. (Ramdy)
19 | RPS: A game of rock-paper-scissors. (Game)
20 | """
21 |
22 |
23 | import bisect
24 | import os
25 | import random
26 |
27 | from .. import game
28 | from .. import options
29 | from .. import player
30 | from .. import utility
31 |
32 |
33 | CREDITS = """
34 | Game Design: Traditional
35 | Game Programming: Craig "Ichabod" O'Brien
36 | Special Thanks: Matt Groening
37 | """
38 |
39 | OPTIONS = """
40 | bot= (b=): The bot you will play against. The valid bots are Bart (b), Lisa
41 | (l), Memor (m), and Randy (r). Defaults to Memor.
42 | gonzo (gz): Equivalent to 'bot=randy lizard-spock match=23'.
43 | lizard-spock (ls): Add the lizard and Spock moves.
44 | match= (m=): The number of rounds played. Defaults to 3.
45 | """
46 |
47 | RULES = """
48 | Each player chooses one of rock (r), paper (p), or scissors (s). Rock beats
49 | scissors, paper beats rock, and scissors beats paper. If players choose the
50 | same thing, both players choose again.
51 |
52 | If the lizard-spock option is chosen, players may also choose lizard (l) or
53 | Spock (sp). Lizard beats paper and Spock and loses to rock and scissors. Spock
54 | beats scissors and rock and loses to paper and lizard.
55 |
56 | The bots you can play against are Bart ('Good old rock, nothing beats rock.'),
57 | Lisa ('Poor Bart, always plays rock'), Memor (he remembers what you've played),
58 | and Randy (he's a bit unpredictable).
59 | """
60 |
61 |
62 | class Bart(player.Bot):
63 | """
64 | Good old rock. Nothing beats rock. (Bot)
65 |
66 | Overridden Methods:
67 | __init__
68 | ask
69 | tell
70 | """
71 |
72 | def __init__(self, taken_names):
73 | """
74 | Set up the bot. (None)
75 |
76 | Parameters:
77 | taken_names: The names already taken. (list of str)
78 | """
79 | super(Bart, self).__init__(taken_names, initial = 'b')
80 |
81 | def ask(self, prompt):
82 | """
83 | Get information from the bot. (str)
84 |
85 | Parameters:
86 | prompt: The question asked of Bart. (str)
87 | """
88 | # Play rock.
89 | if prompt == '\nWhat is your move? ':
90 | return 'rock'
91 | else:
92 | super(Bart, self).ask(prompt)
93 |
94 | def tell(self, text):
95 | pass
96 |
97 |
98 | class Memor(player.Bot):
99 | """
100 | An RPS bot with a memory. (Bot)
101 |
102 | Attributes:
103 | file_path: The location of any stored data for the bot. (str)
104 | losses: The responses that will lose to a given move. (str)
105 | memory: The record of plays against the bot. (str)
106 |
107 | Methods:
108 | load_data: Load stored data of previous plays. (None)
109 |
110 | Overridden Methods:
111 | __init__
112 | ask
113 | clean_up
114 | tell
115 | """
116 |
117 | def __init__(self, taken_names):
118 | """
119 | Set up the bot. (None)
120 |
121 | Parameters:
122 | taken_names: The names already taken. (list of str)
123 | """
124 | # Do the standard initialization.
125 | super(Memor, self).__init__(taken_names, initial = 'm')
126 | # Set up the memory attributes.
127 | self.file_path = 'rps_memor_data{}.txt'
128 | self.losses = {}
129 | self.memory = {}
130 |
131 | def ask(self, prompt):
132 | """
133 | Get information from the bot. (str)
134 |
135 | Parameters:
136 | prompt: The question asked of the bot. (str)
137 | """
138 | # Make sure you have data.
139 | if not self.memory:
140 | self.set_up()
141 | if prompt == '\nWhat is your move? ':
142 | # Get the cumulative frequency of moves.
143 | moves, counts = zip(*self.memory.items())
144 | cum = [sum(counts[:index]) for index in range(1, len(moves) + 1)]
145 | # Make a weighted random guess of the next player's move.
146 | choice = random.random() * cum[-1]
147 | guess = moves[bisect.bisect(cum, choice)]
148 | # Play something that move loses to.
149 | return random.choice(self.losses[guess])
150 | else:
151 | super(Memor, self).ask(prompt)
152 |
153 | def clean_up(self):
154 | """Garbage collect the instance. (None)"""
155 | # Save any stored move data.
156 | if '{}' not in self.file_path:
157 | with open(self.file_path, 'w') as data_file:
158 | for move, count in self.memory.items():
159 | data_file.write('{}:{}\n'.format(move, count))
160 |
161 | def load_data(self):
162 | """Load stored data of previous plays. (None)"""
163 | with open(self.file_path) as data_file:
164 | for line in data_file:
165 | key, value = line.split(':')
166 | if key in self.memory:
167 | self.memory[key] = int(value)
168 |
169 | def set_up(self):
170 | """Set up the bot. (None)"""
171 | # Store lizard-spock data separately.
172 | data_tag = ''
173 | if self.game.wins == self.game.lizard_spock:
174 | data_tag = '_ls'
175 | # Seed the initial memory.
176 | self.memory = {move: 1 for move in self.game.wins}
177 | # Check for a true human opponent.
178 | if hasattr(self.game.human, 'folder_name'):
179 | # Load any data stored for a human's plays.
180 | self.file_path = os.path.join(self.game.human.folder_name, self.file_path.format(data_tag))
181 | if os.path.exists(self.file_path):
182 | self.load_data()
183 | # Get a reverse wins dictionary.
184 | self.losses = {move: [] for move in self.game.wins}
185 | for move, beats in self.game.wins.items():
186 | for loss in beats:
187 | self.losses[loss].append(move)
188 |
189 | def tell(self, text):
190 | """
191 | Give information to the bot. (None)
192 |
193 | Parameters:
194 | text: The information given to the bot. (str)
195 | """
196 | if text in self.memory:
197 | self.memory[text] += 1
198 |
199 |
200 | class Randy(player.Bot):
201 | """
202 | A random RPS bot. (Bot)
203 |
204 | Overridden Methods:
205 | __init__
206 | ask
207 | tell
208 | """
209 |
210 | def __init__(self, taken_names, initial = 'r'):
211 | """
212 | Set up the bot. (None)
213 |
214 | Parameters:
215 | taken_names: The names already taken. (list of str)
216 | """
217 | super(Randy, self).__init__(taken_names, initial)
218 |
219 | def ask(self, prompt):
220 | """
221 | Get information from the bot. (str)
222 |
223 | Parameters:
224 | prompt: The question asked of the bot. (str)
225 | """
226 | # Make a completely random move.
227 | if prompt == '\nWhat is your move? ':
228 | return random.choice(list(self.game.wins.keys()))
229 | else:
230 | super(Randy, self).ask(prompt)
231 |
232 | def tell(self, text):
233 | """
234 | Give information to the bot. (None)
235 |
236 | Parameters:
237 | text: The information given to the bot. (str)
238 | """
239 | pass
240 |
241 |
242 | class Lisa(Randy):
243 | """
244 | An anti-Bart bot. (Bot)
245 |
246 | Attributes:
247 | last_attack: The last move made by Lisa's opponent. (str)
248 |
249 | Overridden Methods:
250 | __init__
251 | ask
252 | tell
253 | """
254 |
255 | def __init__(self, taken_names):
256 | """
257 | Set up the bot. (None)
258 |
259 | Parameters:
260 | taken_names: The names already taken. (list of str)
261 | """
262 | super(Lisa, self).__init__(taken_names, initial = 'l')
263 | self.last_attack = ''
264 |
265 | def ask(self, prompt):
266 | """
267 | Get information from the bot. (str)
268 |
269 | Parameters:
270 | prompt: The question asked of the bot. (str)
271 | """
272 | if prompt == '\nWhat is your move? ' and self.last_attack == 'rock':
273 | # Assume rock means Bart.
274 | return 'paper'
275 | else:
276 | # Otherwise make a random move.
277 | return super(Lisa, self).ask(prompt)
278 |
279 | def tell(self, text):
280 | """
281 | Give information to the bot. (None)
282 |
283 | Parameters:
284 | text: The information given to the bot. (str)
285 | """
286 | if text in self.game.wins:
287 | self.last_attack = text
288 |
289 |
290 | class RPS(game.Game):
291 | """
292 | A game of rock-paper-scissors. (Game)
293 |
294 | Class Attributes:
295 | bot_classes: The bots available as options for play. (dict of str: Bot)
296 | lizard_spock: A wins attribute for the lizard-spock option. (dict)
297 | move_aliases: Abbreviations for the available moves. (dict)
298 | wins: What each move beats. (dict of str: list of str)
299 |
300 | Attributes:
301 | bot: The non-human player. (player.Bot)
302 | bot_cls: The name of the bot class. (str)
303 | lizard_spock: A flag for including the lizard and Spock moves. (bool)
304 | loss_draw: A flag for the next loss counting as a draw. (bool)
305 | match: The number of games in a match. (int)
306 | moves: The moves made keyed to the player's names. (dict of str: str)
307 |
308 | Overridden Methods:
309 | game_over
310 | handle_options
311 | player_action
312 | set_options
313 | set_up
314 | """
315 |
316 | aka = ['RPS', 'Rock Paper Scissors', 'Roshambo']
317 | bot_classes = {'b': Bart, 'bart': Bart, 'l': Lisa, 'lisa': Lisa, 'm': Memor, 'memor': Memor,
318 | 'r': Randy, 'randy': Randy}
319 | categories = ['Other Games']
320 | credits = CREDITS
321 | lizard_spock = {'rock': ['scissors', 'lizard'], 'scissors': ['paper', 'lizard'],
322 | 'paper': ['rock', 'spock'], 'lizard': ['paper', 'spock'], 'spock': ['scissors', 'rock']}
323 | move_aliases = {'r': 'rock', 'p': 'paper', 's': 'scissors', 'l': 'lizard', 'sp': 'spock'}
324 | name = 'Rock-Paper-Scissors'
325 | num_options = 3
326 | options = OPTIONS
327 | rules = RULES
328 | wins = {'rock': ['scissors'], 'scissors': ['paper'], 'paper': ['rock']}
329 |
330 | def do_gipf(self, arguments):
331 | """
332 | Forty Thieves makes your next loss a draw.
333 |
334 | Slider Puzzle makes your next draw a win.
335 | """
336 | game, losses = self.gipf_check(arguments, ('forty thieves', 'slider puzzle'))
337 | go = True
338 | # Forty Thieves turns the next loss into a draw.
339 | if game == 'forty thieves':
340 | if not losses:
341 | self.loss_draw = True
342 | self.human.tell('\nYour next loss will be a draw.')
343 | # Slider Puzzle turns the next draw into a win.
344 | elif game == 'slider puzzle':
345 | if not losses:
346 | self.draw_win = True
347 | self.human.tell('\nYour next draw will be a win.')
348 | # Otherwise I'm confused.
349 | else:
350 | self.human.tell('No thank you.')
351 | return go
352 |
353 | def game_over(self):
354 | """Check for the end of the game. (bool)"""
355 | # Only check if both players have moved.
356 | if not self.turns % 2:
357 | move = self.moves[self.human]
358 | bot_move = self.moves[self.bot]
359 | # Check for a bot win.
360 | if move in self.wins[bot_move] and not self.loss_draw:
361 | self.human.tell('{} beats {}, you lose.'.format(bot_move, move))
362 | self.win_loss_draw[1] += 1
363 | # Check for a human win.
364 | elif bot_move in self.wins[move] or (self.draw_win and move == bot_move):
365 | if move == bot_move:
366 | self.human.tell('You should have drawn with {}, but you win.'.format(move))
367 | self.draw_win = False
368 | else:
369 | self.human.tell('{} beats {}, you win!'.format(move, bot_move))
370 | self.win_loss_draw[0] += 1
371 | # Otherwise assume a tie.
372 | else:
373 | if move == bot_move:
374 | self.human.tell('You both played {}, play again.'.format(move))
375 | else:
376 | self.human.tell('You should have lost to {}, but you drew.'.format(bot_move))
377 | self.loss_draw = False
378 | self.win_loss_draw[2] += 1
379 | # Update the players.
380 | self.human.tell('The score is now {}-{}-{}.'.format(*self.win_loss_draw))
381 | self.bot.tell(move)
382 | if sum(self.win_loss_draw[:2]) == self.match:
383 | if self.match > 1:
384 | if self.win_loss_draw[0] > self.win_loss_draw[1]:
385 | result = 'won'
386 | elif self.win_loss_draw[1] > self.win_loss_draw[0]:
387 | result = 'lost'
388 | else:
389 | result = 'drew'
390 | self.human.tell('\nYou {} the match, {}-{}-{}.'.format(result, *self.win_loss_draw))
391 | return True
392 | else:
393 | return False
394 |
395 | def handle_options(self):
396 | """Handle any game options. (None)"""
397 | # Parse the options.
398 | super(RPS, self).handle_options()
399 | # Set the players.
400 | self.bot = self.bot_classes[self.bot_cls]([self.human])
401 | self.players = [self.human, self.bot]
402 | # Set match play flag.
403 | if self.match > 1:
404 | self.flags |= 256
405 |
406 | def player_action(self, player):
407 | """
408 | Handle a player's turn or other player actions. (bool)
409 |
410 | Parameters:
411 | player: The player whose turn it is. (Player)
412 | """
413 | move = player.ask('\nWhat is your move? ').lower()
414 | move = self.move_aliases.get(move, move)
415 | # Process game moves.
416 | if move in self.wins:
417 | self.moves[player] = move
418 | # Process other commands.
419 | else:
420 | return self.handle_cmd(move)
421 |
422 | def set_options(self):
423 | """Define the options for the game. (None)"""
424 | # Set the bot options.
425 | self.option_set.default_bots = [(Memor, ())]
426 | self.option_set.add_option('bot', ['b'], converter = options.lower, default = 'memor',
427 | target = 'bot_cls', valid = ('b', 'bart', 'l', 'lisa', 'm', 'memor', 'r', 'randy'),
428 | question = 'Which bot would you like to play against? ',
429 | error_text = 'The valid bots are Bart, Lisa, Memor, and Randy.')
430 | # Set the play options.
431 | self.option_set.add_option('lizard-spock', ['ls'], target = 'wins', value = self.lizard_spock,
432 | default = None, question = 'Would you like to play with lizard and Spock? bool')
433 | self.option_set.add_option('match', ['m'], int, default = 3, check = lambda x: x > 0,
434 | question = 'How many games should there be in the match? (return for 3)? ')
435 | # Set the option groups.
436 | self.option_set.add_group('gonzo', ['gz'], 'bot=randy lizard-spock match=23')
437 |
438 | def set_up(self):
439 | """Set up the game. (None)"""
440 | # Set the tracking variables.
441 | self.moves = {player.name: '' for player in self.players}
442 | self.loss_draw = False
443 | self.draw_win = False
444 |
--------------------------------------------------------------------------------
/other_games/hangman_game.py:
--------------------------------------------------------------------------------
1 | """
2 | hangman_game.py
3 |
4 | A game of hangman.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | BODY_PARTS: The symbols for the parts of the hanging body, in order. (str)
11 | CREDITS: The credits for Hangman. (str)
12 | DIAGRAM: The format method ready diagram of the hanging body. (str)
13 | NUMBERS: Digits for the guess. (str)
14 | OPTIONS: The options for Hangman. (str)
15 | RULES: The rules to Hangman. (str)
16 |
17 | Classes:
18 | Hangman: A game of Hangman. (game.Game)
19 | """
20 |
21 |
22 | import collections
23 | import os
24 | import random
25 | import re
26 |
27 | from .. import game
28 | from .. import utility
29 |
30 |
31 | BODY_PARTS = 'O|/\\/\\'
32 |
33 | CREDITS = """
34 | Game Design: Traditional
35 | Game Programming: Craig "Ichabod" O'Brien
36 | """
37 |
38 | DIAGRAM = """
39 | +---+
40 | | |
41 | {0} |
42 | {2}{1}{3} |
43 | {4} {5} |
44 | |
45 | +----+
46 | """
47 |
48 | NUMBERS = '1234567890' * 3
49 |
50 | OPTIONS = """
51 | difficulty= (d=): The difficulty of the game, from 1 to 10 (default = 5).
52 | gonzo (gz): Equivalent to 'difficulty=10'.
53 | status (s): See the status of the computer's thinking.
54 | """
55 |
56 | RULES = """
57 | Hangman is a word guessing game. Each player takes a turn thinking of a secret
58 | word that the other player must guess letter by letter. To start with, you only
59 | know how many letters are in the word. For each correct guess, you learn how
60 | many times the letter is in the word, and where exactly in the word it is. For
61 | each incorrect letter guessed, another body part is added to the hanging man:
62 | head, torso, arm, arm, leg, leg. If all six body part are added, you fail. If
63 | one player can guess their word with fewer errors than the other, they win the
64 | game. Otherwise, the game is a draw.
65 |
66 | COMMANDS:
67 | frequency (freq): Get a frequency list of letters in dictionary words.
68 | guess: Guess the whole word. An incorrect word earns a body part.
69 | """
70 |
71 |
72 | class Hangman(game.Game):
73 | """
74 | A game of Hangman. (game.Game)
75 |
76 | Attributes:
77 | difficulty: A level of difficulty. (int)
78 | foul: A flag for suspected cheating. (bool)
79 | frequency: The frequency order of letters in the words. (str)
80 | guess: The current guess, with blanks. (str)
81 | guessed_letters: The letters guessed so far. (str)
82 | incorrect: The number of incorrect guesses so far. (int)
83 | my_score: The computer's score/the number of incorrect human guesses. (int)
84 | phase: Is the player answering guesses or guessing? (str)
85 | possibles: Words that match information given by the human. (list of str)
86 | rank_dict: The frequency rank for letters in the dictionary words. (dict)
87 | scored_words: The words for each difficulty level. (dict of int: list)
88 | status: A flag for showing the computer's thinking. (bool)
89 | vowels: The frequency order of vowels in the words. (str)
90 | word: The word the player is trying to guess. (str)
91 | word_length: The length of the word being guessed. (int)
92 | words: The known words. (list of str)
93 |
94 | Methods:
95 | do_frequency: Show the frequency list. (bool)
96 | do_guess: Guess what the word is. (bool)
97 | get_word: Get a word for the human to guess. (None)
98 | player_answer: Handle the player answering if letters are correct. (bool)
99 | player_guess: Handle the player guessing if letters are correct. (bool)
100 | score_word: Estimate the difficulty of a word. (int)
101 |
102 | Overridden Methods:
103 | __str__
104 | game_over
105 | player_action
106 | set_options
107 | set_up
108 | """
109 |
110 | aka = ['Hang']
111 | aliases = {'freq': 'frequency'}
112 | categories = ['Other Games']
113 | credits = CREDITS
114 | name = 'Hangman'
115 | num_options = 2
116 | options = OPTIONS
117 | rules = RULES
118 |
119 | def __str__(self):
120 | """Generate a human readable text representation. (str)"""
121 | # Generate the hanging man diagram.
122 | body_parts = BODY_PARTS[:self.incorrect] + ' ' * (6 - self.incorrect)
123 | text = DIAGRAM.format(*tuple(body_parts))
124 | # Add the word so far.
125 | if self.guess:
126 | text += '\n{}\n'.format(self.guess)
127 | text += '{}\n'.format(NUMBERS[:self.word_length])
128 | # Add the guessed letters.
129 | if self.guessed_letters:
130 | text += '\nGuessed: {}\n'.format(self.guessed_letters)
131 | return text
132 |
133 | def do_frequency(self, argument):
134 | """
135 | Show the frequency list. (freq)
136 |
137 | This is the order of the letter by their frequency in all of the words in the
138 | full word list.
139 | """
140 | self.human.tell('\n{}\n'.format(self.frequency))
141 | return True
142 |
143 | def do_gipf(self, arguments):
144 | """
145 | Craps gives you a random letter.
146 | """
147 | game, losses = self.gipf_check(arguments, ('craps',))
148 | go = True
149 | # A win at Craps gives you a random letter.
150 | if game == 'craps':
151 | if not losses:
152 | un_guessed = [letter for letter in set(self.word) if letter not in self.guessed_letters]
153 | bonus = random.choice(un_guessed)
154 | self.human.tell('\nYour bonus letter is {!r}.'.format(bonus))
155 | for letter_index, letter in enumerate(self.word):
156 | if letter == bonus:
157 | self.guess = self.guess[:letter_index] + letter + self.guess[letter_index + 1:]
158 | # Respond to any other games.
159 | else:
160 | self.human.tell('Tamsk.')
161 |
162 | def do_guess(self, argument):
163 | """
164 | Guess what the word is.
165 |
166 | An incorrect guess gets you another body part.
167 | """
168 | if self.word == argument.lower():
169 | # Record a correct guess.
170 | self.guess = self.word
171 | self.human.tell('You guessed wisely!')
172 | else:
173 | # Give a body part for an incorrect guess.
174 | self.incorrect += 1
175 | self.human.tell('You guessed poorly.')
176 |
177 | def game_over(self):
178 | """Check for end of game. (bool)"""
179 | # Check for end state.
180 | if self.incorrect == 6 or '_' not in self.guess:
181 | # If the computer is done, set things up for the human.
182 | if self.phase == 'answer':
183 | self.my_score = self.incorrect
184 | self.phase = 'guess'
185 | self.guessed_letters = ''
186 | self.incorrect = 0
187 | self.word_length = 0
188 | # If the human is done, figure out who did best.
189 | else:
190 | # Tell the human the word if they failed.
191 | if self.incorrect == 6:
192 | self.human.tell('The word was {!r}.\n'.format(self.word))
193 | # Display the scores.
194 | self.scores[self.human.name] = self.incorrect
195 | self.human.tell('\nYou had {} errors, I had {}.'.format(self.incorrect, self.my_score))
196 | # Calculate the winner.
197 | if self.incorrect < self.my_score:
198 | # Account for the human using unknown words.
199 | if self.foul:
200 | self.human.tell("It's a draw because you cheated.")
201 | self.win_loss_draw[1] = 1
202 | else:
203 | self.human.tell('You win!')
204 | self.win_loss_draw[0] = 1
205 | elif self.incorrect > self.my_score:
206 | self.human.tell('You lose.')
207 | self.win_loss_draw[1] = 1
208 | else:
209 | self.human.tell("Alright, let's call it a draw.")
210 | self.win_loss_draw[2] = 1
211 | return True
212 | return False
213 |
214 | def get_word(self):
215 | """Get a word for the human to guess. (None)"""
216 | # Get the human's word.
217 | if '_' in self.guess:
218 | word = self.human.ask('What was the word? ').lower()
219 | else:
220 | word = self.guess
221 | if word not in self.words:
222 | self.human.tell('I cry foul. That word is not in my dictionary.')
223 | # Find a word of similar difficulty.
224 | level = self.score_word(word) - 10 + self.difficulty
225 | while level not in self.scored_words:
226 | level += 1
227 | self.word = random.choice(self.scored_words[level])
228 | # Set the word-dependent tracking variables.
229 | self.word_length = len(self.word)
230 | self.guess = '_' * self.word_length
231 |
232 | def player_action(self, player):
233 | """
234 | Handle player actions. (bool)
235 |
236 | Parameters:
237 | player: The player whose turn it is. (player.Player)
238 | """
239 | if self.phase == 'answer':
240 | # Start computer guessing.
241 | if not self.word:
242 | query = '\nThink of a word for me to guess. How many letters are in it? '
243 | word_length = player.ask_int(query, low = 1)
244 | if isinstance(word_length, int):
245 | self.word_length = word_length
246 | self.guess = '_' * self.word_length
247 | self.word = '???'
248 | self.possibles = [word for word in self.words if len(word) == self.word_length]
249 | else:
250 | return self.handle_cmd(word_length)
251 | # Handle computer guessing.
252 | self.human.tell(self)
253 | return self.player_answer()
254 | else:
255 | # Start human guessing.
256 | if not self.word_length:
257 | self.get_word()
258 | # Handle human guessing.
259 | self.human.tell(self)
260 | return self.player_guess()
261 |
262 | def player_answer(self):
263 | """Handle the player answering if letters are correct. (bool)"""
264 | # Update the human on the computer's status.
265 | if self.status:
266 | count = len(self.possibles)
267 | if count:
268 | self.human.tell('I have narrowed it down to {} words.'.format(count))
269 | else:
270 | self.human.tell('I think you are cheating.')
271 | if len(self.possibles) == 1:
272 | # Guess the only possible word.
273 | correct = self.human.ask('Is the word {!r}? '.format(self.possibles[0])).lower()
274 | if correct in utility.YES:
275 | self.guess = self.possibles[0]
276 | else:
277 | self.incorrect += 1
278 | self.possibles = []
279 | return False
280 | elif self.possibles:
281 | # Get the frequency of unknown letters in possible words.
282 | sub_freq = collections.Counter()
283 | valid = []
284 | for char_index, char in enumerate(self.guess):
285 | if char == '_':
286 | sub_freq += collections.Counter(word[char_index] for word in self.possibles)
287 | valid.append(char_index + 1)
288 | # Guess the most freuent unknown letter in the possible words.
289 | guess = sub_freq.most_common(1)[0][0]
290 | else:
291 | # If the word isn't in the dictionary, guess by overall frequency.
292 | self.foul = True
293 | for guess in self.frequency:
294 | if guess not in self.guessed_letters:
295 | break
296 | valid = [index + 1 for index, letter in enumerate(self.guess) if letter == '_']
297 | query = '\nI guess {0!r}. Please enter the indexes where {0!r} occurs in the word: '.format(guess)
298 | matches = self.human.ask_int_list(query, valid = valid, cmd = False, valid_lens = range(10))
299 | self.guessed_letters += guess
300 | # Readjust possibles based on the results of the guess.
301 | if not matches:
302 | self.incorrect += 1
303 | self.possibles = [word for word in self.possibles if guess not in word]
304 | else:
305 | # Create a regular expression to match new possibles.
306 | for match in matches:
307 | match = int(match) - 1
308 | self.guess = self.guess[:match] + guess + self.guess[match + 1:]
309 | not_letter = '[^{}]'.format(guess)
310 | reg_text = self.guess.replace('_', not_letter)
311 | regex = re.compile(reg_text)
312 | # Update possibles using the regular expression.
313 | self.possibles = [word for word in self.possibles if regex.match(word)]
314 |
315 | def player_guess(self):
316 | """Handle the player guessing if letters are correct. (bool)"""
317 | # Get the player's guess.
318 | guess = self.human.ask('What letter do you guess? ')
319 | # Handle non-guesses.
320 | if len(guess) != 1 or guess.lower() not in 'abcdefghijklmnopqrstuvwxyz':
321 | return self.handle_cmd(guess)
322 | # Check for repeated guesses.
323 | if guess in self.guessed_letters:
324 | self.human.tell('You already guessed {!r}, try again.'.format(guess))
325 | return True
326 | # Handle the guessed letter.
327 | self.guessed_letters += guess
328 | if guess in self.word:
329 | for letter_index, letter in enumerate(self.word):
330 | if letter == guess:
331 | self.guess = self.guess[:letter_index] + letter + self.guess[letter_index + 1:]
332 | else:
333 | self.incorrect += 1
334 |
335 | def score_word(self, word):
336 | """
337 | Estimate the difficulty of a word. (int)
338 |
339 | Parameters:
340 | word: The word to get a difficulty score for. (str)
341 | """
342 | letters = len(word)
343 | worst = min(self.rank_dict[letter] for letter in word.lower())
344 | return worst - letters
345 |
346 | def set_options(self):
347 | """Set the available game options. (None)"""
348 | self.option_set.add_option('status', ['s'],
349 | question = "Would you like updates on the computer's thinking? bool")
350 | self.option_set.add_option('difficulty', ['diff', 'd'], int, valid = range(1, 11), default = 5,
351 | question = "What difficulty level from 1 to 10 would you like to play at (return for 5)? ")
352 | self.option_set.add_group('gonzo', ['gz'], 'difficulty=10')
353 |
354 | def set_up(self):
355 | """Set up the game. (None)"""
356 | # Load the words.
357 | if not hasattr(self, 'words'):
358 | self.human.tell('Loading words...')
359 | letter_count = collections.Counter()
360 | self.words = []
361 | with open(os.path.join(utility.LOC, self.interface.word_list)) as word_file:
362 | for word in word_file:
363 | self.words.append(word.strip())
364 | letter_count += collections.Counter(word.strip().lower())
365 | # Get the frequencies.
366 | self.frequency = ''.join([letter for letter, count in letter_count.most_common()])
367 | self.vowels = ''.join([letter for letter in self.frequency if letter in 'aeiou']) + 'y'
368 | # Score the words.
369 | self.rank_dict = {letter: letter_rank + 1 for letter_rank, letter in enumerate(self.frequency)}
370 | self.scored_words = collections.defaultdict(list)
371 | for word in self.words:
372 | self.scored_words[self.score_word(word)].append(word)
373 | # Set up the game variables.
374 | self.phase = 'answer'
375 | self.word = ''
376 | self.guess = ''
377 | self.guessed_letters = ''
378 | self.incorrect = 0
379 | self.word_length = 0
380 | self.foul = False
381 |
--------------------------------------------------------------------------------
/other_games/slider_game.py:
--------------------------------------------------------------------------------
1 | """
2 | slider_game.py
3 |
4 | A classic puzzle with sliding tiles.
5 |
6 | Copyright (C) 2018-2020 by Craig O'Brien and the t_games contributors.
7 | See the top level __init__.py file for details on the t_games license.
8 |
9 | Constants:
10 | CREDITS: The credits for Slider Puzzle.
11 | OPTIONS: The options for Slider Puzzle.
12 | RULES: The rules to Slider Puzzle.
13 |
14 | Classes:
15 | Slider: A classic puzzle with sliding tiles. (game.Game)
16 | TileBoard: A board of sliding tiles. (board.DimBoard)
17 | """
18 |
19 |
20 | import itertools
21 | import random
22 | import string
23 |
24 | from .. import board
25 | from .. import game
26 |
27 |
28 | CREDITS = """
29 | Game Design: Traditional
30 | Game Programming: Craig "Ichabod" O'Brien
31 | """
32 |
33 | OPTIONS = """
34 | columns= (c=): The number of columns in the puzzle (defaults to 4).
35 | gonzo (gz): Equivalent to 'columns=7 rows=8 shuffles=9'.
36 | rows= (r=): The number of rows in the puzzle (defaults to 4).
37 | size= (s=): The number of columns and rows in the table.
38 | shuffles= (sh=): The number of times to shuffle the solved puzzle before play
39 | (defaults to 3).
40 | text= (t=): The text to use in the puzzle (defaults to 1-9A-Za-z).
41 | """
42 |
43 | RULES = """
44 | The board has one empty space. Any tile horizontally or vertically adjacent to
45 | that space may be slid into that space. The goal is to get all of the tiles in
46 | order, with the space at the bottom right.
47 |
48 | Note that the correct order for the default game is 123456789ABCDEF.
49 |
50 | You may move tiles by the character on them using the move command (m). You can
51 | enter multiple tiles with the move command. Additionally, if there are no lower
52 | case characters in the puzzle, the argument to the move command is
53 | automatically capitalized. Alternatively, you may move the tile above the blank
54 | spot with the north command (n). Similar commands exist for east (e), south
55 | (s), and west (w). You can give a string of directional commands as an argument
56 | to another directional command. For example 'e nsw' would move the eastern
57 | tile, then the northern, southern, and western tiles.
58 | """
59 |
60 |
61 | class Slider(game.Game):
62 | """
63 | A classic puzzle with sliding tiles. (game.Game)
64 |
65 | Attributes:
66 | auto_cap: A flag for capitalizing user input. (bool)
67 | blank_cell: The current space that tiles can slide into. (board.BoardCell)
68 | board: The "board" the puzzle is played on. (TileBoard)
69 | columns: The number of tiles across the puzzle. (int)
70 | moves: How many moves the player has made. (int)
71 | rows: The number of tiles up and down the puzzle. (int)
72 | shuffles: How many times to shuffle the tiles. (int)
73 | size: The number of rows and columns, if they match. (int)
74 | text: The characters representing the default tiles. (str)
75 |
76 | Class Attributes:
77 | tiles: The potential characters for tiles in the puzzle. (str)
78 |
79 | Methods:
80 | direction_move: Move a piece a given offset from the blank spot. (None)
81 | do_east: Move the tile to the east of the blank spot. (None)
82 | do_move: Move the specified tile. (None)
83 | do_north: Move the tile to the north of the blank spot. (None)
84 | do_south: Move the tile to the south of the blank spot. (None)
85 | do_west: Move the tile to the west of the blank spot. (None)
86 | place_text: Put the text into the puzzle. (None)
87 |
88 | Overridden Methods:
89 | game_over
90 | handle_options
91 | player_action
92 | set_options
93 | set_up
94 | """
95 |
96 | aka = ['Slider', 'slpu']
97 | aliases = {'e': 'east', 'm': 'move', 'n': 'north', 's': 'south', 'w': 'west'}
98 | categories = ['Other Games']
99 | credits = CREDITS
100 | name = 'Slider Puzzle'
101 | num_options = 4
102 | options = OPTIONS
103 | rules = RULES
104 | tiles = '123456789' + string.ascii_uppercase + string.ascii_lowercase
105 |
106 | def direction_move(self, offset, direction, argument):
107 | """
108 | Move a piece a given offset from the blank spot. (None)
109 |
110 | Parameters:
111 | offset: The relative coordinate of the tile to move. (tuple of int)
112 | direction: The name of the direction of the tile to move. (str)
113 | argument: The argument to the original directional command. (str)
114 | """
115 | # Check for a valid tile.
116 | try:
117 | start = self.board.offset(self.blank_cell.location, offset)
118 | except KeyError:
119 | self.human.error('There is no tile {} of the blank spot.'.format(direction))
120 | else:
121 | # Move the valid tile.
122 | self.board.move(start.location, self.blank_cell.location, start.contents)
123 | self.blank_cell = start
124 | self.moves += 1
125 | # Do any further directional moves.
126 | for char in argument.lower():
127 | if char in 'ensw':
128 | self.handle_cmd(char)
129 | else:
130 | break
131 |
132 | def do_east(self, argument):
133 | """
134 | Move the tile to the east of the blank spot. (e)
135 | """
136 | self.direction_move((1, 0), 'east', argument)
137 |
138 | def do_gipf(self, arguments):
139 | """
140 | Prisoner's Dilemma lets you rotate three adjacent tiles.
141 |
142 | Thoughful Solitaire solves the next unsovled row.
143 | """
144 | game, losses = self.gipf_check(arguments, ("prisoner's dilemma", 'thoughtful solitaire'))
145 | go = False
146 | if game == "prisoner's dilemma":
147 | if not losses:
148 | # Get three tiles to rotate.
149 | player = self.players[self.player_index]
150 | player.tell(self.board)
151 | while True:
152 | tiles = player.ask('\nEnter three tiles to rotate (123 -> 231): ')
153 | if self.auto_cap:
154 | tiles = tiles.upper()
155 | if len(tiles) != len(set(tiles)):
156 | player.tell('Please enter three tiles with no spaces.')
157 | elif all(char in self.text for char in tiles):
158 | break
159 | else:
160 | player.tell('Please enter valid tiles.')
161 | # Find the tiles.
162 | cells = []
163 | for cell in self.board.cells.values():
164 | if cell.contents and cell.contents in tiles:
165 | cells.append(cell)
166 | tiles = [cell.contents for cell in cells]
167 | # Rotate and place the tiles.
168 | tiles.append(tiles.pop(0))
169 | for tile, cell in zip(tiles, cells):
170 | cell.contents = tile
171 | elif game == 'thoughtful solitaire':
172 | if not losses:
173 | # Figure out what rows are solved.
174 | for row in range(1, self.rows + 1):
175 | # Get what the row is.
176 | row_text = ''
177 | for column in range(1, self.columns + 1):
178 | tile = self.board.cells[(column, row)].contents
179 | if tile is not None:
180 | row_text = '{}{}'.format(row_text, tile)
181 | # Compare the row to what it should be.
182 | target_text = self.text[((row - 1) * self.columns):(row * self.columns)]
183 | if target_text != row_text:
184 | break
185 | # Solve everything.
186 | self.place_text()
187 | # Mix up the rows to be left unsolved.
188 | if row != self.rows:
189 | column_range = range(1, self.columns + 1)
190 | row_range = range(row + 1, self.rows + 1)
191 | row_locations = itertools.product(column_range, row_range)
192 | self.shuffle(shuffle_cells = set(board.Coordinate(pair) for pair in row_locations))
193 | else:
194 | self.human.tell('Language!')
195 | go = True
196 | return go
197 |
198 | def do_move(self, argument):
199 | """
200 | Move the specified tile. (m)
201 |
202 | The argument is the tile (or tiles) to move. Each one in order is moved into
203 | the blank space. Spaces are ignored in the argument, but any invalid move stops
204 | the movement.
205 | """
206 | if self.auto_cap:
207 | argument = argument.upper()
208 | for char in argument:
209 | # Skip spaces.
210 | if char == ' ':
211 | continue
212 | # Find the cell to move.
213 | movers = []
214 | # Only search movable squares.
215 | for offset in ((-1, 0), (0, -1), (0, 1), (1, 0)):
216 | try:
217 | possible = self.board.offset(self.blank_cell.location, offset)
218 | except KeyError:
219 | continue
220 | if char in possible:
221 | movers.append(possible)
222 | # Check for illegal moves.
223 | if len(movers) > 1:
224 | self.human.error('The move {} is ambiguous.'.format(char))
225 | break
226 | elif not movers:
227 | if char in self.text:
228 | self.human.error('The {} tile cannot be moved.'.format(char))
229 | else:
230 | self.human.error('There is no {} tile in the puzzle.'.format(char))
231 | break
232 | # Move the piece.
233 | self.board.move(movers[0].location, self.blank_cell.location, char)
234 | self.blank_cell = movers[0]
235 | self.moves += 1
236 |
237 | def do_north(self, argument):
238 | """
239 | Move the tile to the north of the blank spot. (n)
240 | """
241 | self.direction_move((0, -1), 'north', argument)
242 |
243 | def do_south(self, argument):
244 | """
245 | Move the tile to the south of the blank spot. (s)
246 | """
247 | self.direction_move((0, 1), 'south', argument)
248 |
249 | def do_west(self, argument):
250 | """
251 | Move the tile to the west of the blank spot. (w)
252 | """
253 | self.direction_move((-1, 0), 'west', argument)
254 |
255 | def game_over(self):
256 | """Determine if the puzzle is solved. (None)"""
257 | # Compare the current board to the winning text.
258 | current = str(self.board).replace('\n', '')
259 | if current[:-1] == self.text:
260 | self.human.tell('')
261 | self.human.tell(self.board)
262 | self.human.tell('\nYou solved the puzzle!')
263 | self.human.tell('It took you {} moves to solve the puzzle.'.format(self.moves))
264 | self.win_loss_draw[0] = 1
265 | self.scores[self.human.name] = self.rows * self.columns - 1
266 | self.turns = self.moves
267 | return True
268 | else:
269 | return False
270 |
271 | def handle_options(self):
272 | """Game the options for the game. (None)"""
273 | # Parse the user's option choices.
274 | super(Slider, self).handle_options()
275 | # Make size override rows and columns
276 | if self.size:
277 | self.rows = self.size
278 | self.columns = self.size
279 | elif self.rows == self.columns:
280 | self.size = self.rows
281 | # Set autocapitalize.
282 | text_len = self.columns * self.rows - 1
283 | self.auto_cap = not self.text and text_len < 36
284 | # Make sure the text is the right size.
285 | if self.text and len(self.text) < text_len:
286 | self.human.error('Puzzle text padded because it is too short.')
287 | elif len(self.text) > text_len:
288 | self.human.error('Puzzle text truncated because it is too long.')
289 | self.text = (self.text + self.tiles)[:text_len]
290 | if len(self.text) < text_len:
291 | self.option_set.errors.append('Puzzle text is too short. Puzzle size shrunk to max square.')
292 | self.columns = int(len(self.text) ** 0.5)
293 | self.rows = self.columns
294 |
295 | def place_text(self):
296 | """Put the text into the puzzle. (None)"""
297 | for row in range(self.rows):
298 | for column in range(self.columns):
299 | if column + 1 == self.columns and row + 1 == self.rows:
300 | break
301 | self.board.place((column + 1, row + 1), self.text[row * self.columns + column])
302 | self.blank_cell = self.board.cells[(self.columns, self.rows)]
303 | self.blank_cell.clear()
304 |
305 | def player_action(self, player):
306 | """
307 | Handle a player's turn or other player actions. (bool)
308 |
309 | Parameters:
310 | player: The player whose turn it is. (Player)
311 | """
312 | player.tell('')
313 | player.tell(self.board)
314 | move = player.ask('\nWhat is your move? ')
315 | return self.handle_cmd(move)
316 |
317 | def set_options(self):
318 | """Set up the game options. (None)"""
319 | self.option_set.add_option('columns', ['c'], int, 4, check = lambda x: x > 0,
320 | question = 'How many columns should the board have (return for 4)? ')
321 | self.option_set.add_option('rows', ['r'], int, 4, check = lambda x: x > 0,
322 | question = 'How many columns should the board have (return for 4)? ')
323 | self.option_set.add_option('size', ['s'], int, 0, check = lambda x: x > 0,
324 | question = 'How many columns and rows should the board have (return to ignore)? ')
325 | self.option_set.add_option('text', ['t'], default = '',
326 | question = 'What text should the solution be (return for numbers + letters)? ')
327 | self.option_set.add_option('shuffles', ['sh'], int, default = 3, check = lambda x: x > 0,
328 | question = 'How many times should the puzzle be shuffled (return for 3)? ')
329 | self.option_set.add_group('gonzo', ['gz'], 'columns=7 rows=8 shuffles=9')
330 |
331 | def set_up(self):
332 | """Set up the board for the game. (None)"""
333 | self.moves = 0
334 | self.board = TileBoard((self.columns, self.rows))
335 | self.place_text()
336 | self.shuffle()
337 |
338 | def shuffle(self, shuffle_cells = []):
339 | """
340 | Shuffle the board. (None)
341 |
342 | Parameters:
343 | shuffle_cells: The locations of the cells to shuffle. (set of board.Coordinate)
344 | """
345 | if not shuffle_cells:
346 | shuffle_cells = set(self.board.cells.keys())
347 | blanks = set([self.blank_cell.location])
348 | for mix in range(self.shuffles):
349 | while len(blanks) < len(shuffle_cells):
350 | offset = random.choice(((-1, 0), (0, -1), (0, 1), (1, 0)))
351 | target = self.blank_cell.location + offset
352 | if target in shuffle_cells:
353 | target_cell = self.board.cells[target]
354 | self.board.move(target, self.blank_cell.location, target_cell.contents)
355 | self.blank_cell = target_cell
356 | blanks.add(target_cell)
357 | blanks = set()
358 |
359 |
360 | class TileBoard(board.DimBoard):
361 | """
362 | A board of sliding tiles. (board.DimBoard)
363 |
364 | Overridden Methods:
365 | move
366 | safe
367 | """
368 |
369 | def __str__(self):
370 | """Human readable text representation. (str)"""
371 | lines = []
372 | for row in range(self.dimensions[1]):
373 | lines.append([])
374 | for column in range(self.dimensions[0]):
375 | lines[-1].append(str(self.cells[(column + 1, row + 1)]))
376 | return '\n'.join([''.join(line) for line in lines])
377 |
378 | def move(self, start, end, piece = None):
379 | """
380 | Move a piece from one cell to a blank cell. (object)
381 |
382 | Parameters:
383 | start: The location containing the piece to move. (Coordinate)
384 | end: The location to move the piece to. (Coordinate)
385 | piece: The piece to move. (object)
386 | """
387 | if not self.cells[end].contents:
388 | self.cells[start].contents = None
389 | self.cells[end].contents = piece
390 | else:
391 | raise ValueError('End square is not empty.')
392 |
--------------------------------------------------------------------------------