Some notes before diving-in...
it's probably a good idea to get a snack and drink; I'm a bit verbose and am about to compress some years of knowledge
__bar__
when spoken is "dunder bar" , and the phylum that theirthey're classified under are "magic methods"what I share is not gospel as such, but a collection of tricks I wish someone had shown me when I was getting into Python
... okay back on track.
#!/usr/bin/env python
import time
import random
print_separator = "".join(['_' for _ in range(9)])
__author__ = "S0AndS0"
#
# Functions
#
def question(message):
""" Returns response to `message` from user """
return input("{message}? ".format(message = message))
#
# Classes
#
class Gone_Fishing(dict):
"""
Gone_Fishing is a simple simulation inspired by
[Python - Fishing Simulator](https://codereview.stackexchange.com/q/217357/197446)
## Arguments
- `fishes`, `dict`ionary such as `{'cod': {'amount': 0, 'chances': [1, 2]}}`
- `min_chance`, `int`eger of min number that `random.randint` may generate
- `max_chance`, `int`eger of max number that `random.randint` may generate
"""
def __init__(self, fishes, min_chance = 1, max_chance = 10, **kwargs):
super(Gone_Fishing, self).__init__(**kwargs)
self.update(fishes = fishes,
chances = {'min': min_chance, 'max': max_chance})
@staticmethod
def questionkeep_fishing(message, expected):
""" ReturnsReturn response`bool`ean of if `response` to `message` frommatches user`expected` """
return input("{message}? ".format(messageresponse = message))
@staticmethod
def keep_fishingquestion(response, expectedmessage):
""" Return `bool`ean of if `response` matches `expected` """
if not response or not isinstance(response, str):
return False
return response.lower() == expected
@property
def dump_cooler(self):
"""
Returns `score`, a `dict`ionary similar to `{'cod': 5, 'tire': 2}`,
after printing and reseting _`amount`s_ caught
"""
score = {}
for fish, data in self['fishes'].items():
if data['amount'] > 0:
score.update({fish: data['amount']})
if data['amount'] > 1 and data.get('plural'):
fish = data['plural']
print("{amount} {fish}".format(**{
'fish': fish,
'amount': data['amount']}))
data['amount'] = 0
return score
def catch(self, chance):
""" Returns `None` or name of `fish` caught based on `chance` """
caught = []
for fish, data in self['fishes'].items():
if chance in data['chances']:
caught.append(fish)
return caught
def main_loop(self):
"""
Asks questions, adds to _cooler_ anything caught, and prints score when finished
"""
first = True
message = 'Go fishing'
expected = 'yes'
while self.keep_fishing(self.question(message), expected):
time.sleep(1)
if first:
first = False
message = "Keep fishing"
chances = random.randint(self['chances']['min'], self['chances']['max'])
caught = self.catch(chances)
if caught:
for fish in caught:
self['fishes'][fish]['amount'] += 1
fancy_fish = ' '.join(fish.split('_')).title()
print("You caught a {fish}".format(fish = fancy_fish))
else:
print("Nothing was caught this time.")
print("{0}\nThanks for playing".format(print_separator))
if True in [x['amount'] > 0 for x in self['fishes'].values()]:
print("You caught")
self.dump_cooler
print(print_separator)
if __name__ == '__main__':
"""
This block of code is not executed during import
and instead is usually run when a file is executed,
eg. `python gone_fishing.py`, making it a good
place for simple unit tests and example usage.
"""
gone_fishing = Gone_Fishing(
fishes = {
'cod': {'amount': 0, 'chances': [1]},
'salmon': {'amount': 0, 'chances': [5]},
'shark': {'amount': 0, 'chances': [9, 10], 'plural': 'sharks'},
'wild_fish': {'amount': 0, 'chances': [7], 'plural': 'wild_fishes'},
'old_shoe': {'amount': 0, 'chances': [10, 15], 'plural': 'old_shoes'},
'tire': {'amount': 0, 'chances': [2, 19], 'plural': 'tires'},
},
min_chances = 0,
max_chances = 20,
)
gone_fishing.main_loop()
Note from the future; check the comments of this answer for some swell suggestions from @Izaak van Dongen.
Side note; the
__init__
method is one of many that are called implicitly by preforming some action with an object, eg.__add__
is called implicitly by using+
between twoObjects
with a__add__
method (side-side note, I'll get into why that was ana
and not anan
in a bit), which is why the following works with lists...
list_one = [3, 2, 1]
list_two = [0, -1, -2]
list_one + list_two
# -> [3, 2, 1, 0, -1, -2]
That bit with **kwargs
stands for key word arguments
which passes things as a bare dict
ionary, the other syntax you may run across is *args
, which passes things as a bare list
of arguments; there be some fanciness that can be done with this syntax that I'll not get into at this point other than saying that context matters. However, you'll find some examples of passing an unwrapped dictionary, such as to format
via print("{amount} {fish}".format(**{...}))
, which hint hint, is a great way of passing variable parameter names.
... it's possible to call self.update()
from within SomeThing.___init__
without causing confusion of intent, where as to have SomeThing
still operate as a dict
ionary, eg. assigning something = SomeThing(spam = 'Spam')
without causing errors, one should use super(SomeThing, self).__init__(**kwargs)
to allow Python to preform it's voodoo with figuring out which inheriting class
'll take responsibility for those arguments. Side note, that does mean that one could do class SomeThing(dict, Iterator)
, and have that mean something but I'll not get into that here; kinda already covered that specifically on math stack in regards to graph modeling and prioritization.
That does mean that one could do
class SomeThing(dict, Iterator)
, and have that mean something but I'll not get into that here; kinda already covered that specifically on math stack in regards to graph modeling and prioritization.
... but can only be gotten not set, which makes'em a great place to stash dynamic or semiprivate properties about an objectObject
.
In the case of staticmethod
s, they're not passed a reference to self
so cannot easily access or modify saved states, but they can be more easily used without initializing so operate similarly to regular functions, eg...
responses = []
responses.append(Gone_Fishing.question("Where to"))
print("I heard -> {response}".format(response = responses[-1]))
for _ in range(7):
responses.append(Gone_Fishing.question("... are you sure"))
print("I heard -> {response}".format(response = responses[-1]))
print("Okay... though...")
Note from the future; as pointed out by @Maarten Fabré I indeed slipped in some superfluous use of the
staticmethod
decorator, good catch there, and this'll now serve as an example of getting carried away whendecorat
ing.Generally I use
staticmethod
s when I've a class that isn't concerned with it's internal state but isn't large enough to warrant it's own file, very edge case kinda thing, and usually it means that I should probably split'em out into a file that organizes similar functions. Hopefully recent edits now look closer to proper for future readers.
That bit within the main_loop
method
with while self.keep_fishing(self.question(message), expected)
, when unwrapped I think you'll really like, it's returning True
or False
at the top of every iteration based on asking the user a question and comparing their response with what's expected.
There's certainly more room for improvement, eg. having gone_fishing['fishes'][fish_name]['ammount']gone_fishing['fishes'][fish_name]['amount']
subtracted from, while adding to gone_fishing['cooler']
or similar structure; just for a start. But this was all just to expose quick-n-dirty methods of organizing the problem space with Object Oriented Programing.