diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..f6af837 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,569 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths and can be in Posix or Windows format. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.9 + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + +# Generate a config with with `pylint --generate-rcfile > .my_pylintrc` + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members=pygame.* + +# Tells whether missing members accessed in mixin class should be ignored. A +# class is considered mixin if its name matches the mixin-class-rgx option. +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins ignore-mixin- +# members is set to 'yes' +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=no + +# Signatures are removed from the similarity computation +ignore-signatures=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/Makefile b/Makefile index 63dc017..748af9b 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,13 @@ -# Usage: -# make # setup the project for the first time -# make install # install pipenv dependencies -# make packages # adds src/ files to be available as python modules - # default target .DEFAULT_GOAL := init # targets that do not create a file .PHONY: init activate test lint lint-src lintfix lintfixhard install lock clean +# help target taken from https://gist.github.com/prwhite/8168133 +help: ## Shows help message + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[$$()% 0-9a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + PY_FILES := src/ cli/ init: activate packages install test diff --git a/Pipfile b/Pipfile index 9f79fce..5532574 100644 --- a/Pipfile +++ b/Pipfile @@ -7,6 +7,7 @@ name = "pypi" tensorflow = "*" matplotlib = "*" autopep8 = "*" +pygame = "*" [requires] python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index ed92a8a..72f82b1 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "ea05cac1273735604885e9e4141acd4431228feb5f171c710d960fb2437e88b0" + "sha256": "e78125eed01a864b28a31bca9946ad1ba779349bfaab7d311fc903d88c72271c" }, "pipfile-spec": 6, "requires": { @@ -79,11 +79,11 @@ }, "fonttools": { "hashes": [ - "sha256:084dd1762f083a1bf49e41da1bfeafb475c9dce46265690a6bdd33290b9a63f4", - "sha256:6985cc5380c06db07fdc73ade15e6adbd4ce6ff850d7561ca00f97090b4b263d" + "sha256:236b29aee6b113e8f7bee28779c1230a86ad2aac9a74a31b0aedf57e7dfb62a4", + "sha256:2df636a3f402ef14593c6811dac0609563b8c374bd7850e76919eb51ea205426" ], "markers": "python_version >= '3.7'", - "version": "==4.30.0" + "version": "==4.31.2" }, "gast": { "hashes": [ @@ -95,11 +95,11 @@ }, "google-auth": { "hashes": [ - "sha256:218ca03d7744ca0c8b6697b6083334be7df49b7bf76a69d555962fd1a7657b5f", - "sha256:ad160fc1ea8f19e331a16a14a79f3d643d813a69534ba9611d2c80dc10439dad" + "sha256:3ba4d63cb29c1e6d5ffcc1c0623c03cf02ede6240a072f213084749574e691ab", + "sha256:60d449f8142c742db760f4c0be39121bc8d9be855555d784c252deaca1ced3f5" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", - "version": "==2.6.0" + "version": "==2.6.2" }, "google-auth-oauthlib": { "hashes": [ @@ -199,11 +199,11 @@ }, "importlib-metadata": { "hashes": [ - "sha256:b36ffa925fe3139b2f6ff11d6925ffd4fa7bc47870165e3ac260ac7b4f91e6ac", - "sha256:d16e8c1deb60de41b8e8ed21c1a7b947b0bc62fab7e1d470bcdf331cea2e6735" + "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6", + "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539" ], "markers": "python_version < '3.10'", - "version": "==4.11.2" + "version": "==4.11.3" }, "keras": { "hashes": [ @@ -220,58 +220,58 @@ }, "kiwisolver": { "hashes": [ - "sha256:0007840186bacfaa0aba4466d5890334ea5938e0bb7e28078a0eb0e63b5b59d5", - "sha256:19554bd8d54cf41139f376753af1a644b63c9ca93f8f72009d50a2080f870f77", - "sha256:1d45d1c74f88b9f41062716c727f78f2a59a5476ecbe74956fafb423c5c87a76", - "sha256:1d819553730d3c2724582124aee8a03c846ec4362ded1034c16fb3ef309264e6", - "sha256:2210f28778c7d2ee13f3c2a20a3a22db889e75f4ec13a21072eabb5693801e84", - "sha256:22521219ca739654a296eea6d4367703558fba16f98688bd8ce65abff36eaa84", - "sha256:25405f88a37c5f5bcba01c6e350086d65e7465fd1caaf986333d2a045045a223", - "sha256:2b65bd35f3e06a47b5c30ea99e0c2b88f72c6476eedaf8cfbc8e66adb5479dcf", - "sha256:2ddb500a2808c100e72c075cbb00bf32e62763c82b6a882d403f01a119e3f402", - "sha256:2f8f6c8f4f1cff93ca5058d6ec5f0efda922ecb3f4c5fb76181f327decff98b8", - "sha256:30fa008c172355c7768159983a7270cb23838c4d7db73d6c0f6b60dde0d432c6", - "sha256:3dbb3cea20b4af4f49f84cffaf45dd5f88e8594d18568e0225e6ad9dec0e7967", - "sha256:4116ba9a58109ed5e4cb315bdcbff9838f3159d099ba5259c7c7fb77f8537492", - "sha256:44e6adf67577dbdfa2d9f06db9fbc5639afefdb5bf2b4dfec25c3a7fbc619536", - "sha256:5326ddfacbe51abf9469fe668944bc2e399181a2158cb5d45e1d40856b2a0589", - "sha256:70adc3658138bc77a36ce769f5f183169bc0a2906a4f61f09673f7181255ac9b", - "sha256:72be6ebb4e92520b9726d7146bc9c9b277513a57a38efcf66db0620aec0097e0", - "sha256:7843b1624d6ccca403a610d1277f7c28ad184c5aa88a1750c1a999754e65b439", - "sha256:7ba5a1041480c6e0a8b11a9544d53562abc2d19220bfa14133e0cdd9967e97af", - "sha256:80efd202108c3a4150e042b269f7c78643420cc232a0a771743bb96b742f838f", - "sha256:82f49c5a79d3839bc8f38cb5f4bfc87e15f04cbafa5fbd12fb32c941cb529cfb", - "sha256:83d2c9db5dfc537d0171e32de160461230eb14663299b7e6d18ca6dca21e4977", - "sha256:8d93a1095f83e908fc253f2fb569c2711414c0bfd451cab580466465b235b470", - "sha256:8dc3d842fa41a33fe83d9f5c66c0cc1f28756530cd89944b63b072281e852031", - "sha256:9661a04ca3c950a8ac8c47f53cbc0b530bce1b52f516a1e87b7736fec24bfff0", - "sha256:a498bcd005e8a3fedd0022bb30ee0ad92728154a8798b703f394484452550507", - "sha256:a7a4cf5bbdc861987a7745aed7a536c6405256853c94abc9f3287c3fa401b174", - "sha256:b5074fb09429f2b7bc82b6fb4be8645dcbac14e592128beeff5461dcde0af09f", - "sha256:b6a5431940f28b6de123de42f0eb47b84a073ee3c3345dc109ad550a3307dd28", - "sha256:ba677bcaff9429fd1bf01648ad0901cea56c0d068df383d5f5856d88221fe75b", - "sha256:bcadb05c3d4794eb9eee1dddf1c24215c92fb7b55a80beae7a60530a91060560", - "sha256:bf7eb45d14fc036514c09554bf983f2a72323254912ed0c3c8e697b62c4c158f", - "sha256:c358721aebd40c243894298f685a19eb0491a5c3e0b923b9f887ef1193ddf829", - "sha256:c4550a359c5157aaf8507e6820d98682872b9100ce7607f8aa070b4b8af6c298", - "sha256:c6572c2dab23c86a14e82c245473d45b4c515314f1f859e92608dcafbd2f19b8", - "sha256:cba430db673c29376135e695c6e2501c44c256a81495da849e85d1793ee975ad", - "sha256:dedc71c8eb9c5096037766390172c34fb86ef048b8e8958b4e484b9e505d66bc", - "sha256:e6f5eb2f53fac7d408a45fbcdeda7224b1cfff64919d0f95473420a931347ae9", - "sha256:ec2eba188c1906b05b9b49ae55aae4efd8150c61ba450e6721f64620c50b59eb", - "sha256:ee040a7de8d295dbd261ef2d6d3192f13e2b08ec4a954de34a6fb8ff6422e24c", - "sha256:eedd3b59190885d1ebdf6c5e0ca56828beb1949b4dfe6e5d0256a461429ac386", - "sha256:f441422bb313ab25de7b3dbfd388e790eceb76ce01a18199ec4944b369017009", - "sha256:f8eb7b6716f5b50e9c06207a14172cf2de201e41912ebe732846c02c830455b9", - "sha256:fc4453705b81d03568d5b808ad8f09c77c47534f6ac2e72e733f9ca4714aa75c" + "sha256:0b7f50a1a25361da3440f07c58cd1d79957c2244209e4f166990e770256b6b0b", + "sha256:0c380bb5ae20d829c1a5473cfcae64267b73aaa4060adc091f6df1743784aae0", + "sha256:0d98dca86f77b851350c250f0149aa5852b36572514d20feeadd3c6b1efe38d0", + "sha256:0e45e780a74416ef2f173189ef4387e44b5494f45e290bcb1f03735faa6779bf", + "sha256:0e8afdf533b613122e4bbaf3c1e42c2a5e9e2d1dd3a0a017749a7658757cb377", + "sha256:1008346a7741620ab9cc6c96e8ad9b46f7a74ce839dbb8805ddf6b119d5fc6c2", + "sha256:1d1078ba770d6165abed3d9a1be1f9e79b61515de1dd00d942fa53bba79f01ae", + "sha256:1dcade8f6fe12a2bb4efe2cbe22116556e3b6899728d3b2a0d3b367db323eacc", + "sha256:240009fdf4fa87844f805e23f48995537a8cb8f8c361e35fda6b5ac97fcb906f", + "sha256:240c2d51d098395c012ddbcb9bd7b3ba5de412a1d11840698859f51d0e643c4f", + "sha256:262c248c60f22c2b547683ad521e8a3db5909c71f679b93876921549107a0c24", + "sha256:2e6cda72db409eefad6b021e8a4f964965a629f577812afc7860c69df7bdb84a", + "sha256:3c032c41ae4c3a321b43a3650e6ecc7406b99ff3e5279f24c9b310f41bc98479", + "sha256:42f6ef9b640deb6f7d438e0a371aedd8bef6ddfde30683491b2e6f568b4e884e", + "sha256:484f2a5f0307bc944bc79db235f41048bae4106ffa764168a068d88b644b305d", + "sha256:69b2d6c12f2ad5f55104a36a356192cfb680c049fe5e7c1f6620fc37f119cdc2", + "sha256:6e395ece147f0692ca7cdb05a028d31b83b72c369f7b4a2c1798f4b96af1e3d8", + "sha256:6ece2e12e4b57bc5646b354f436416cd2a6f090c1dadcd92b0ca4542190d7190", + "sha256:71469b5845b9876b8d3d252e201bef6f47bf7456804d2fbe9a1d6e19e78a1e65", + "sha256:7f606d91b8a8816be476513a77fd30abe66227039bd6f8b406c348cb0247dcc9", + "sha256:7f88c4b8e449908eeddb3bbd4242bd4dc2c7a15a7aa44bb33df893203f02dc2d", + "sha256:81237957b15469ea9151ec8ca08ce05656090ffabc476a752ef5ad7e2644c526", + "sha256:89b57c2984f4464840e4b768affeff6b6809c6150d1166938ade3e22fbe22db8", + "sha256:8a830a03970c462d1a2311c90e05679da56d3bd8e78a4ba9985cb78ef7836c9f", + "sha256:8ae5a071185f1a93777c79a9a1e67ac46544d4607f18d07131eece08d415083a", + "sha256:8b6086aa6936865962b2cee0e7aaecf01ab6778ce099288354a7229b4d9f1408", + "sha256:8ec2e55bf31b43aabe32089125dca3b46fdfe9f50afbf0756ae11e14c97b80ca", + "sha256:8ff3033e43e7ca1389ee59fb7ecb8303abb8713c008a1da49b00869e92e3dd7c", + "sha256:91eb4916271655dfe3a952249cb37a5c00b6ba68b4417ee15af9ba549b5ba61d", + "sha256:9d2bb56309fb75a811d81ed55fbe2208aa77a3a09ff5f546ca95e7bb5fac6eff", + "sha256:a4e8f072db1d6fb7a7cc05a6dbef8442c93001f4bb604f1081d8c2db3ca97159", + "sha256:b1605c7c38cc6a85212dfd6a641f3905a33412e49f7c003f35f9ac6d71f67720", + "sha256:b3e251e5c38ac623c5d786adb21477f018712f8c6fa54781bd38aa1c60b60fc2", + "sha256:b978afdb913ca953cf128d57181da2e8798e8b6153be866ae2a9c446c6162f40", + "sha256:be9a650890fb60393e60aacb65878c4a38bb334720aa5ecb1c13d0dac54dd73b", + "sha256:c222f91a45da9e01a9bc4f760727ae49050f8e8345c4ff6525495f7a164c8973", + "sha256:c839bf28e45d7ddad4ae8f986928dbf5a6d42ff79760d54ec8ada8fb263e097c", + "sha256:cbb5eb4a2ea1ffec26268d49766cafa8f957fe5c1b41ad00733763fae77f9436", + "sha256:e348f1904a4fab4153407f7ccc27e43b2a139752e8acf12e6640ba683093dd96", + "sha256:e677cc3626287f343de751e11b1e8a5b915a6ac897e8aecdbc996cd34de753a0", + "sha256:f74f2a13af201559e3d32b9ddfc303c94ae63d63d7f4326d06ce6fe67e7a8255", + "sha256:fa4d97d7d2b2c082e67907c0b8d9f31b85aa5d3ba0d33096b7116f03f8061261", + "sha256:ffbdb9a96c536f0405895b5e21ee39ec579cb0ed97bdbd169ae2b55f41d73219" ], "markers": "python_version >= '3.7'", - "version": "==1.3.2" + "version": "==1.4.2" }, "libclang": { "hashes": [ "sha256:069407eac2e20ea8f18212d28c6598db31014e7b8a77febc92e762ec133c3226", "sha256:9c1e623340ccafe3a10a2abbc90f59593ff29f0c854f4ddb65b6220d9d998fb4", + "sha256:b0acfcfbd1f6d411f654cf6ec4f09cecf0f80b3480e4c9f834d1dcb1f8bd6907", "sha256:b61dedc1b941f43acca1fa15df0a6669c6c3983197c6f3226ae03a766281dd37", "sha256:b7de34393ed46c6cf7b22178d0d43cec2f2dab2f5f95450520a47fc1cf2df5ac", "sha256:bcaffec6b1ab9486811670db7af29d4a361830d6cb75da4f5672e884aa973bda", @@ -381,76 +381,77 @@ }, "pillow": { "hashes": [ - "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97", - "sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049", - "sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c", - "sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae", - "sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28", - "sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030", - "sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56", - "sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976", - "sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e", - "sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e", - "sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f", - "sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b", - "sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a", - "sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e", - "sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa", - "sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7", - "sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00", - "sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838", - "sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360", - "sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b", - "sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a", - "sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd", - "sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4", - "sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70", - "sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204", - "sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc", - "sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b", - "sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669", - "sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7", - "sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e", - "sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c", - "sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092", - "sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c", - "sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5", - "sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac" + "sha256:01ce45deec9df310cbbee11104bae1a2a43308dd9c317f99235b6d3080ddd66e", + "sha256:0c51cb9edac8a5abd069fd0758ac0a8bfe52c261ee0e330f363548aca6893595", + "sha256:17869489de2fce6c36690a0c721bd3db176194af5f39249c1ac56d0bb0fcc512", + "sha256:21dee8466b42912335151d24c1665fcf44dc2ee47e021d233a40c3ca5adae59c", + "sha256:25023a6209a4d7c42154073144608c9a71d3512b648a2f5d4465182cb93d3477", + "sha256:255c9d69754a4c90b0ee484967fc8818c7ff8311c6dddcc43a4340e10cd1636a", + "sha256:35be4a9f65441d9982240e6966c1eaa1c654c4e5e931eaf580130409e31804d4", + "sha256:3f42364485bfdab19c1373b5cd62f7c5ab7cc052e19644862ec8f15bb8af289e", + "sha256:3fddcdb619ba04491e8f771636583a7cc5a5051cd193ff1aa1ee8616d2a692c5", + "sha256:463acf531f5d0925ca55904fa668bb3461c3ef6bc779e1d6d8a488092bdee378", + "sha256:4fe29a070de394e449fd88ebe1624d1e2d7ddeed4c12e0b31624561b58948d9a", + "sha256:55dd1cf09a1fd7c7b78425967aacae9b0d70125f7d3ab973fadc7b5abc3de652", + "sha256:5a3ecc026ea0e14d0ad7cd990ea7f48bfcb3eb4271034657dc9d06933c6629a7", + "sha256:5cfca31ab4c13552a0f354c87fbd7f162a4fafd25e6b521bba93a57fe6a3700a", + "sha256:66822d01e82506a19407d1afc104c3fcea3b81d5eb11485e593ad6b8492f995a", + "sha256:69e5ddc609230d4408277af135c5b5c8fe7a54b2bdb8ad7c5100b86b3aab04c6", + "sha256:6b6d4050b208c8ff886fd3db6690bf04f9a48749d78b41b7a5bf24c236ab0165", + "sha256:7a053bd4d65a3294b153bdd7724dce864a1d548416a5ef61f6d03bf149205160", + "sha256:82283af99c1c3a5ba1da44c67296d5aad19f11c535b551a5ae55328a317ce331", + "sha256:8782189c796eff29dbb37dd87afa4ad4d40fc90b2742704f94812851b725964b", + "sha256:8d79c6f468215d1a8415aa53d9868a6b40c4682165b8cb62a221b1baa47db458", + "sha256:97bda660702a856c2c9e12ec26fc6d187631ddfd896ff685814ab21ef0597033", + "sha256:a325ac71914c5c043fa50441b36606e64a10cd262de12f7a179620f579752ff8", + "sha256:a336a4f74baf67e26f3acc4d61c913e378e931817cd1e2ef4dfb79d3e051b481", + "sha256:a598d8830f6ef5501002ae85c7dbfcd9c27cc4efc02a1989369303ba85573e58", + "sha256:a5eaf3b42df2bcda61c53a742ee2c6e63f777d0e085bbc6b2ab7ed57deb13db7", + "sha256:aea7ce61328e15943d7b9eaca87e81f7c62ff90f669116f857262e9da4057ba3", + "sha256:af79d3fde1fc2e33561166d62e3b63f0cc3e47b5a3a2e5fea40d4917754734ea", + "sha256:c24f718f9dd73bb2b31a6201e6db5ea4a61fdd1d1c200f43ee585fc6dcd21b34", + "sha256:c5b0ff59785d93b3437c3703e3c64c178aabada51dea2a7f2c5eccf1bcf565a3", + "sha256:c7110ec1701b0bf8df569a7592a196c9d07c764a0a74f65471ea56816f10e2c8", + "sha256:c870193cce4b76713a2b29be5d8327c8ccbe0d4a49bc22968aa1e680930f5581", + "sha256:c9efef876c21788366ea1f50ecb39d5d6f65febe25ad1d4c0b8dff98843ac244", + "sha256:de344bcf6e2463bb25179d74d6e7989e375f906bcec8cb86edb8b12acbc7dfef", + "sha256:eb1b89b11256b5b6cad5e7593f9061ac4624f7651f7a8eb4dfa37caa1dfaa4d0", + "sha256:ed742214068efa95e9844c2d9129e209ed63f61baa4d54dbf4cf8b5e2d30ccf2", + "sha256:f401ed2bbb155e1ade150ccc63db1a4f6c1909d3d378f7d1235a44e90d75fb97", + "sha256:fb89397013cf302f282f0fc998bb7abf11d49dcff72c8ecb320f76ea6e2c5717" ], "markers": "python_version >= '3.7'", - "version": "==9.0.1" + "version": "==9.1.0" }, "protobuf": { "hashes": [ - "sha256:072fbc78d705d3edc7ccac58a62c4c8e0cec856987da7df8aca86e647be4e35c", - "sha256:09297b7972da685ce269ec52af761743714996b4381c085205914c41fcab59fb", - "sha256:16f519de1313f1b7139ad70772e7db515b1420d208cb16c6d7858ea989fc64a9", - "sha256:1c91ef4110fdd2c590effb5dca8fdbdcb3bf563eece99287019c4204f53d81a4", - "sha256:3112b58aac3bac9c8be2b60a9daf6b558ca3f7681c130dcdd788ade7c9ffbdca", - "sha256:36cecbabbda242915529b8ff364f2263cd4de7c46bbe361418b5ed859677ba58", - "sha256:4276cdec4447bd5015453e41bdc0c0c1234eda08420b7c9a18b8d647add51e4b", - "sha256:435bb78b37fc386f9275a7035fe4fb1364484e38980d0dd91bc834a02c5ec909", - "sha256:48ed3877fa43e22bcacc852ca76d4775741f9709dd9575881a373bd3e85e54b2", - "sha256:54a1473077f3b616779ce31f477351a45b4fef8c9fd7892d6d87e287a38df368", - "sha256:69da7d39e39942bd52848438462674c463e23963a1fdaa84d88df7fbd7e749b2", - "sha256:6cbc312be5e71869d9d5ea25147cdf652a6781cf4d906497ca7690b7b9b5df13", - "sha256:7bb03bc2873a2842e5ebb4801f5c7ff1bfbdf426f85d0172f7644fcda0671ae0", - "sha256:7ca7da9c339ca8890d66958f5462beabd611eca6c958691a8fe6eccbd1eb0c6e", - "sha256:835a9c949dc193953c319603b2961c5c8f4327957fe23d914ca80d982665e8ee", - "sha256:84123274d982b9e248a143dadd1b9815049f4477dc783bf84efe6250eb4b836a", - "sha256:8961c3a78ebfcd000920c9060a262f082f29838682b1f7201889300c1fbe0616", - "sha256:96bd766831596d6014ca88d86dc8fe0fb2e428c0b02432fd9db3943202bf8c5e", - "sha256:9df0c10adf3e83015ced42a9a7bd64e13d06c4cf45c340d2c63020ea04499d0a", - "sha256:b38057450a0c566cbd04890a40edf916db890f2818e8682221611d78dc32ae26", - "sha256:bd95d1dfb9c4f4563e6093a9aa19d9c186bf98fa54da5252531cc0d3a07977e7", - "sha256:c1068287025f8ea025103e37d62ffd63fec8e9e636246b89c341aeda8a67c934", - "sha256:c438268eebb8cf039552897d78f402d734a404f1360592fef55297285f7f953f", - "sha256:cdc076c03381f5c1d9bb1abdcc5503d9ca8b53cf0a9d31a9f6754ec9e6c8af0f", - "sha256:f358aa33e03b7a84e0d91270a4d4d8f5df6921abe99a377828839e8ed0c04e07", - "sha256:f51d5a9f137f7a2cec2d326a74b6e3fc79d635d69ffe1b036d39fc7d75430d37" + "sha256:001c2160c03b6349c04de39cf1a58e342750da3632f6978a1634a3dcca1ec10e", + "sha256:0b250c60256c8824219352dc2a228a6b49987e5bf94d3ffcf4c46585efcbd499", + "sha256:1d24c81c2310f0063b8fc1c20c8ed01f3331be9374b4b5c2de846f69e11e21fb", + "sha256:1eb13f5a5a59ca4973bcfa2fc8fff644bd39f2109c3f7a60bd5860cb6a49b679", + "sha256:25d2fcd6eef340082718ec9ad2c58d734429f2b1f7335d989523852f2bba220b", + "sha256:32bf4a90c207a0b4e70ca6dd09d43de3cb9898f7d5b69c2e9e3b966a7f342820", + "sha256:38fd9eb74b852e4ee14b16e9670cd401d147ee3f3ec0d4f7652e0c921d6227f8", + "sha256:47257d932de14a7b6c4ae1b7dbf592388153ee35ec7cae216b87ae6490ed39a3", + "sha256:4eda68bd9e2a4879385e6b1ea528c976f59cd9728382005cc54c28bcce8db983", + "sha256:52bae32a147c375522ce09bd6af4d2949aca32a0415bc62df1456b3ad17c6001", + "sha256:542f25a4adf3691a306dcc00bf9a73176554938ec9b98f20f929a044f80acf1b", + "sha256:5b5860b790498f233cdc8d635a17fc08de62e59d4dcd8cdb6c6c0d38a31edf2b", + "sha256:6efe066a7135233f97ce51a1aa007d4fb0be28ef093b4f88dac4ad1b3a2b7b6f", + "sha256:71b2c3d1cd26ed1ec7c8196834143258b2ad7f444efff26fdc366c6f5e752702", + "sha256:7a53d4035427b9dbfbb397f46642754d294f131e93c661d056366f2a31438263", + "sha256:7dcd84dc31ebb35ade755e06d1561d1bd3b85e85dbdbf6278011fc97b22810db", + "sha256:88c8be0558bdfc35e68c42ae5bf785eb9390d25915d4863bbc7583d23da77074", + "sha256:8be43a91ab66fe995e85ccdbdd1046d9f0443d59e060c0840319290de25b7d33", + "sha256:8d84453422312f8275455d1cb52d850d6a4d7d714b784e41b573c6f5bfc2a029", + "sha256:9d0f3aca8ca51c8b5e204ab92bd8afdb2a8e3df46bd0ce0bd39065d79aabcaa4", + "sha256:a1eebb6eb0653e594cb86cd8e536b9b083373fca9aba761ade6cd412d46fb2ab", + "sha256:bc14037281db66aa60856cd4ce4541a942040686d290e3f3224dd3978f88f554", + "sha256:fbcbb068ebe67c4ff6483d2e2aa87079c325f8470b24b098d6bf7d4d21d57a69", + "sha256:fd7133b885e356fa4920ead8289bb45dc6f185a164e99e10279f33732ed5ce15" ], - "markers": "python_version >= '3.5'", - "version": "==3.19.4" + "markers": "python_version >= '3.7'", + "version": "==3.20.0" }, "pyasn1": { "hashes": [ @@ -496,6 +497,70 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==2.8.0" }, + "pygame": { + "hashes": [ + "sha256:0427c103f741234336e5606d2fad86f5403c1a3d1dc55c309fbff3c984f0c9ae", + "sha256:07ca9f683075aea9bd977af9f09a720ebf747343d3ea8103e4f1735283b02330", + "sha256:0e06ae8e1c830f1b9c36a2bc6bb11de840232e95b78e2c349c6ed803a303be19", + "sha256:0e97d38308c441942577fea7fcd1326308bc56d6be6c024218e94d075d322e0f", + "sha256:119dee20c372c85dc47b717119534d15a60c64ceab8b0eb09278866d10486afe", + "sha256:1219a963941bd53aa754e8449364c142004fe706c33a9c22ff2a76521a82d078", + "sha256:1fddec8829e96424800c806582d73a5173b7d48946cccf7d035839ca09850db8", + "sha256:20676da24e3e3e6b9fc4eecc7ba09d77ef46c3a83a028763ba1d222476c2e3fb", + "sha256:2405414d8c572668e04739875661e030a0c588e197fa95463fe301c3d0a0510b", + "sha256:24254c4244f0d9bdc904f5d3f38e86757ca4c6aa0e44a6d55ef5e016bc7274d6", + "sha256:24b4f7f30fa2b3d092b60be6fcc725fb91d569fc87a9bcc91614ee8b0c005726", + "sha256:3bb0674aa789848ddc264bfc60c54965bf3bb659c141de4f600e379acc9b944c", + "sha256:3c8d6637ff75351e581327efefa9d04eeb0f257b533392b6cc6b15ceca4f7c5e", + "sha256:40e4d8d65985bb467d9c5a1305fb53fd6820c61d764979600becab973339676f", + "sha256:4aa3ae32320cc704d63e185864e44f6265c2a6e52c9384afe152cc3d51b3a2ef", + "sha256:50d9a21edd551669862c27c9272747401b20b1939abaacb842c08ea1cdd1c04d", + "sha256:5c7600bf307de1ca1dca0cc7840e34604d5b0b0a5a5dad345c3fa62b054b886d", + "sha256:5d0c14152d0ca8ef5fbcc5ed9981462bdf59a9ae85a291e62d8a8d0b7e5cbe43", + "sha256:5e88b0d4338b94960686f59396f23f7f684fed4859fcc3b9f40286d72c1c61af", + "sha256:5ebbefb8b576572c8fc97a3321d37dc2b4afea6b6e3877a67f7158d8c2c4cefe", + "sha256:636f51f56615d67459b11918206bb4da30cd7d7042027bf997c218ccd6c77902", + "sha256:660c80c0b2e80f1f801715583b759fb4c7bc0c11eb3b534e89c9fc4bfbc38acd", + "sha256:6ecda8dd4583982bb65f9c682f244a5e94524dcf628379766227e9ed97201a49", + "sha256:754c2906f2ef47173a14493e1de116b2a56a2c8e1764f1202ba844d080248a5b", + "sha256:7889dce887ec83c9a0bef8d9eb3669d8863fdaf37c45bacec707d8ad90b24a38", + "sha256:7fdb93b4282962c9a2ebf1af994ee698be823dd913218ed97a5f2fb372b10b66", + "sha256:8e87716114e97322fb177e223d889a4be369a0f73212f4f8507fe0cd43253b23", + "sha256:93c4cbfc942dd00410eaa9e84252129f9f9993f37f683006d7b49ab245342254", + "sha256:9649419254d3282dae41f23837de4108b17bc62187c3acd8af2ae3801b765cbd", + "sha256:97a74ba186deee68318a52637012ef6abf5be6282c659e1d1ba6ad08cf35ec85", + "sha256:9d6452419e01a0f848aed0597f69fd10a4c2a7750c15d1b0607f86090a39dcf3", + "sha256:9d7b021b8dde5d528363e474bc18bd6f79a9666eef89fb4859bcb8f0a536c9de", + "sha256:a0ccf8e3dce7ca67d523a6020b7e3dbf4b26797a9a8db5cc4c7b5ef20fb64701", + "sha256:a56a811d8821f7b9a594e3d0e0dd8bd39b25e3eea8963d5963263b90fd2ea5c2", + "sha256:c5ea87da5fe4b6164c3854f3b0c9146811dbad0dd7fa74297683dfacc485ae1c", + "sha256:c99b95e62cdda29c2e60235d7763447c168a6a877403e6f9ca5b2e2bb297c2ce", + "sha256:c9ce7f3d8af14d7e04eb7eb41c5e5313c43508c252bb2b9eb53e51fc87ada9fd", + "sha256:ca5ef1315fa67c241a657ab077be44f230c05740c95f0b46409457dceefdc7e5", + "sha256:d2d3c50ee9847b743db6cd7b1bb17a94c2c2abc16679d70f5e745cabdf19e655", + "sha256:d6d0eca28f886f0477cd0721ac688189155a587f2bb8eae740e52ca56c3ad23c", + "sha256:dad6bf3fdd3752d7519422f3732be779b98fe7c87d32c3efe2fdffdcbeebb6ca", + "sha256:db2f40d5a75fd9cdda473c58b0d8b294da6e0179f00bb3b1fc2f7f29cac09bea", + "sha256:dc4444d61d48c5546df5137cdf81554887ddb6e2ef1be7f51eb77ea3b6bdd56f", + "sha256:dcc285ee1f1d0e2672cc52f880fd3f564b1505de710e817f692fbf64a72ca657", + "sha256:dd528dbb91eca16f7522c975d0f9e94b95f6b5024c82c3247dc0383d242d33c6", + "sha256:e09044e9e1aa8512d6a9c7ce5f94b881824bcfc401105f3c24f546dfc3bb4aa5", + "sha256:e18c9466131378421d00fc40b637425229238d506a073d9c537b230b355a25d6", + "sha256:e1bb25986db77a48f632469c6bc61baf7508ce945aa6161c02180d4ee5ac5b8d", + "sha256:e4b4cd440d50a9f8551b8989e856aab175593af07eb825cad22fd2f8f6f2ffce", + "sha256:e627300a66a90651fb39e41601d447b1fdbbfffca3f08ef0278d6cc0436b2160", + "sha256:e7a8e18677e0064b7a422f6653a622652d932826a27e50f279d55a8b122a1a83", + "sha256:e8632f6b2ddb90f6f3950744bd65d5ef15af615e3034057fa30ff836f48a7179", + "sha256:ea36f4f93524554a35cac2359df63b50af6556ed866830aa1f07f0d8580280ea", + "sha256:f149e182d0eeef15d8a9b4c9dad1b87dc6eba3a99bd3c44a777a3a2b053a3dca", + "sha256:fc2e5db54491e8f27785fc5204c96f540d3557dcf5b0a9a857b6594d6b32561b", + "sha256:fc30e834f65b893d1b4c230070183bf98e6b70c41c1511687e8436a33d5ce49d", + "sha256:fcc9586e17875c0cdf8764597955f9daa979098fd4f80be07ed68276ac225480", + "sha256:ff961c3280d6ee5f4163f4772f963d7a4dbe42e36c6dd54b79ad436c1f046e5d" + ], + "index": "pypi", + "version": "==2.1.2" + }, "pyparsing": { "hashes": [ "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea", @@ -536,6 +601,14 @@ "markers": "python_version >= '3.6'", "version": "==4.8" }, + "setuptools": { + "hashes": [ + "sha256:7999cbd87f1b6e1f33bf47efa368b224bed5e27b5ef2c4d46580186cbcb1a86a", + "sha256:a65e3802053e99fc64c6b3b29c11132943d5b8c8facbcc461157511546510967" + ], + "markers": "python_version >= '3.7'", + "version": "==62.0.0" + }, "six": { "hashes": [ "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", @@ -587,6 +660,7 @@ "tensorflow-io-gcs-filesystem": { "hashes": [ "sha256:2862e0869453ce1f872a28d1362768ee078ec227ea587dd69164081dea6d7177", + "sha256:2a9c7f26ef9248bdfccc91fdddd66623754a6b08bd4440a780f23feaed8c5be7", "sha256:2f67d19a2f2579dc55f1590faf48c2e882cabb860992b5a9c7edb0ed8b3eb187", "sha256:658764aaaf9419ddefb3daa95bdc84e5210c691ff73b8ac2606d5c839040206b", "sha256:6e65009770a05a3b55c5f782348f785e5034d277a727832811ad737bd857c8c9", @@ -635,19 +709,19 @@ }, "urllib3": { "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" + "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", + "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.8" + "version": "==1.26.9" }, "werkzeug": { "hashes": [ - "sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8", - "sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c" + "sha256:3c5493ece8268fecdcdc9c0b112211acd006354723b280d643ec732b6d4063d6", + "sha256:f8e89a20aeabbe8a893c24a461d3ee5dad2123b05cc6abd73ceed01d39c3ae74" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.3" + "markers": "python_version >= '3.7'", + "version": "==2.1.1" }, "wheel": { "hashes": [ @@ -729,21 +803,21 @@ }, "zipp": { "hashes": [ - "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d", - "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375" + "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", + "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" ], "markers": "python_version >= '3.7'", - "version": "==3.7.0" + "version": "==3.8.0" } }, "develop": { "astroid": { "hashes": [ - "sha256:1efdf4e867d4d8ba4a9f6cf9ce07cd182c4c41de77f23814feb27ca93ca9d877", - "sha256:506daabe5edffb7e696ad82483ad0228245a9742ed7d2d8c9cdb31537decf9f6" + "sha256:8d0a30fe6481ce919f56690076eafbb2fb649142a89dc874f1ec0e7a011492d0", + "sha256:cc8cc0d2d916c42d0a7c476c57550a4557a083081976bf42a73414322a6411d9" ], "markers": "python_full_version >= '3.6.2'", - "version": "==2.9.3" + "version": "==2.11.2" }, "attrs": { "hashes": [ @@ -761,6 +835,14 @@ "index": "pypi", "version": "==1.6.0" }, + "dill": { + "hashes": [ + "sha256:7e40e4a70304fd9ceab3535d36e58791d9c4a776b38ec7f7ec9afc8d3dca4d4f", + "sha256:9f9734205146b2b353ab3fec9af0070237b6ddae78452af83d2fca84d739e675" + ], + "markers": "python_version >= '2.7' and python_version != '3.0'", + "version": "==0.3.4" + }, "iniconfig": { "hashes": [ "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", @@ -821,10 +903,11 @@ }, "mccabe": { "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" ], - "version": "==0.6.1" + "markers": "python_version >= '3.6'", + "version": "==0.7.0" }, "packaging": { "hashes": [ @@ -868,11 +951,11 @@ }, "pylint": { "hashes": [ - "sha256:9d945a73640e1fec07ee34b42f5669b770c759acd536ec7b16d7e4b87a9c9ff9", - "sha256:daabda3f7ed9d1c60f52d563b1b854632fd90035bcf01443e234d3dc794e3b74" + "sha256:c149694cfdeaee1aa2465e6eaab84c87a881a7d55e6e93e09466be7164764d1e", + "sha256:dab221658368c7a05242e673c275c488670144123f4bd262b2777249c1c0de9b" ], "index": "pypi", - "version": "==2.12.2" + "version": "==2.13.5" }, "pyparsing": { "hashes": [ @@ -884,11 +967,19 @@ }, "pytest": { "hashes": [ - "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db", - "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171" + "sha256:841132caef6b1ad17a9afde46dc4f6cfa59a05f9555aae5151f73bdf2820ca63", + "sha256:92f723789a8fdd7180b6b06483874feca4c48a5c76968e03bb3e7f806a1869ea" ], "index": "pypi", - "version": "==7.0.1" + "version": "==7.1.1" + }, + "setuptools": { + "hashes": [ + "sha256:7999cbd87f1b6e1f33bf47efa368b224bed5e27b5ef2c4d46580186cbcb1a86a", + "sha256:a65e3802053e99fc64c6b3b29c11132943d5b8c8facbcc461157511546510967" + ], + "markers": "python_version >= '3.7'", + "version": "==62.0.0" }, "toml": { "hashes": [ @@ -903,7 +994,7 @@ "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" ], - "markers": "python_version >= '3.7'", + "markers": "python_version < '3.11'", "version": "==2.0.1" }, "typing-extensions": { diff --git a/cli/.DS_Store b/cli/.DS_Store new file mode 100644 index 0000000..80862e2 Binary files /dev/null and b/cli/.DS_Store differ diff --git a/cli/play_snake.py b/cli/play_snake.py new file mode 100644 index 0000000..b41e1d6 --- /dev/null +++ b/cli/play_snake.py @@ -0,0 +1,62 @@ +"""CLI to play the snake game manually""" +import pygame + +from game.snake import SnakeGame, player_to_snake_perspective + + +def play_snake(): + """Initialize and run the game loop""" + pygame.init() + + game = SnakeGame() + + speed = 20 + clock = pygame.time.Clock() + stop = False + # game loop + while True: + + # 1. collect user input + action = "forward" + for event in pygame.event.get(): + if event.type == pygame.QUIT: + game.quit() + stop = True + + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_q: + game.quit() + stop = True + + if event.key in [ + pygame.K_LEFT, + pygame.K_RIGHT, + pygame.K_UP, + pygame.K_DOWN]: # any other key keeps forward + + player_direction = { + pygame.K_LEFT: "left", + pygame.K_RIGHT: "right", + pygame.K_UP: "up", + pygame.K_DOWN: "down" + }[event.key] + + action = player_to_snake_perspective(game.direction, + player_direction) + if stop: + break + + _, score, game_over = game.play_step(action) + game.pygame_draw() + clock.tick(speed) + + if game_over: + break + + print('Final Score', score) + + pygame.quit() + + +if __name__ == '__main__': + play_snake() diff --git a/cli/qlearning_snake.py b/cli/qlearning_snake.py new file mode 100644 index 0000000..64fb78a --- /dev/null +++ b/cli/qlearning_snake.py @@ -0,0 +1,198 @@ +"""CLI to train/see QLearning in action solving the snake game""" +import argparse +import numpy as np +import pygame + +from reinforcement_learning.q_learning import ( + SnakeAgent, ai_direction_to_snake, + QTrainer, linear_qnet, snake_state_11, snake_reward +) +from game.snake import SnakeGame +from plotter import gamescore_plotter + + +def buil_arg_parser(): + """Parses the user's arguments""" + parser = argparse.ArgumentParser( + description="Use Deep-QLearning in the snake game", + epilog="Built with <3 by Emmanuel Byrd at 8th Light Ltd.") + parser.add_argument( + "--best-models-dir", + metavar="./model", default="./model", type=str, + help="Folder to store the increasingly best models" + ) + parser.add_argument( + "--score-history", + metavar="./score_history", default="./score_history", type=str, + help="Where to store the score history" + ) + parser.add_argument( + "--checkpoint-path", metavar="./model/snake_5.pth", type=str, + help="Path of pre-trained model to start from" + ) + parser.add_argument( + "--fps", metavar="100", type=int, default=100, + help="Frames per second" + ) + parser.add_argument( + "--learning-rate", metavar="1e-3", type=float, default=1e-3, + help="QTrainer learning rate" + ) + parser.add_argument( + "--gamma", metavar="0.9", type=float, default=0.9, + help="QTrainer gamma value" + ) + parser.add_argument( + "--hidden-layer-size", metavar="256", type=int, default=256, + help="Size of the hidden layer" + ) + parser.add_argument( + "--max-width", metavar="400", type=int, default=400, + help="Maximum board width" + ) + parser.add_argument( + "--max-height", metavar="300", type=int, default=300, + help="Maximum board height" + ) + return parser + + +def train(args): + """Execute AI training/game loop""" + pygame.init() + + score_tracker = ScoreTracker() + + high_score = 0 + + agent = SnakeAgent( + QTrainer(generate_model(args), + learning_rate=args.learning_rate, + gamma=args.gamma) + ) + game = SnakeGame(width=200, height=160) + + clock = pygame.time.Clock() + + game_frames = 0 + + while True: + # get old state + state = snake_state_11(game) + + # get move + action = agent.get_action(state) + # [0, 0, 0] -> left, right, forward + + # perform move and get new state + eaten, score, done = game.play_step(ai_direction_to_snake(action)) + + # show AI training in real-time + for event in pygame.event.get(): + if event.type == pygame.QUIT or ( + event.type == pygame.KEYDOWN and event.key == pygame.K_q): + pygame.quit() + return + + # drawing requires to consume events e.g. pygame.event.get() + game.pygame_draw() # draw the game + clock.tick(args.fps) + + reward = snake_reward(eaten, done) + + game_frames += 1 + if game_frames > 30 * len(game.snake): + eaten = False + done = True + reward = -10 + print("Stopping due to infinite loop strategy") + + state_next = snake_state_11(game) + # train short memory + agent.train_short_memory(state, action, reward, state_next, done) + + # remember + agent.remember(state, action, reward, state_next, done) + + if done: + if score > high_score: + high_score = score + agent.save_model(args.best_models_dir, + f'snake_{high_score}.pth') + + game = scaling_board(high_score, + args.max_width, args.max_height) + + game_frames = 0 + agent.n_games += 1 + + # train long memory (replay memory, or experience replay) + agent.train_long_memory() + + print('Game', agent.n_games, 'Score', score, 'Record:', high_score) + + # show the results + score_tracker.add_new_score(score) + score_tracker.show_hist() + np.save(args.score_history, np.array(score_tracker.get_hist())) + + +def generate_model(args): + """Generate a linear neural network of input 11 and output 3""" + model = linear_qnet(11, args.hidden_layer_size, 3) + if args.checkpoint_path: + model.load_weights(args.checkpoint_path) + + return model + + +def scaling_board(high_score, max_width, max_height): + """Choose the appropriate size for the next game depending on the score""" + if high_score > 5: + return SnakeGame() + + if high_score > 3: + return SnakeGame(width=max_width, height=max_height) + + if high_score > 1: + return SnakeGame(width=320, height=240) + + return SnakeGame(width=200, height=160) + + +class ScoreTracker: + """State class that keeps updated information on the score""" + + def __init__(self): + """Initialize analysis variables""" + self.plot_scores = [] + self.plot_mean_scores = [] + self.total_score = 0 + + def add_new_score(self, score): + """Adds the given score and calculates the average so far""" + self.plot_scores.append(score) + self.total_score += score + self.plot_mean_scores.append(self.total_score / len(self.plot_scores)) + + def show_hist(self): + """Plot all the stored information""" + gamescore_plotter(self.plot_scores, self.plot_mean_scores) + + def get_hist(self): + """Returns a list with the scores and mean scores""" + return [self.plot_scores, self.plot_mean_scores] + + +def main(): + """Main function""" + arg_parser = buil_arg_parser() + args = arg_parser.parse_args() + + train(args) + + print("Finished.") + + +if __name__ == "__main__": + main() diff --git a/src/game/__init__.py b/src/game/__init__.py new file mode 100644 index 0000000..d18f5dd --- /dev/null +++ b/src/game/__init__.py @@ -0,0 +1,2 @@ +"""All games should be playable by both humans and AI""" +from .snake import * diff --git a/src/game/snake/__init__.py b/src/game/snake/__init__.py new file mode 100644 index 0000000..f40bf2e --- /dev/null +++ b/src/game/snake/__init__.py @@ -0,0 +1,2 @@ +"""Simple game of snake""" +from .snake import * diff --git a/src/game/snake/snake.py b/src/game/snake/snake.py new file mode 100644 index 0000000..b042b79 --- /dev/null +++ b/src/game/snake/snake.py @@ -0,0 +1,211 @@ +"""SnakeGame. It's interface is suitable for humans an AI programs'""" +# pylint: disable=too-many-instance-attributes + +# Taken from +# https://github.com/python-engineer/python-fun/blob/master/snake-pygame/snake_game.py +import random +from enum import Enum +from collections import namedtuple +import pygame + + +class Direction(Enum): + """Direction enum used to know where the snake is heading towards""" + RIGHT = 1 + LEFT = 2 + UP = 3 + DOWN = 4 + + +Point = namedtuple('Point', 'coordx, coordy') + +# rgb colors +WHITE = (255, 255, 255) +RED = (200, 0, 0) +GREEN = (0, 200, 50) +BLACK = (0, 0, 0) + + +class SnakeGame: + """Simple 2D snake game""" + + def __init__(self, width=640, height=480): + """Initialize the graphic blocks and a snake of size 3 heading right""" + self.block_size = 20 + self.width = width + self.height = height + + self.display = None + self.font = None + + self.direction = Direction.RIGHT + + self.head = Point(self.width / 2, self.height / 2) + self.snake = [self.head, + Point(self.head.coordx - self.block_size, + self.head.coordy), + Point(self.head.coordx - (2 * self.block_size), + self.head.coordy)] + + self.score = 0 + self.food = None + self._place_food() + + def _place_food(self): + """Place the food randomly in a non-colliding coordinate""" + while True: + coordx = random.randint(0, (self.width - self.block_size) // + self.block_size) * self.block_size + coordy = random.randint(0, (self.height - self.block_size) // + self.block_size) * self.block_size + self.food = Point(coordx, coordy) + if self.food not in self.snake: + break + + def _choose_direction(self, action): + """Rotate the snake given the provided `action` string""" + clockwise = [Direction.UP, Direction.RIGHT, + Direction.DOWN, Direction.LEFT] + current = clockwise.index(self.direction) + + if action == "left": + return clockwise[(current - 1) % 4] + if action == "right": + return clockwise[(current + 1) % 4] + if action == "forward": + return self.direction + + raise ValueError("Unknown action: " + str(action)) + + def play_step(self, action): + """Receive an action and execute its effects of moving and colliding""" + if action not in ["left", "right", "forward"]: + raise ValueError("Unknown action: " + str(action)) + + self.direction = self._choose_direction(action) + + # move + self._move(self.direction) # update the head + self.snake.insert(0, self.head) # update the snake + + # check if game over + eaten = False + game_over = False + if self.is_collision(self.head): + game_over = True + return eaten, self.score, game_over + + # place new food or just move + if self.head == self.food: + self.score += 1 + eaten = True + self._place_food() + else: + self.snake.pop() + + # return game over and score + return eaten, self.score, game_over + + def is_collision(self, point): + """Returns true if the current state is a collision""" + # hits boundary + if point.coordx > self.width - self.block_size or \ + point.coordy > self.height - self.block_size or \ + point.coordx < 0 or point.coordy < 0: + return True + # hits itself + if point in self.snake[1:]: + return True + + return False + + def pygame_draw(self): + """Uses pygame to draw the game in the screen""" + if self.display is None: + self.font = pygame.font.Font(pygame.font.get_default_font(), 25) + + self.display = pygame.display.set_mode((self.width, self.height)) + pygame.display.set_caption('Snake') + + self.display.fill(BLACK) + + for body_point in self.snake: + pygame.draw.rect( + self.display, + GREEN, + pygame.Rect( + body_point.coordx, + body_point.coordy, + self.block_size, + self.block_size)) + + pygame.draw.rect( + self.display, + RED, + pygame.Rect( + self.food.coordx, + self.food.coordy, + self.block_size, + self.block_size)) + + text = self.font.render("Score: " + + str(self.score), True, (255, 255, 255)) + self.display.blit(text, [0, 0]) + pygame.display.flip() + + def _move(self, direction): + """Adds a new head according to the given direction""" + coordx = self.head.coordx + coordy = self.head.coordy + if direction == Direction.RIGHT: + coordx += self.block_size + elif direction == Direction.LEFT: + coordx -= self.block_size + elif direction == Direction.DOWN: + coordy += self.block_size + elif direction == Direction.UP: + coordy -= self.block_size + + self.head = Point(coordx, coordy) + + def quit(self): + """Quit display""" + if self.display: + pygame.quit() + +# player_direction: ['up', 'left', 'right', 'down'] + + +def player_to_snake_perspective(snake_direction, player_direction): + """Transforms universal directions (player) to local directions (snake)""" + if snake_direction == Direction.UP: + return { + 'up': 'forward', + 'left': 'left', + 'down': 'forward', # <- no tail crash + 'right': 'right' + }[player_direction] + + if snake_direction == Direction.LEFT: + return { + 'up': 'right', + 'left': 'forward', + 'down': 'left', + 'right': 'forward' # <- no tail crash + }[player_direction] + + if snake_direction == Direction.DOWN: + return { + 'up': 'forward', # <- no tail crash + 'left': 'right', + 'down': 'forward', + 'right': 'left' + }[player_direction] + + # Direction.RIGHT + return { + 'up': 'left', + 'left': 'forward', # <- no tail crash + 'down': 'right', + 'right': 'forward' + }[player_direction] diff --git a/src/game/snake/snake_test.py b/src/game/snake/snake_test.py new file mode 100644 index 0000000..4b93a36 --- /dev/null +++ b/src/game/snake/snake_test.py @@ -0,0 +1,134 @@ +"""Tests the .snake file""" +# pylint: disable=missing-function-docstring +from .snake import SnakeGame, Direction, Point, player_to_snake_perspective + + +def test_initial_direction_is_right(): + game = SnakeGame() + assert game.direction == Direction.RIGHT + + +def test_snake_is_size_3(): + game = SnakeGame() + assert len(game.snake) == 3 + + +def test_direction_right(): + game = SnakeGame() + game.food = Point(0, 0) + + eaten, score, game_over = game.play_step("right") + assert game.direction == Direction.DOWN + assert eaten is False + assert score == 0 + assert game_over is False + + +def test_direction_forward(): + game = SnakeGame() + game.food = Point(0, 0) + + eaten, score, game_over = game.play_step("forward") + assert game.direction == Direction.RIGHT + assert eaten is False + assert score == 0 + assert game_over is False + + +def test_direction_left(): + game = SnakeGame() + game.food = Point(0, 0) + + eaten, score, game_over = game.play_step("left") + assert game.direction == Direction.UP + assert eaten is False + assert score == 0 + assert game_over is False + + +def player_to_snake_perspective_when_facing_up(): + orders = ["up", "down", "left", "right"] + expected_actions = ["forward", "forward", "left", "right"] + + for player_order, expected_action in zip(orders, expected_actions): + snake_action = player_to_snake_perspective(Direction.UP, player_order) + assert snake_action == expected_action + + +def player_to_snake_perspective_when_facing_right(): + orders = ["up", "down", "left", "right"] + expected_actions = ["left", "right", "forward", "forward"] + + for player_order, expected_action in zip(orders, expected_actions): + snake_action = player_to_snake_perspective( + Direction.RIGHT, player_order) + assert snake_action == expected_action + + +def player_to_snake_perspective_when_facing_left(): + orders = ["up", "down", "left", "right"] + expected_actions = ["right", "left", "forward", "forward"] + + for player_order, expected_action in zip(orders, expected_actions): + snake_action = player_to_snake_perspective( + Direction.LEFT, player_order) + assert snake_action == expected_action + + +def player_to_snake_perspective_when_facing_down(): + orders = ["up", "down", "left", "right"] + expected_actions = ["forward", "forward", "right", "left"] + + for player_order, expected_action in zip(orders, expected_actions): + snake_action = player_to_snake_perspective( + Direction.DOWN, player_order) + assert snake_action == expected_action + + +def food_is_eaten(): + game = SnakeGame() + game.food = Point(game.head.coordx + game.block_size, game.head.coordy) + eaten, score, game_over = game.play_step("forward") + + assert eaten is True + assert score == 1 + assert game_over is False + + +def can_collide_with_body(): + game = SnakeGame() + game.snake.append( + Point(game.head.coordx + game.block_size, game.head.coordy) + ) + eaten, score, game_over = game.play_step("forward") + + assert eaten is False + assert score == 0 + assert game_over is True + + +def can_collide_with_border(): + game = SnakeGame() + new_head = Point(game.width, game.height / 2) + assert game.is_collision(new_head) is False + + eaten, score, game_over = game.play_step("forward") + + assert eaten is False + assert score == 0 + assert game_over is True + + +def border_collisions(): + game = SnakeGame() + assert game.is_collision(Point(0, game.height - 2)) is False + assert game.is_collision(Point(-1, game.height - 2)) is True + + assert game.is_collision(Point(game.width / 2, 0)) is False + assert game.is_collision(Point(game.width / 2, -1)) is True + + assert game.is_collision(Point(game.width, game.height - 2)) is False + assert game.is_collision(Point(game.width + 1, game.height - 2)) is True + + assert game.is_collision(Point(game.width / 2, game.height)) is False + assert game.is_collision(Point(game.width / 2, game.height + 1)) is True diff --git a/src/plotter/__init__.py b/src/plotter/__init__.py index 3c61937..fb9f468 100644 --- a/src/plotter/__init__.py +++ b/src/plotter/__init__.py @@ -1,2 +1,3 @@ """Custom plotting functionality""" from .multiplot import * +from .gamescore_plotter import * diff --git a/src/plotter/gamescore_plotter.py b/src/plotter/gamescore_plotter.py new file mode 100644 index 0000000..735cbec --- /dev/null +++ b/src/plotter/gamescore_plotter.py @@ -0,0 +1,16 @@ +"""Plotter useful in games""" +import matplotlib.pyplot as plt + +plt.ion() + + +def gamescore_plotter(scores, mean_scores): + """Plots the scores and mean scores""" + plt.clf() + plt.title('Training...') + plt.xlabel('Number of Games') + plt.ylabel('Score') + plt.plot(scores) + plt.plot(mean_scores) + plt.text(len(scores) - 1, scores[-1], str(scores[-1])) + plt.text(len(mean_scores) - 1, mean_scores[-1], str(mean_scores[-1])) diff --git a/src/plotter/multiplot.py b/src/plotter/multiplot.py index 52b76cf..58b3606 100644 --- a/src/plotter/multiplot.py +++ b/src/plotter/multiplot.py @@ -6,6 +6,7 @@ class MultiPlot: """ MultiPlot receives images and creates a Matplotlib figure for them """ + def __init__(self): """Initialize variables images, names, cmaps and figure""" self.images = [] diff --git a/src/reinforcement_learning/__init__.py b/src/reinforcement_learning/__init__.py new file mode 100644 index 0000000..e8b5dd9 --- /dev/null +++ b/src/reinforcement_learning/__init__.py @@ -0,0 +1,2 @@ +"""Reinforcement Learning module""" +from .q_learning import * diff --git a/src/reinforcement_learning/q_learning/__init__.py b/src/reinforcement_learning/q_learning/__init__.py new file mode 100644 index 0000000..6659324 --- /dev/null +++ b/src/reinforcement_learning/q_learning/__init__.py @@ -0,0 +1,3 @@ +"""QLearning agent and training model""" +from .agent import * +from .model import * diff --git a/src/reinforcement_learning/q_learning/agent.py b/src/reinforcement_learning/q_learning/agent.py new file mode 100644 index 0000000..69bc3f7 --- /dev/null +++ b/src/reinforcement_learning/q_learning/agent.py @@ -0,0 +1,182 @@ +"""Agent that handles the training timing and the rewards""" +# pylint: disable=too-many-arguments +import random +import os +from collections import deque +import numpy as np +import tensorflow as tf + +from game.snake import Direction, Point + +def as_tensors(states, actions, rewards, next_states, dones): + """Converts arrays to tensors""" + state = tf.constant(states, dtype=tf.float32) + action = tf.constant(actions, dtype=tf.float32) + reward = tf.constant(rewards, dtype=tf.float32) + next_state = tf.constant(next_states, dtype=tf.float32) + done = tf.constant(dones, dtype=tf.float32) + + return state, action, reward, next_state, done + +def add_dimension(states, actions, rewards, next_states, dones): + """Expands with a dimension. To use when wanting (1,x) but having (1,)""" + state = tf.expand_dims(states, axis=0) + action = tf.expand_dims(actions, axis=0) + reward = tf.expand_dims(rewards, axis=0) + next_state = tf.expand_dims(next_states, axis=0) + done = tf.expand_dims(dones, axis=0) + + return state, action, reward, next_state, done + +def snake_state_11(game): + """ + Gets the 11 state vector state of the snake game. + It uses 3 places for danger, 4 for direction and 4 for food location + """ + head = game.snake[0] + point_l = Point(head.coordx - game.block_size, head.coordy) + point_r = Point(head.coordx + game.block_size, head.coordy) + point_u = Point(head.coordx, head.coordy - game.block_size) + point_d = Point(head.coordx, head.coordy + game.block_size) + + dir_l = game.direction == Direction.LEFT + dir_r = game.direction == Direction.RIGHT + dir_u = game.direction == Direction.UP + dir_d = game.direction == Direction.DOWN + + state = [ + # Danger straight + (dir_r and game.is_collision(point_r)) or + (dir_l and game.is_collision(point_l)) or + (dir_u and game.is_collision(point_u)) or + (dir_d and game.is_collision(point_d)), + + # Danger right + (dir_u and game.is_collision(point_r)) or + (dir_d and game.is_collision(point_l)) or + (dir_l and game.is_collision(point_u)) or + (dir_r and game.is_collision(point_d)), + + # Danger left + (dir_d and game.is_collision(point_r)) or + (dir_u and game.is_collision(point_l)) or + (dir_r and game.is_collision(point_u)) or + (dir_l and game.is_collision(point_d)), + + # Move direction + dir_l, + dir_r, + dir_u, + dir_d, + + # Food location + game.food.coordx < game.head.coordx, # food left + game.food.coordx > game.head.coordx, # food right + game.food.coordy < game.head.coordy, # food up + game.food.coordy > game.head.coordy # food down + ] + return np.array(state, dtype=int) # zeroes or ones + + +def snake_reward(eaten: bool, done: bool) -> int: + """Gets reward based on the eaten and done parameters""" + if not isinstance(eaten, bool) or not isinstance(done, bool): + raise ValueError("Reward function only receives booleans") + + if eaten: + return 10 + + if done: + return -10 + + return 0 + + +class SnakeAgent: + """Has the data of all past states and trains the model accordingly""" + + def __init__( + self, + trainer, + max_memory=100_000, + batch_size=1000, + epsilon=0): + """Initializes a deque memory so that we don't have to""" + self.n_games = 0 + self.epsilon = epsilon # control the randomness + self.memory = deque(maxlen=max_memory) + + self.trainer = trainer + self.batch_size = batch_size + + def remember(self, state, action, reward, next_state, done): + """ + Appens the variables to memory and pops left if maximum memory + is reached. + """ + self.memory.append((state, action, reward, next_state, done)) + + def train_long_memory(self): + """Gets a random sample of the data of states to replay in training""" + if len(self.memory) > self.batch_size: + mini_sample = random.sample( + self.memory, self.batch_size) # list of tuples + else: + mini_sample = self.memory + + states, actions, rewards, next_states, dones = zip(*mini_sample) + states, actions, rewards, next_states, dones = as_tensors( + states, actions, rewards, next_states, dones + ) + + self.trainer.train_step(states, actions, rewards, next_states, dones) + + def train_short_memory(self, state, action, reward, next_state, done): + """Trains using the given single state""" + state, action, reward, next_state, done = as_tensors( + state, action, reward, next_state, done + ) + state, action, reward, next_state, done = add_dimension( + state, action, reward, next_state, done) + self.trainer.train_step(state, action, reward, next_state, done) + + def get_action(self, state): + """Gets an action using an exploration/eploitation tradeoff""" + self.epsilon = 80 - self.n_games + final_move = [0, 0, 0] # left, right, forward + if random.randint(0, 200) < self.epsilon: # explore + move = random.randint(0, 2) + final_move[move] = 1 + else: + state0 = tf.constant([state], dtype=tf.float32) # create a tensor + prediction = self.trainer.model.predict(state0) + # Get index of maximum value + move = tf.argmax(prediction[0]).numpy() + final_move[move] = 1 + + return final_move + + def save_model(self, model_dir_path="./model", file_name='model.pth'): + """Saves the model as a .pth folder""" + if not os.path.exists(model_dir_path): + os.mkdir(model_dir_path) + + file_name = os.path.join(model_dir_path, file_name) + self.trainer.model.save(file_name) + + +def ai_direction_to_snake(action: list): + """Transforms a vector of actions to the snake game's perspective string""" + if not isinstance(action, list): + raise ValueError("Action must be a list") + + if action == [1, 0, 0]: + return "left" + + if action == [0, 1, 0]: + return "right" + + if action == [0, 0, 1]: + return "forward" + + raise ValueError("Unknown action: " + str(action)) diff --git a/src/reinforcement_learning/q_learning/model.py b/src/reinforcement_learning/q_learning/model.py new file mode 100644 index 0000000..16437e4 --- /dev/null +++ b/src/reinforcement_learning/q_learning/model.py @@ -0,0 +1,62 @@ +"""Holds the NN model and its parameter update functionality""" +# pylint: disable=too-many-arguments,too-few-public-methods +import tensorflow as tf +from tensorflow import keras + + +def linear_qnet(input_size: int, hidden_size: int, + output_size: int) -> keras.Model: + """Creates a 1-hidden-layer dense neural network""" + inputs = keras.layers.Input(shape=input_size, name="Input") + + layer1 = keras.layers.Dense( + hidden_size, + activation="relu", + name="Dense_1")(inputs) + action = keras.layers.Dense(output_size, name="Dense_2")(layer1) + + return keras.Model(inputs=inputs, outputs=action) + + +class QTrainer(): + """Trains the given model according to an optimizer and loss function""" + + def __init__(self, model, learning_rate=1e-4, gamma=0.9): + """Uses the ADAM optimizer and MeanSquaredError loss function""" + self.model = model + self.gamma = gamma + + self.optimizer = keras.optimizers.Adam(learning_rate=learning_rate) + self.loss_object = keras.losses.MeanSquaredError() + + + def train_step(self, states, actions, rewards, next_states, dones): + """ + Updates the model's parameters by calculating their derivatives + with respect to the loss function + """ + future_rewards = self.model.predict(next_states) + # Q value = reward + discount factor * expected future reward + updated_q_values = rewards + self.gamma * tf.reduce_max( + future_rewards, axis=1 + ) + + updated_q_values = updated_q_values * (1 - dones) + + masks = actions + + with tf.GradientTape() as tape: + tape.watch(self.model.trainable_variables) + # train the model on the states and updated Q-values + q_values = self.model(states) # similar to action_probs + + # apply the masks to the Q-values to get the Q-value for the action + # taken + q_action = tf.reduce_sum(tf.multiply(q_values, masks), axis=1) + # calculate loss between new Q-value and old Q-value + loss = self.loss_object(updated_q_values, q_action) + + # Backpropagation + grads = tape.gradient(loss, self.model.trainable_variables) + self.optimizer.apply_gradients( + zip(grads, self.model.trainable_variables)) diff --git a/src/reinforcement_learning/q_learning/test/__init__.py b/src/reinforcement_learning/q_learning/test/__init__.py new file mode 100644 index 0000000..fc933fb --- /dev/null +++ b/src/reinforcement_learning/q_learning/test/__init__.py @@ -0,0 +1 @@ +"""Tests for the q_learning module""" diff --git a/src/reinforcement_learning/q_learning/test/agent_test.py b/src/reinforcement_learning/q_learning/test/agent_test.py new file mode 100644 index 0000000..ed484e5 --- /dev/null +++ b/src/reinforcement_learning/q_learning/test/agent_test.py @@ -0,0 +1,218 @@ +"""Tests the agent.py file""" +# pylint: disable=missing-function-docstring +import pytest +import tensorflow as tf +import numpy as np + +from game.snake import Direction, SnakeGame, Point +from ..agent import SnakeAgent, ai_direction_to_snake, snake_state_11, snake_reward +from ..model import QTrainer, linear_qnet + +placeholder_trainer = QTrainer(linear_qnet(11, 5, 3)) + + +def test_ai_direction_left(): + snake_direction = ai_direction_to_snake([1, 0, 0]) + assert snake_direction == "left" + + +def test_ai_direction_right(): + snake_direction = ai_direction_to_snake([0, 1, 0]) + assert snake_direction == "right" + + +def test_ai_direction_forward(): + snake_direction = ai_direction_to_snake([0, 0, 1]) + assert snake_direction == "forward" + + +def test_ai_direction_correct_parameter(): + with pytest.raises(ValueError) as e_info: + ai_direction_to_snake([1, 1, 1]) + + assert str(e_info.value) == "Unknown action: [1, 1, 1]" + + +def test_ai_direction_cannot_receive_tensor(): + with pytest.raises(ValueError) as e_info: + ai_direction_to_snake(tf.constant([1, 0, 0])) + + assert str(e_info.value) == "Action must be a list" + + +def test_ai_direction_cannot_receive_numpy_array(): + with pytest.raises(ValueError) as e_info: + ai_direction_to_snake(np.array([1, 0, 0])) + + assert str(e_info.value) == "Action must be a list" + + +def test_reward_correct_parameters(): + with pytest.raises(ValueError) as e_info: + snake_reward(False, 0.56) + + assert str(e_info.value) == "Reward function only receives booleans" + + with pytest.raises(ValueError) as e_info: + snake_reward(0.42, True) + + assert str(e_info.value) == "Reward function only receives booleans" + + +def test_reward_on_done(): + reward = snake_reward(False, True) + assert reward == -10 + + +def test_reward_on_eaten(): + reward = snake_reward(True, False) + assert reward == 10 + + +def test_reward_on_default(): + reward = snake_reward(False, False) + assert reward == 0 + + +def test_get_action_shape(): + agent = SnakeAgent(placeholder_trainer) + game = SnakeGame() + state = snake_state_11(game) + action = agent.get_action(state) + + assert isinstance(action, list) + assert len(action) == 3 + assert sum(action) == 1 + for elem in action: + assert isinstance(elem, int) + + +def test_defaults(): + agent = SnakeAgent(placeholder_trainer) + + assert agent.n_games == 0 + assert agent.epsilon == 0 + assert agent.batch_size == 1000 + +def test_get_state_shape(): + game = SnakeGame() + state = snake_state_11(game) + + assert isinstance(state, np.ndarray) + assert state.shape == (11,) + + +def test_state_danger_right(): + game = SnakeGame() + unit = game.block_size + coordx = game.head.coordx + coordy = game.head.coordy + + directions = [Direction.UP, Direction.RIGHT, + Direction.DOWN, Direction.LEFT] + tails = [Point(coordx + unit, coordy), Point(coordx, coordy + unit), + Point(coordx - unit, coordy), Point(coordx, coordy - unit)] + + for direction, tail in zip(directions, tails): + game.direction = direction + game.snake = [game.head, tail] + state = snake_state_11(game) + assert (state[:3] == [0, 1, 0]).all() + + +def test_state_danger_straight(): + game = SnakeGame() + unit = game.block_size + coordx = game.head.coordx + coordy = game.head.coordy + + directions = [Direction.UP, Direction.RIGHT, + Direction.DOWN, Direction.LEFT] + tails = [Point(coordx, coordy - unit), Point(coordx + unit, coordy), + Point(coordx, coordy + unit), Point(coordx - unit, coordy)] + + for direction, tail in zip(directions, tails): + game.direction = direction + game.snake = [game.head, tail] + state = snake_state_11(game) + assert (state[:3] == [1, 0, 0]).all() + + +def test_state_danger_left(): + game = SnakeGame() + unit = game.block_size + coordx = game.head.coordx + coordy = game.head.coordy + + directions = [Direction.UP, Direction.RIGHT, + Direction.DOWN, Direction.LEFT] + tails = [Point(coordx - unit, coordy), Point(coordx, coordy - unit), + Point(coordx + unit, coordy), Point(coordx, coordy + unit)] + + for direction, tail in zip(directions, tails): + game.direction = direction + game.snake = [game.head, tail] + state = snake_state_11(game) + assert (state[:3] == [0, 0, 1]).all() + + +def test_state_directions(): + game = SnakeGame() + indexes = [3, 4, 5, 6] + directions = [Direction.LEFT, Direction.RIGHT, + Direction.UP, Direction.DOWN] + + for idx, direction in zip(indexes, directions): + game.direction = direction + state = snake_state_11(game) + other_indexes = [i for i in indexes if i != idx] + assert state[idx] == 1 + for other_idx in other_indexes: + assert state[other_idx] == 0 + + +def test_state_food_locations(): + game = SnakeGame() + unit = game.block_size + coordx = game.head.coordx + coordy = game.head.coordy + + # food is up + game.food = Point(coordx, coordy - unit) + state = snake_state_11(game) + assert (state[7:11] == [0, 0, 1, 0]).all() + + # food is up-right + game.food = Point(coordx + unit, coordy - unit) + state = snake_state_11(game) + assert (state[7:11] == [0, 1, 1, 0]).all() + + # food is right + game.food = Point(coordx + unit, coordy) + state = snake_state_11(game) + assert (state[7:11] == [0, 1, 0, 0]).all() + + # food is down-right + game.food = Point(coordx + unit, coordy + unit) + state = snake_state_11(game) + assert (state[7:11] == [0, 1, 0, 1]).all() + + # food is down + game.food = Point(coordx, coordy + unit) + state = snake_state_11(game) + assert (state[7:11] == [0, 0, 0, 1]).all() + + # food is down-left + game.food = Point(coordx - unit, coordy + unit) + state = snake_state_11(game) + assert (state[7:11] == [1, 0, 0, 1]).all() + + # food is left + game.food = Point(coordx - unit, coordy) + state = snake_state_11(game) + assert (state[7:11] == [1, 0, 0, 0]).all() + + # food is up-left + game.food = Point(coordx - unit, coordy - unit) + state = snake_state_11(game) + assert (state[7:11] == [1, 0, 1, 0]).all() diff --git a/src/reinforcement_learning/q_learning/test/model_test.py b/src/reinforcement_learning/q_learning/test/model_test.py new file mode 100644 index 0000000..28b01cc --- /dev/null +++ b/src/reinforcement_learning/q_learning/test/model_test.py @@ -0,0 +1,62 @@ +"""Tests the model.py file""" +# pylint: disable=missing-function-docstring +import tensorflow as tf +import numpy as np +from tensorflow import keras + +from game.snake import SnakeGame +from ..agent import SnakeAgent, ai_direction_to_snake, snake_state_11, snake_reward +from ..model import QTrainer, linear_qnet + +placeholder_model = linear_qnet(11, 3, 3) + + +def test_shapes_linear_model(): + model = linear_qnet(11, 256, 3) + model.compile() + + assert len(model.layers) == 3 + + assert isinstance(model.layers[0], keras.layers.InputLayer) + assert isinstance(model.layers[1], keras.layers.Dense) + assert isinstance(model.layers[2], keras.layers.Dense) + + assert model.layers[0].output_shape == [(None, 11)] + assert model.layers[1].output_shape == (None, 256) + assert model.layers[2].output_shape == (None, 3) + + +def test_qtrainer_defaults(): + trainer = QTrainer(placeholder_model) + + assert trainer.model == placeholder_model + assert trainer.gamma == 0.9 + + assert isinstance(trainer.optimizer, keras.optimizers.Adam) + assert (trainer.optimizer.learning_rate.numpy()) == np.float32(1e-4) + + assert isinstance(trainer.loss_object, keras.losses.MeanSquaredError) + + +def test_parameters_updated(): + agent = SnakeAgent(QTrainer(linear_qnet(11, 256, 3))) + game = SnakeGame() + + state = snake_state_11(game) + action = agent.get_action(state) + snake_action = ai_direction_to_snake(action) + + eaten, _, done = game.play_step(snake_action) + reward = snake_reward(eaten, done) + + state_next = snake_state_11(game) + + weights = [] + for variable in agent.trainer.model.trainable_variables: + weights.append(tf.identity(variable)) + + agent.train_short_memory(state, action, reward, state_next, done) + + for i, new_weight in enumerate(agent.trainer.model.trainable_variables): + # at least one element changed in every layer + assert not tf.math.reduce_all(weights[i] == new_weight).numpy()