tachibanaa710 commited on
Commit
10de6b6
·
verified ·
1 Parent(s): b70a0c0

Upload 31 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ akinator.py/banner.png filter=lfs diff=lfs merge=lfs -text
akinator.py/.github/ISSUE_TEMPLATE/bug_report.yml ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Bug Report
2
+ description: Report broken or incorrect behaviour
3
+ labels: bug
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: >
8
+ Thank you for taking the time to fill out a bug.
9
+
10
+ Please note that this form is for bugs only!
11
+ - type: input
12
+ attributes:
13
+ label: Summary
14
+ description: A simple summary of your bug report.
15
+ validations:
16
+ required: true
17
+ - type: textarea
18
+ attributes:
19
+ label: Reproduction Steps
20
+ description: >
21
+ What you did to make it happen.
22
+ validations:
23
+ required: true
24
+ - type: textarea
25
+ attributes:
26
+ label: Minimal Reproducible Code
27
+ description: >
28
+ A short snippet of code that showcases the bug.
29
+ render: python
30
+ - type: textarea
31
+ attributes:
32
+ label: Expected Results
33
+ description: >
34
+ What did you expect to happen?
35
+ validations:
36
+ required: true
37
+ - type: textarea
38
+ attributes:
39
+ label: Actual Results
40
+ description: >
41
+ What actually happened?
42
+ validations:
43
+ required: true
44
+ - type: textarea
45
+ attributes:
46
+ label: System Information
47
+ description: >
48
+ Provide some information about your system.
49
+ validations:
50
+ required: true
51
+ - type: checkboxes
52
+ attributes:
53
+ label: Checklist
54
+ description: >
55
+ Let's make sure you've properly done due diligence when reporting this issue!
56
+ options:
57
+ - label: I have searched open issues for duplicates.
58
+ required: true
59
+ - label: I have shown the entire traceback, if possible.
60
+ required: true
61
+ - label: I have made sure that this issue is valid.
62
+ required: true
63
+ - type: textarea
64
+ attributes:
65
+ label: Additional Context
66
+ description: If you have anything else to say, please do so here.
akinator.py/.github/ISSUE_TEMPLATE/config.yml ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Ask a Question
4
+ about: Ask questions and have discussions with other users.
5
+ url: https://github.com/Ombucha/brawlstars.py/discussions
akinator.py/.github/ISSUE_TEMPLATE/feature_request.yml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Feature Request
2
+ description: Suggest a feature for this library
3
+ labels: enhancement
4
+ body:
5
+ - type: input
6
+ attributes:
7
+ label: Summary
8
+ description: >
9
+ A short summary of what your feature request is.
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ attributes:
14
+ label: The Problem
15
+ description: >
16
+ What problem is your feature trying to solve?
17
+ What becomes easier or possible when this feature is implemented?
18
+ validations:
19
+ required: true
20
+ - type: textarea
21
+ attributes:
22
+ label: The Ideal Solution
23
+ description: >
24
+ What is your ideal solution to the problem?
25
+ What would you like this feature to do?
26
+ validations:
27
+ required: true
28
+ - type: textarea
29
+ attributes:
30
+ label: The Current Solution
31
+ description: >
32
+ What is the current solution to the problem, if any?
33
+ validations:
34
+ required: false
35
+ - type: textarea
36
+ attributes:
37
+ label: Additional Context
38
+ description: If there is anything else to say, please do so here.
akinator.py/.github/workflows/codeql.yml ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: "CodeQL"
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+ schedule:
9
+ - cron: '22 21 * * 4'
10
+
11
+ jobs:
12
+ analyze:
13
+ name: Analyze
14
+ runs-on: ubuntu-latest
15
+ permissions:
16
+ actions: read
17
+ contents: read
18
+ security-events: write
19
+
20
+ strategy:
21
+ fail-fast: false
22
+ matrix:
23
+ language: [ 'python' ]
24
+
25
+ steps:
26
+ - name: Checkout repository
27
+ uses: actions/checkout@v3
28
+
29
+ - name: Initialize CodeQL
30
+ uses: github/codeql-action/init@v3
31
+ with:
32
+ languages: ${{ matrix.language }}
33
+
34
+ - name: Autobuild
35
+ uses: github/codeql-action/autobuild@v3
36
+
37
+ - name: Perform CodeQL Analysis
38
+ uses: github/codeql-action/analyze@v3
akinator.py/.github/workflows/pylint.yml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Pylint
2
+
3
+ on: [push]
4
+
5
+ jobs:
6
+ build:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
11
+ steps:
12
+ - uses: actions/checkout@v3
13
+ - name: Set up Python ${{ matrix.python-version }}
14
+ uses: actions/setup-python@v3
15
+ with:
16
+ python-version: ${{ matrix.python-version }}
17
+ - name: Install dependencies
18
+ run: |
19
+ python -m pip install --upgrade pip
20
+ pip install pylint
21
+ - name: Analysing the code with pylint
22
+ run: |
23
+ pylint $(git ls-files '*.py')
akinator.py/.github/workflows/python-publish.yml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Upload Python Package
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ deploy:
12
+
13
+ runs-on: ubuntu-latest
14
+
15
+ steps:
16
+ - uses: actions/checkout@v3
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v3
19
+ with:
20
+ python-version: '3.x'
21
+ - name: Install dependencies
22
+ run: |
23
+ python -m pip install --upgrade pip
24
+ pip install build
25
+ - name: Build package
26
+ run: python -m build
27
+ - name: Publish package
28
+ uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
29
+ with:
30
+ user: __token__
31
+ password: ${{ secrets.PYPI_API_TOKEN }}
akinator.py/.github/workflows/unittest.yml ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Run Unit Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ matrix:
14
+ python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
15
+ steps:
16
+ - uses: actions/checkout@v3
17
+ - name: Set up Python ${{ matrix.python-version }}
18
+ uses: actions/setup-python@v3
19
+ with:
20
+ python-version: ${{ matrix.python-version }}
21
+ - name: Install dependencies
22
+ run: |
23
+ python -m pip install --upgrade pip
24
+ pip install -r requirements.txt
25
+ - name: Run unittests
26
+ run: |
27
+ python -m unittest discover -s tests
akinator.py/.gitignore ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
akinator.py/.pylintrc ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ [MASTER]
2
+ disable = W0718, C0301, R0902, R1710, R0903, R0801, E0401
akinator.py/.readthedocs.yml ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: 2
2
+
3
+ build:
4
+ os: ubuntu-20.04
5
+ tools:
6
+ python: "3.9"
7
+
8
+ sphinx:
9
+ configuration: docs/conf.py
10
+
11
+ python:
12
+ install:
13
+ - requirements: requirements.txt
akinator.py/CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our
6
+ community a harassment-free experience for everyone, regardless of age, body
7
+ size, visible or invisible disability, ethnicity, sex characteristics, gender
8
+ identity and expression, level of experience, education, socio-economic status,
9
+ nationality, personal appearance, race, religion, or sexual identity
10
+ and orientation.
11
+
12
+ We pledge to act and interact in ways that contribute to an open, welcoming,
13
+ diverse, inclusive, and healthy community.
14
+
15
+ ## Our Standards
16
+
17
+ Examples of behavior that contributes to a positive environment for our
18
+ community include:
19
+
20
+ * Demonstrating empathy and kindness toward other people
21
+ * Being respectful of differing opinions, viewpoints, and experiences
22
+ * Giving and gracefully accepting constructive feedback
23
+ * Accepting responsibility and apologizing to those affected by our mistakes,
24
+ and learning from the experience
25
+ * Focusing on what is best not just for us as individuals, but for the
26
+ overall community
27
+
28
+ Examples of unacceptable behavior include:
29
+
30
+ * The use of sexualized language or imagery, and sexual attention or
31
+ advances of any kind
32
+ * Trolling, insulting or derogatory comments, and personal or political attacks
33
+ * Public or private harassment
34
+ * Publishing others' private information, such as a physical or email
35
+ address, without their explicit permission
36
+ * Other conduct which could reasonably be considered inappropriate in a
37
+ professional setting
38
+
39
+ ## Enforcement Responsibilities
40
+
41
+ Community leaders are responsible for clarifying and enforcing our standards of
42
+ acceptable behavior and will take appropriate and fair corrective action in
43
+ response to any behavior that they deem inappropriate, threatening, offensive,
44
+ or harmful.
45
+
46
+ Community leaders have the right and responsibility to remove, edit, or reject
47
+ comments, commits, code, wiki edits, issues, and other contributions that are
48
+ not aligned to this Code of Conduct, and will communicate reasons for moderation
49
+ decisions when appropriate.
50
+
51
+ ## Scope
52
+
53
+ This Code of Conduct applies within all community spaces, and also applies when
54
+ an individual is officially representing the community in public spaces.
55
+ Examples of representing our community include using an official e-mail address,
56
+ posting via an official social media account, or acting as an appointed
57
+ representative at an online or offline event.
58
+
59
+ ## Enforcement
60
+
61
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
62
+ reported to the community leaders responsible for enforcement at
63
+ .
64
+ All complaints will be reviewed and investigated promptly and fairly.
65
+
66
+ All community leaders are obligated to respect the privacy and security of the
67
+ reporter of any incident.
68
+
69
+ ## Enforcement Guidelines
70
+
71
+ Community leaders will follow these Community Impact Guidelines in determining
72
+ the consequences for any action they deem in violation of this Code of Conduct:
73
+
74
+ ### 1. Correction
75
+
76
+ **Community Impact**: Use of inappropriate language or other behavior deemed
77
+ unprofessional or unwelcome in the community.
78
+
79
+ **Consequence**: A private, written warning from community leaders, providing
80
+ clarity around the nature of the violation and an explanation of why the
81
+ behavior was inappropriate. A public apology may be requested.
82
+
83
+ ### 2. Warning
84
+
85
+ **Community Impact**: A violation through a single incident or series
86
+ of actions.
87
+
88
+ **Consequence**: A warning with consequences for continued behavior. No
89
+ interaction with the people involved, including unsolicited interaction with
90
+ those enforcing the Code of Conduct, for a specified period of time. This
91
+ includes avoiding interactions in community spaces as well as external channels
92
+ like social media. Violating these terms may lead to a temporary or
93
+ permanent ban.
94
+
95
+ ### 3. Temporary Ban
96
+
97
+ **Community Impact**: A serious violation of community standards, including
98
+ sustained inappropriate behavior.
99
+
100
+ **Consequence**: A temporary ban from any sort of interaction or public
101
+ communication with the community for a specified period of time. No public or
102
+ private interaction with the people involved, including unsolicited interaction
103
+ with those enforcing the Code of Conduct, is allowed during this period.
104
+ Violating these terms may lead to a permanent ban.
105
+
106
+ ### 4. Permanent Ban
107
+
108
+ **Community Impact**: Demonstrating a pattern of violation of community
109
+ standards, including sustained inappropriate behavior, harassment of an
110
+ individual, or aggression toward or disparagement of classes of individuals.
111
+
112
+ **Consequence**: A permanent ban from any sort of public interaction within
113
+ the community.
114
+
115
+ ## Attribution
116
+
117
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
118
+ version 2.0, available at
119
+ https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
120
+
121
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct
122
+ enforcement ladder](https://github.com/mozilla/diversity).
123
+
124
+ [homepage]: https://www.contributor-covenant.org
125
+
126
+ For answers to common questions about this code of conduct, see the FAQ at
127
+ https://www.contributor-covenant.org/faq. Translations are available at
128
+ https://www.contributor-covenant.org/translations.
akinator.py/CONTRIBUTING.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing to akinator.py
2
+
3
+ Thank you for your interest in contributing! Your help is appreciated.
4
+
5
+ ## How to Contribute
6
+
7
+ 1. **Fork the repository** and clone your fork.
8
+ 2. **Create a new branch** for your feature or bugfix:
9
+ ```sh
10
+ git checkout -b my-feature
11
+ ```
12
+ 3. **Make your changes** and add tests if possible.
13
+ 4. **Lint your code** using [pylint](https://pylint.pycqa.org/).
14
+ 5. **Commit your changes** with a clear message.
15
+ 6. **Push to your fork** and open a Pull Request (PR) against the `main` branch.
16
+
17
+ ## Code Style
18
+
19
+ - Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) guidelines.
20
+ - Use type hints where possible.
21
+ - Document your functions and classes.
22
+
23
+ ## Pull Request Checklist
24
+
25
+ - [ ] Code is linted with `pylint`
26
+ - [ ] Documentation is updated if needed
27
+ - [ ] PR description explains the change
28
+
29
+ ## Reporting Issues
30
+
31
+ If you find a bug or have a feature request, please [open an issue](https://github.com/Ombucha/akinator.py/issues) and provide as much detail as possible.
32
+
33
+ ## Community
34
+
35
+ - Be respectful and constructive.
36
+ - Ask questions or discuss ideas in issues or PRs.
37
+
38
+ Thank you for helping improve **akinator.py**!
akinator.py/LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Omkaar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
akinator.py/MANIFEST.in ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ include README.md
2
+ include LICENSE
akinator.py/README.rst ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .. image:: https://raw.githubusercontent.com/Ombucha/akinator.py/main/banner.png
2
+
3
+ .. image:: https://img.shields.io/pypi/v/akinator
4
+ :target: https://pypi.python.org/pypi/akinator
5
+ :alt: PyPI version
6
+ .. image:: https://static.pepy.tech/personalized-badge/akinator?period=total&left_text=downloads&left_color=grey&right_color=red
7
+ :target: https://pypi.python.org/pypi/akinator
8
+ :alt: PyPI downloads
9
+ .. image:: https://sloc.xyz/github/Ombucha/akinator.py?lower=True
10
+ :target: https://github.com/Ombucha/akinator.py/graphs/contributors
11
+ :alt: Lines of code
12
+ .. image:: https://img.shields.io/github/repo-size/Ombucha/akinator.py?color=yellow
13
+ :target: https://github.com/Ombucha/akinator.py
14
+ :alt: Repository size
15
+
16
+ A modern, easy-to-use Python wrapper for the Akinator web game, supporting both synchronous and asynchronous usage.
17
+
18
+ Background
19
+ ----------
20
+
21
+ Originally, there was a popular Python library called ``akinator.py``, which provided a simple interface to interact with the Akinator API. However, this library suddenly disappeared from public repositories without notice. In response, a mirror was created here to preserve its functionality. Unfortunately, it too stopped working after Akinator made changes to their backend API. Later, another library called ``akipy`` emerged to fill the gap, but it also became non-functional when Cloudflare protection was introduced on Akinator's endpoints. This library revives Akinator interaction by replacing the standard ``requests`` library with ``cloudscraper``, allowing it to bypass Cloudflare's anti-bot measures and restoring full functionality.
22
+
23
+ Features
24
+ --------
25
+
26
+ - Play Akinator in Python (sync and async)
27
+ - Supports all official Akinator languages and themes
28
+ - Simple, Pythonic interface
29
+ - Type hints for better editor support
30
+ - Custom exceptions for robust error handling
31
+ - Well-tested and documented
32
+ - Actively maintained and open source
33
+
34
+ Requirements
35
+ ------------
36
+
37
+ - **Python 3.9 or higher**
38
+ - `cloudscraper <https://pypi.org/project/cloudscraper/>`_
39
+
40
+ Installation
41
+ ------------
42
+
43
+ To install the latest stable version:
44
+
45
+ .. code-block:: sh
46
+
47
+ python3 -m pip install akinator
48
+
49
+ To install the development version:
50
+
51
+ .. code-block:: sh
52
+
53
+ git clone https://github.com/Ombucha/akinator.py
54
+ cd akinator.py
55
+ python3 -m pip install -e .
56
+
57
+ Getting Started
58
+ ---------------
59
+
60
+ Synchronous Example
61
+ ~~~~~~~~~~~~~~~~~~~
62
+
63
+ .. code-block:: python
64
+
65
+ import akinator
66
+
67
+ aki = akinator.Akinator()
68
+ aki.start_game()
69
+
70
+ while not aki.finished:
71
+ print(f"\nQuestion: {str(aki)}")
72
+ user_input = input(
73
+ "Your answer ([y]es/[n]o/[i] don't know/[p]robably/[pn] probably not, [b]ack): "
74
+ ).strip().lower()
75
+ if user_input == "b":
76
+ try:
77
+ aki.back()
78
+ except akinator.CantGoBackAnyFurther:
79
+ print("You can't go back any further!")
80
+ else:
81
+ try:
82
+ aki.answer(user_input)
83
+ except akinator.InvalidChoiceError:
84
+ print("Invalid answer. Please try again.")
85
+
86
+ print("\n--- Game Over ---")
87
+ print(f"Proposition: {aki.name_proposition}")
88
+ print(f"Description: {aki.description_proposition}")
89
+ print(f"Pseudo: {aki.pseudo}")
90
+ print(f"Photo: {aki.photo}")
91
+ print(f"Final Message: {aki.question}")
92
+
93
+
94
+ Asynchronous Example
95
+ ~~~~~~~~~~~~~~~~~~~~
96
+
97
+ .. code-block:: python
98
+
99
+ import asyncio
100
+ import akinator
101
+
102
+ aki = akinator.Akinator()
103
+
104
+ async def play():
105
+ await aki.start_game()
106
+
107
+ while not aki.finished:
108
+ print(f"\nQuestion: {str(aki)}")
109
+ user_input = input(
110
+ "Your answer ([y]es/[n]o/[i] don't know/[p]robably/[pn] probably not, [b]ack): "
111
+ ).strip().lower()
112
+ if user_input == "b":
113
+ try:
114
+ await aki.back()
115
+ except akinator.CantGoBackAnyFurther:
116
+ print("You can't go back any further!")
117
+ else:
118
+ try:
119
+ await aki.answer(user_input)
120
+ except akinator.InvalidChoiceError:
121
+ print("Invalid answer. Please try again.")
122
+
123
+ print("\n--- Game Over ---")
124
+ print(f"Proposition: {aki.name_proposition}")
125
+ print(f"Description: {aki.description_proposition}")
126
+ print(f"Pseudo: {aki.pseudo}")
127
+ print(f"Photo: {aki.photo}")
128
+ print(f"Final Message: {aki.question}")
129
+
130
+ asyncio.run(play())
131
+
132
+
133
+ Advanced Usage
134
+ --------------
135
+
136
+ - **Languages:** All official Akinator languages are supported (see `LANG_MAP` in the code).
137
+ - **Themes:** Use "c" for characters, "a" for animals, "o" for objects (not all themes are available in all languages).
138
+ - **Error Handling:** All errors raise custom exceptions like `CantGoBackAnyFurther`, `InvalidLanguageError`, `InvalidChoiceError`, and `InvalidThemeError`.
139
+ - **Custom Session:** You can pass your own `cloudscraper.CloudScraper` session for advanced usage.
140
+ - **Async and Sync:** Both sync and async clients are available for all use cases.
141
+ - **Testing:** Comprehensive test suite for both sync and async clients.
142
+ - **Examples:** See the `examples/` directory for CLI and bot scripts.
143
+
144
+ Links
145
+ -----
146
+
147
+ - `Akinator <https://akinator.com/>`_
148
+ - `Documentation <https://akinator.readthedocs.io>`_
149
+ - `Examples <https://github.com/Ombucha/akinator.py/tree/main/examples>`_
150
+ - `PyPI <https://pypi.org/project/akinator.py/>`_
151
+
152
+ Contributing
153
+ ------------
154
+
155
+ Contributions are welcome! Please see the `CONTRIBUTING.md` file for details.
156
+
157
+ License
158
+ -------
159
+
160
+ This project is licensed under the MIT License. See the `LICENSE` file for details.
akinator.py/akinator/__init__.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Akinator API Wrapper
3
+ ~~~~~~~~~~~~~~~~~~~
4
+
5
+ A basic API wrapper for Akinator.
6
+
7
+ :copyright: (c) 2025 Omkaar
8
+ :license: MIT, see LICENSE for more details.
9
+ """
10
+
11
+
12
+ __title__ = "akinator"
13
+ __author__ = "Omkaar"
14
+ __license__ = "MIT"
15
+ __copyright__ = "Copyright 2025 Omkaar"
16
+ __version__ = "2.0.1"
17
+
18
+
19
+ from .client import *
20
+ from .exceptions import *
21
+ from .async_client import *
akinator.py/akinator/async_client.py ADDED
@@ -0,0 +1,469 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MIT License
3
+
4
+ Copyright (c) 2025 Omkaar
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ """
24
+
25
+ from typing import Literal, Optional
26
+ from re import search
27
+ from html import unescape
28
+ from asyncio import to_thread
29
+ from cloudscraper import CloudScraper, create_scraper
30
+
31
+ from .exceptions import CantGoBackAnyFurther, InvalidLanguageError, InvalidThemeError, InvalidChoiceError
32
+
33
+
34
+ LANG_MAP = {
35
+ "english": "en",
36
+ "arabic": "ar",
37
+ "chinese": "cn",
38
+ "german": "de",
39
+ "spanish": "es",
40
+ "french": "fr",
41
+ "hebrew": "il",
42
+ "italian": "it",
43
+ "japanese": "jp",
44
+ "korean": "kr",
45
+ "dutch": "nl",
46
+ "polish": "pl",
47
+ "portuguese": "pt",
48
+ "russian": "ru",
49
+ "turkish": "tr",
50
+ "indonesian": "id",
51
+ }
52
+
53
+ THEME_IDS = {"c": 1, "a": 14, "o": 2}
54
+
55
+ # c - characters
56
+ # a - animals
57
+ # o - objects
58
+
59
+ THEME_MAP = {
60
+ "en": ["c", "a", "o"],
61
+ "ar": ["c"],
62
+ "cn": ["c"],
63
+ "de": ["c", "a"],
64
+ "es": ["c", "a"],
65
+ "fr": ["c", "a", "o"],
66
+ "il": ["c"],
67
+ "it": ["c", "a"],
68
+ "jp": ["c", "a"],
69
+ "kr": ["c"],
70
+ "nl": ["c"],
71
+ "pl": ["c"],
72
+ "pt": ["c"],
73
+ "ru": ["c"],
74
+ "tr": ["c"],
75
+ "id": ["c"],
76
+ }
77
+
78
+ ANSWER_IDS = {
79
+ 0: ["yes", "y", "0"],
80
+ 1: ["no", "n", "1"],
81
+ 2: ["i", "idk", "i dont know", "i don't know", "2"],
82
+ 3: ["p", "probably", "3"],
83
+ 4: ["pn", "probably not", "4"],
84
+ }
85
+
86
+ ANSWER_MAP = {item: key for key, values in ANSWER_IDS.items() for item in values}
87
+
88
+ class AsyncCloudScraper:
89
+ """
90
+ An asynchronous wrapper around `CloudScraper` to handle HTTP requests.
91
+ """
92
+ def __init__(self, **kwargs):
93
+ self.scraper = create_scraper(**kwargs)
94
+
95
+ async def post(self, url, data=None, json=None, **kwargs):
96
+ """
97
+ An asynchronous method to perform a POST request using the `cloudscraper` library.
98
+ """
99
+ return await to_thread(self.scraper.post, url, data=data, json=json, **kwargs)
100
+
101
+ class AsyncClient:
102
+
103
+ """
104
+ A class representing an asynchronous client for the Akinator game.
105
+
106
+ :param session: An optional `CloudScraper` object to use for making HTTP requests. If not provided, a new session will be created.
107
+ :type session: Optional[CloudScraper]
108
+
109
+ :ivar flag_photo: The URL of the flag photo associated with the current game session.
110
+ :ivar photo: The URL of the character photo associated with the current game session.
111
+ :ivar pseudo: The pseudo name of the player in the current game session.
112
+ :ivar theme: The theme of the game session, which can be "c" (characters), "a" (animals), or "o" (objects).
113
+ :ivar session_id: The unique identifier for the current game session.
114
+ :ivar signature: The signature for the current game session.
115
+ :ivar identifiant: The unique identifier for the player in the current game session.
116
+ :ivar child_mode: A boolean indicating whether child mode is enabled for the game session.
117
+ :ivar language: The language used for the game session, represented as a string (e.g., "en" for English).
118
+ :ivar question: The current question being asked in the game session.
119
+ :ivar progression: The progression percentage of the game session, represented as a float.
120
+ :ivar step: The current step number in the game session.
121
+ :ivar akitude: The current akitude image associated with the game session.
122
+ :ivar step_last_proposition: The last proposition made in the current step.
123
+ :ivar finished: A boolean indicating whether the game session has finished.
124
+ :ivar win: A boolean indicating whether the player has won the game session.
125
+ :ivar id_proposition: The unique identifier for the current proposition in the game session.
126
+ :ivar name_proposition: The name of the current proposition in the game session.
127
+ :ivar description_proposition: The description of the current proposition in the game session.
128
+ :ivar proposition: The current proposition being made in the game session.
129
+ :ivar completion: The completion status of the game session, which can be "OK, "KO - TIMEOUT", "SOUNDLIKE", or other values.
130
+ :ivar confidence: The confidence level of the current question, represented as a float between 0 and 1.
131
+ :ivar theme_id: The ID of the current theme, represented as an integer.
132
+ :ivar theme_name: The name of the current theme, represented as a string.
133
+ :ivar akitude_url: The URL of the current akitude image associated with the game session.
134
+ """
135
+
136
+ def __init__(self, session: Optional[CloudScraper] = None):
137
+ self.session = session if session else AsyncCloudScraper()
138
+
139
+ self.flag_photo = None
140
+ self.photo = None
141
+ self.pseudo = None
142
+ self.theme = None
143
+ self.session_id = None
144
+ self.signature = None
145
+ self.identifiant = None
146
+ self.child_mode = False
147
+ self.language = None
148
+ self.theme = None
149
+
150
+ self.question = None
151
+ self.progression = None
152
+ self.step = None
153
+ self.akitude = None
154
+ self.step_last_proposition = ""
155
+ self.finished = False
156
+
157
+ self.win = False
158
+ self.id_proposition = None
159
+ self.name_proposition = None
160
+ self.description_proposition = None
161
+ self.proposition = ""
162
+ self.completion = None
163
+
164
+ async def __handler(self, response):
165
+ response.raise_for_status()
166
+ try:
167
+ data = response.json()
168
+ except Exception as e:
169
+ if "A technical problem has ocurred." in response.text:
170
+ raise RuntimeError("A technical problem has occurred. Please try again later.") from e
171
+ raise RuntimeError("Failed to parse the response as JSON.") from e
172
+
173
+ if "completion" not in data:
174
+ data["completion"] = self.completion
175
+ if data["completion"] == "KO - TIMEOUT":
176
+ raise RuntimeError("The session has timed out. Please start a new game.")
177
+ if data["completion"] == "SOUNDLIKE":
178
+ self.finished = True
179
+ self.win = True
180
+ if not self.id_proposition:
181
+ self.defeat()
182
+ elif "id_proposition" in data:
183
+ self.win = True
184
+ self.id_proposition = data["id_proposition"]
185
+ self.name_proposition = data["name_proposition"]
186
+ self.description_proposition = data["description_proposition"]
187
+ self.step_last_proposition = self.step
188
+ self.pseudo = data["pseudo"]
189
+ self.flag_photo = data["flag_photo"]
190
+ self.photo = data["photo"]
191
+ else:
192
+ self.akitude = data["akitude"]
193
+ self.step = int(data["step"])
194
+ self.progression = float(data["progression"])
195
+ self.question = data["question"]
196
+ self.completion = data["completion"]
197
+
198
+ async def start_game(self, *, language: str = "en", child_mode: bool = False, theme: Literal["c", "a", "o"] = "c"):
199
+ """
200
+ Starts a new game session with the specified language, child mode, and theme.
201
+
202
+ :param language: The language to use for the game. Defaults to "en" (English).
203
+ :type language: str
204
+ :param child_mode: Whether to enable child mode. Defaults to False.
205
+ :type child_mode: bool
206
+ :param theme: The theme to use for the game. Can be "c" (characters), "a" (animals), or "o" (objects). Defaults to "c".
207
+ :type theme: Literal["c", "a", "o"]
208
+ """
209
+ if language not in LANG_MAP and language not in LANG_MAP.values():
210
+ raise InvalidLanguageError(f"Unsupported language: {language}. Supported languages: {', '.join(LANG_MAP.keys())}")
211
+
212
+ if theme not in THEME_IDS and theme not in THEME_IDS.values():
213
+ raise InvalidThemeError(f"Unsupported theme: {theme}. Supported themes: {', '.join(THEME_IDS.keys())}")
214
+
215
+ if theme not in THEME_MAP[LANG_MAP.get(language.lower(), language.lower())]:
216
+ raise InvalidThemeError(f"Theme '{theme}' is not available for language '{language}'.")
217
+
218
+ try:
219
+ self.theme = theme
220
+ self.language = LANG_MAP.get(language.lower(), language.lower())
221
+ self.child_mode = child_mode
222
+
223
+ response = await self.session.post(f"https://{self.language}.akinator.com/game", data={"sid": THEME_IDS[theme], "cm": str(child_mode).lower()})
224
+ response.raise_for_status()
225
+ text = response.text
226
+
227
+ self.session_id = search(r"#session'\).val\('(.+?)'\)", text).group(1)
228
+ self.signature = search(r"#signature'\).val\('(.+?)'\)", text).group(1)
229
+ self.identifiant = search(r"#identifiant'\).val\('(.+?)'\)", text).group(1)
230
+
231
+ if not all([self.session_id, self.signature, self.identifiant]):
232
+ raise ValueError("Failed to extract session information from the response.")
233
+
234
+ question = search(r'<div class="bubble-body"><p class="question-text" id="question-label">(.+)</p></div>', text)
235
+
236
+ if not question:
237
+ raise ValueError("Failed to extract the initial question from the response.")
238
+
239
+ self.question = unescape(question.group(1))
240
+
241
+ proposition = search(r'<div class="sub-bubble-propose"><p id="p-sub-bubble">([\w\s]+)</p></div>', text)
242
+
243
+ if not proposition:
244
+ raise ValueError("Failed to extract the proposition from the response.")
245
+
246
+ self.proposition = unescape(proposition.group(1))
247
+ self.progression = 0
248
+ self.step = 0
249
+ self.akitude = "defi.png"
250
+ except Exception as e:
251
+ raise RuntimeError("Failed to start the game.") from e
252
+
253
+ return self.session
254
+
255
+ async def answer(self, answer: str):
256
+ """
257
+ Submits an answer to the current question.
258
+
259
+ :param answer: The answer to submit. Can be "yes", "no", "i don't know", "probably", or "probably not".
260
+ :type answer: str
261
+ """
262
+ if not answer.lower() in ANSWER_MAP:
263
+ raise InvalidChoiceError(f"Invalid answer: {answer}. Valid answers are: {', '.join(ANSWER_MAP.keys())}")
264
+ answer_id = ANSWER_MAP[answer.lower()]
265
+
266
+ if self.win:
267
+ if answer_id == 0:
268
+ return await self.choose()
269
+ if answer_id == 1:
270
+ return await self.exclude()
271
+ raise InvalidChoiceError("Invalid answer after Akinator has proposed a win. Only 'yes' or 'no' are valid answers at this point.")
272
+
273
+ url = f"https://{self.language}.akinator.com/answer"
274
+ data = {
275
+ "step": self.step,
276
+ "progression": self.progression,
277
+ "sid": THEME_IDS[self.theme],
278
+ "cm": str(self.child_mode).lower(),
279
+ "answer": answer_id,
280
+ "step_last_proposition": self.step_last_proposition,
281
+ "session": self.session_id,
282
+ "signature": self.signature
283
+ }
284
+
285
+ try:
286
+ response = await self.session.post(url, data=data)
287
+ await self.__handler(response)
288
+ except Exception as e:
289
+ raise RuntimeError("Failed to submit the answer.") from e
290
+
291
+ async def back(self):
292
+ """
293
+ Goes back to the previous question in the game.
294
+
295
+ .. note::
296
+
297
+ This method can only be called if the current step is greater than 0. If the step is 0, it raises a `CantGoBackAnyFurther` exception.
298
+ """
299
+ if self.step == 0:
300
+ raise CantGoBackAnyFurther()
301
+
302
+ url = f"https://{self.language}.akinator.com/cancel_answer"
303
+ data = {
304
+ "step": self.step,
305
+ "progression": self.progression,
306
+ "sid": THEME_IDS[self.theme],
307
+ "cm": str(self.child_mode).lower(),
308
+ "session": self.session_id,
309
+ "signature": self.signature
310
+ }
311
+ self.win = False
312
+
313
+ try:
314
+ response = await self.session.post(url, data=data)
315
+ await self.__handler(response)
316
+ except Exception as e:
317
+ raise RuntimeError("Failed to go back to the previous question.") from e
318
+
319
+ async def exclude(self):
320
+ """
321
+ Excludes the current proposition from the game.
322
+
323
+ .. note::
324
+
325
+ This method can only be called after Akinator has proposed a win. If the game is already finished, it will call the `defeat` method.
326
+ """
327
+ if not self.win:
328
+ raise RuntimeError("You can only exclude a proposition after Akinator has proposed a win.")
329
+
330
+ if self.finished:
331
+ return await self.defeat()
332
+
333
+ url = f"https://{self.language}.akinator.com/exclude"
334
+ data = {
335
+ "step": self.step,
336
+ "progression": self.progression,
337
+ "sid": THEME_IDS[self.theme],
338
+ "cm": str(self.child_mode).lower(),
339
+ "session": self.session_id,
340
+ "signature": self.signature
341
+ }
342
+ self.win = False
343
+ self.id_proposition = ""
344
+
345
+ try:
346
+ response = await self.session.post(url, data=data)
347
+ await self.__handler(response)
348
+ except Exception as e:
349
+ raise RuntimeError("Failed to exclude the proposition.") from e
350
+
351
+ async def choose(self):
352
+ """
353
+ Chooses the current proposition as the answer to the game.
354
+
355
+ .. note::
356
+
357
+ This method can only be called after Akinator has proposed a win. If the game is already finished, it will raise a `RuntimeError`.
358
+ """
359
+ if not self.win:
360
+ raise RuntimeError("You can only choose a proposition after Akinator has proposed a win.")
361
+
362
+ url = f"https://{self.language}.akinator.com/choice"
363
+ data = {
364
+ "step": self.step,
365
+ "sid": THEME_IDS[self.theme],
366
+ "session": self.session_id,
367
+ "signature": self.signature,
368
+ "identifiant": self.identifiant,
369
+ "pid": self.id_proposition,
370
+ "charac_name": self.name_proposition,
371
+ "charac_description": self.description_proposition,
372
+ "pflag_photo": self.flag_photo
373
+ }
374
+
375
+ try:
376
+ response = await self.session.post(url, data=data, allow_redirects=True)
377
+ if response.status_code not in range(200, 400):
378
+ response.raise_for_status()
379
+
380
+ self.finished = True
381
+ self.win = True
382
+ self.akitude = "triomphe.png"
383
+ self.id_proposition = ""
384
+ except Exception as e:
385
+ raise RuntimeError("Failed to choose the proposition.") from e
386
+
387
+ try:
388
+ text = response.text
389
+ win_message = unescape(search(r'<span class="win-sentence">(.+?)<\/span>', text).group(1))
390
+ already_played = unescape(search(r'let tokenDejaJoue = "([\w\s]+)";', text).group(1))
391
+ times_selected = search(r'let timesSelected = "(\d+)";', text).group(1)
392
+ times = unescape(search(r'<span id="timesselected"><\/span>\s+([\w\s]+)<\/span>', text).group(1))
393
+ self.question = f"{win_message}\n{already_played} {times_selected} {times}"
394
+ except Exception:
395
+ pass
396
+
397
+ self.progression = 100
398
+
399
+ async def defeat(self):
400
+ """
401
+ Handles the defeat scenario in the game.
402
+ """
403
+ self.finished = True
404
+ self.win = False
405
+ self.akitude = "deception.png"
406
+ self.id_proposition = ""
407
+
408
+ questions = {
409
+ "en": "Bravo, you have defeated me !\nShare your feat with your friends.",
410
+ "ar": "أحسنت، لقد هزمتني !\nشارك إنجازك مع أصدقائك.",
411
+ "cn": "太棒了,你打败了我!\n与朋友分享你的成就吧。",
412
+ "de": "Bravo, du hast mich besiegt !\nTeile deinen Erfolg mit deinen Freunden.",
413
+ "es": "¡Bravo, me has derrotado !\nComparte tu hazaña con tus amigos.",
414
+ "fr": "Bravo, tu m'as vaincu !\nPartage ton exploit avec tes amis.",
415
+ "il": "כל הכבוד, הצלחת להביס אותי !\nשתף את ההישג שלך עם חברים.",
416
+ "it": "Bravo, mi hai sconfitto !\nCondividi la tua impresa con i tuoi amici.",
417
+ "jp": "すごい、あなたは私を倒しました!\nこの偉業を友達と共有しましょう。",
418
+ "kr": "브라보, 당신이 저를 이겼습니다 !\n당신의 업적을 친구들과 공유하세요.",
419
+ "nl": "Bravo, je hebt me verslagen !\nDeel je prestatie met je vrienden.",
420
+ "pl": "Brawo, pokonałeś mnie !\nPodziel się swoim wyczynem ze znajomymi.",
421
+ "pt": "Bravo, você me derrotou !\nCompartilhe sua conquista com seus amigos.",
422
+ "ru": "Браво, ты победил меня !\nПоделись своим достижением с друзьями.",
423
+ "tr": "Bravo, beni yendin !\nBu başarını arkadaşlarınla paylaş.",
424
+ "id": "Hebat, kamu mengalahkanku !\nBagikan pencapaianmu kepada teman-temanmu.",
425
+ }
426
+
427
+ self.question = questions[self.language]
428
+ self.progression = 100
429
+
430
+ @property
431
+ def confidence(self) -> float:
432
+ """
433
+ Returns the confidence level of the current question.
434
+ """
435
+ return self.progression / 100
436
+
437
+ @property
438
+ def theme_id(self) -> int:
439
+ """
440
+ Returns the ID of the current theme.
441
+ """
442
+ return THEME_IDS[self.theme]
443
+
444
+ @property
445
+ def theme_name(self) -> str:
446
+ """
447
+ Returns the name of the current theme.
448
+ """
449
+ return "Characters" if self.theme == "c" else "Animals" if self.theme == "a" else "Objects"
450
+
451
+ @property
452
+ def akitude_url(self) -> str:
453
+ """
454
+ Returns the URL of the current akitude image.
455
+ """
456
+ return f"https://{self.language}.akinator.com/assets/img/akitudes_670x1096/{self.akitude}"
457
+
458
+ def __str__(self):
459
+ if self.win and not self.finished:
460
+ return f"{self.proposition} {self.name_proposition} ({self.description_proposition})"
461
+ return self.question
462
+
463
+ def __repr__(self):
464
+ return f"<Akinator Client (Language: {self.language}, Theme: {self.theme_name}, Step: {self.step}, Progression: {self.progression}%)>"
465
+
466
+ class AsyncAkinator(AsyncClient):
467
+ """
468
+ A class identical to `AsyncClient`, but created for compatibility with the previous version of the library.
469
+ """
akinator.py/akinator/client.py ADDED
@@ -0,0 +1,454 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MIT License
3
+
4
+ Copyright (c) 2025 Omkaar
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ """
24
+
25
+ from typing import Literal, Optional
26
+ from re import search
27
+ from html import unescape
28
+ from cloudscraper import CloudScraper, create_scraper
29
+
30
+ from .exceptions import CantGoBackAnyFurther, InvalidLanguageError, InvalidChoiceError, InvalidThemeError
31
+
32
+ LANG_MAP = {
33
+ "english": "en",
34
+ "arabic": "ar",
35
+ "chinese": "cn",
36
+ "german": "de",
37
+ "spanish": "es",
38
+ "french": "fr",
39
+ "hebrew": "il",
40
+ "italian": "it",
41
+ "japanese": "jp",
42
+ "korean": "kr",
43
+ "dutch": "nl",
44
+ "polish": "pl",
45
+ "portuguese": "pt",
46
+ "russian": "ru",
47
+ "turkish": "tr",
48
+ "indonesian": "id",
49
+ }
50
+
51
+ THEME_IDS = {"c": 1, "a": 14, "o": 2}
52
+
53
+ # c - characters
54
+ # a - animals
55
+ # o - objects
56
+
57
+ THEME_MAP = {
58
+ "en": ["c", "a", "o"],
59
+ "ar": ["c"],
60
+ "cn": ["c"],
61
+ "de": ["c", "a"],
62
+ "es": ["c", "a"],
63
+ "fr": ["c", "a", "o"],
64
+ "il": ["c"],
65
+ "it": ["c", "a"],
66
+ "jp": ["c", "a"],
67
+ "kr": ["c"],
68
+ "nl": ["c"],
69
+ "pl": ["c"],
70
+ "pt": ["c"],
71
+ "ru": ["c"],
72
+ "tr": ["c"],
73
+ "id": ["c"],
74
+ }
75
+
76
+ ANSWER_IDS = {
77
+ 0: ["yes", "y", "0"],
78
+ 1: ["no", "n", "1"],
79
+ 2: ["i", "idk", "i dont know", "i don't know", "2"],
80
+ 3: ["p", "probably", "3"],
81
+ 4: ["pn", "probably not", "4"],
82
+ }
83
+
84
+ ANSWER_MAP = {item: key for key, values in ANSWER_IDS.items() for item in values}
85
+
86
+ class Client:
87
+
88
+ """
89
+ A class representing a client for the Akinator game.
90
+
91
+ :param session: An optional `CloudScraper` object to use for making HTTP requests. If not provided, a new session will be created.
92
+ :type session: Optional[CloudScraper]
93
+
94
+ :ivar flag_photo: The URL of the flag photo associated with the current game session.
95
+ :ivar photo: The URL of the character photo associated with the current game session.
96
+ :ivar pseudo: The pseudo name of the player in the current game session.
97
+ :ivar theme: The theme of the game session, which can be "c" (characters), "a" (animals), or "o" (objects).
98
+ :ivar session_id: The unique identifier for the current game session.
99
+ :ivar signature: The signature for the current game session.
100
+ :ivar identifiant: The unique identifier for the player in the current game session.
101
+ :ivar child_mode: A boolean indicating whether child mode is enabled for the game session.
102
+ :ivar language: The language used for the game session, represented as a string (e.g., "en" for English).
103
+ :ivar question: The current question being asked in the game session.
104
+ :ivar progression: The progression percentage of the game session, represented as a float.
105
+ :ivar step: The current step number in the game session.
106
+ :ivar akitude: The current akitude image associated with the game session.
107
+ :ivar step_last_proposition: The last proposition made in the current step.
108
+ :ivar finished: A boolean indicating whether the game session has finished.
109
+ :ivar win: A boolean indicating whether the player has won the game session.
110
+ :ivar id_proposition: The unique identifier for the current proposition in the game session.
111
+ :ivar name_proposition: The name of the current proposition in the game session.
112
+ :ivar description_proposition: The description of the current proposition in the game session.
113
+ :ivar proposition: The current proposition being made in the game session.
114
+ :ivar completion: The completion status of the game session, which can be "OK, "KO - TIMEOUT", "SOUNDLIKE", or other values.
115
+ :ivar confidence: The confidence level of the current question, represented as a float between 0 and 1.
116
+ :ivar theme_id: The ID of the current theme, represented as an integer.
117
+ :ivar theme_name: The name of the current theme, represented as a string.
118
+ :ivar akitude_url: The URL of the current akitude image associated with the game session.
119
+ """
120
+
121
+ def __init__(self, session: Optional[CloudScraper] = None):
122
+ self.session = session if session else create_scraper()
123
+
124
+ self.flag_photo = None
125
+ self.photo = None
126
+ self.pseudo = None
127
+ self.theme = None
128
+ self.session_id = None
129
+ self.signature = None
130
+ self.identifiant = None
131
+ self.child_mode = False
132
+ self.language = None
133
+ self.theme = None
134
+
135
+ self.question = None
136
+ self.progression = None
137
+ self.step = None
138
+ self.akitude = None
139
+ self.step_last_proposition = ""
140
+ self.finished = False
141
+
142
+ self.win = False
143
+ self.id_proposition = None
144
+ self.name_proposition = None
145
+ self.description_proposition = None
146
+ self.proposition = ""
147
+ self.completion = None
148
+
149
+ def __handler(self, response):
150
+ response.raise_for_status()
151
+ try:
152
+ data = response.json()
153
+ except Exception as e:
154
+ if "A technical problem has ocurred." in response.text:
155
+ raise RuntimeError("A technical problem has occurred. Please try again later.") from e
156
+ raise RuntimeError("Failed to parse the response as JSON.") from e
157
+
158
+ if "completion" not in data:
159
+ data["completion"] = self.completion
160
+ if data["completion"] == "KO - TIMEOUT":
161
+ raise RuntimeError("The session has timed out. Please start a new game.")
162
+ if data["completion"] == "SOUNDLIKE":
163
+ self.finished = True
164
+ self.win = True
165
+ if not self.id_proposition:
166
+ self.defeat()
167
+ elif "id_proposition" in data:
168
+ self.win = True
169
+ self.id_proposition = data["id_proposition"]
170
+ self.name_proposition = data["name_proposition"]
171
+ self.description_proposition = data["description_proposition"]
172
+ self.step_last_proposition = self.step
173
+ self.pseudo = data["pseudo"]
174
+ self.flag_photo = data["flag_photo"]
175
+ self.photo = data["photo"]
176
+ else:
177
+ self.akitude = data["akitude"]
178
+ self.step = int(data["step"])
179
+ self.progression = float(data["progression"])
180
+ self.question = data["question"]
181
+ self.completion = data["completion"]
182
+
183
+ def start_game(self, *, language: str = "en", child_mode: bool = False, theme: Literal["c", "a", "o"] = "c"):
184
+ """
185
+ Starts a new game session with the specified language, child mode, and theme.
186
+
187
+ :param language: The language to use for the game. Defaults to "en" (English).
188
+ :type language: str
189
+ :param child_mode: Whether to enable child mode. Defaults to False.
190
+ :type child_mode: bool
191
+ :param theme: The theme to use for the game. Can be "c" (characters), "a" (animals), or "o" (objects). Defaults to "c".
192
+ :type theme: Literal["c", "a", "o"]
193
+ """
194
+ if language not in LANG_MAP and language not in LANG_MAP.values():
195
+ raise InvalidLanguageError(f"Unsupported language: {language}. Supported languages: {', '.join(LANG_MAP.keys())}")
196
+
197
+ if theme not in THEME_IDS and theme not in THEME_IDS.values():
198
+ raise InvalidThemeError(f"Unsupported theme: {theme}. Supported themes: {', '.join(THEME_IDS.keys())}")
199
+
200
+ if theme not in THEME_MAP[LANG_MAP.get(language.lower(), language.lower())]:
201
+ raise InvalidThemeError(f"Theme '{theme}' is not available for language '{language}'.")
202
+
203
+ try:
204
+ self.theme = theme
205
+ self.language = LANG_MAP.get(language.lower(), language.lower())
206
+ self.child_mode = child_mode
207
+
208
+ response = self.session.post(f"https://{self.language}.akinator.com/game", data={"sid": THEME_IDS[theme], "cm": str(child_mode).lower()})
209
+ response.raise_for_status()
210
+ text = response.text
211
+
212
+ self.session_id = search(r"#session'\).val\('(.+?)'\)", text).group(1)
213
+ self.signature = search(r"#signature'\).val\('(.+?)'\)", text).group(1)
214
+ self.identifiant = search(r"#identifiant'\).val\('(.+?)'\)", text).group(1)
215
+
216
+ if not all([self.session_id, self.signature, self.identifiant]):
217
+ raise ValueError("Failed to extract session information from the response.")
218
+
219
+ question = search(r'<div class="bubble-body"><p class="question-text" id="question-label">(.+)</p></div>', text)
220
+
221
+ if not question:
222
+ raise ValueError("Failed to extract the initial question from the response.")
223
+
224
+ self.question = unescape(question.group(1))
225
+
226
+ proposition = search(r'<div class="sub-bubble-propose"><p id="p-sub-bubble">([\w\s]+)</p></div>', text)
227
+
228
+ if not proposition:
229
+ raise ValueError("Failed to extract the proposition from the response.")
230
+
231
+ self.proposition = unescape(proposition.group(1))
232
+ self.progression = 0
233
+ self.step = 0
234
+ self.akitude = "defi.png"
235
+ except Exception as e:
236
+ raise RuntimeError("Failed to start the game.") from e
237
+
238
+ return self.session
239
+
240
+ def answer(self, answer: str):
241
+ """
242
+ Submits an answer to the current question.
243
+
244
+ :param answer: The answer to submit. Can be "yes", "no", "i don't know", "probably", or "probably not".
245
+ :type answer: str
246
+ """
247
+ if not answer.lower() in ANSWER_MAP:
248
+ raise InvalidChoiceError(f"Invalid answer: {answer}. Valid answers are: {', '.join(ANSWER_MAP.keys())}")
249
+ answer_id = ANSWER_MAP[answer.lower()]
250
+
251
+ if self.win:
252
+ if answer_id == 0:
253
+ return self.choose()
254
+ if answer_id == 1:
255
+ return self.exclude()
256
+ raise InvalidChoiceError("Invalid answer after Akinator has proposed a win. Only 'yes' or 'no' are valid answers at this point.")
257
+
258
+ url = f"https://{self.language}.akinator.com/answer"
259
+ data = {
260
+ "step": self.step,
261
+ "progression": self.progression,
262
+ "sid": THEME_IDS[self.theme],
263
+ "cm": str(self.child_mode).lower(),
264
+ "answer": answer_id,
265
+ "step_last_proposition": self.step_last_proposition,
266
+ "session": self.session_id,
267
+ "signature": self.signature
268
+ }
269
+
270
+ try:
271
+ response = self.session.post(url, data=data)
272
+ self.__handler(response)
273
+ except Exception as e:
274
+ raise RuntimeError("Failed to submit the answer.") from e
275
+
276
+ def back(self):
277
+ """
278
+ Goes back to the previous question in the game.
279
+
280
+ .. note::
281
+
282
+ This method can only be called if the current step is greater than 0. If the step is 0, it raises a `CantGoBackAnyFurther` exception.
283
+ """
284
+ if self.step == 0:
285
+ raise CantGoBackAnyFurther()
286
+
287
+ url = f"https://{self.language}.akinator.com/cancel_answer"
288
+ data = {
289
+ "step": self.step,
290
+ "progression": self.progression,
291
+ "sid": THEME_IDS[self.theme],
292
+ "cm": str(self.child_mode).lower(),
293
+ "session": self.session_id,
294
+ "signature": self.signature
295
+ }
296
+ self.win = False
297
+
298
+ try:
299
+ response = self.session.post(url, data=data)
300
+ self.__handler(response)
301
+ except Exception as e:
302
+ raise RuntimeError("Failed to go back to the previous question.") from e
303
+
304
+ def exclude(self):
305
+ """
306
+ Excludes the current proposition from the game.
307
+
308
+ .. note::
309
+
310
+ This method can only be called after Akinator has proposed a win. If the game is already finished, it will call the `defeat` method.
311
+ """
312
+ if not self.win:
313
+ raise RuntimeError("You can only exclude a proposition after Akinator has proposed a win.")
314
+
315
+ if self.finished:
316
+ return self.defeat()
317
+
318
+ url = f"https://{self.language}.akinator.com/exclude"
319
+ data = {
320
+ "step": self.step,
321
+ "progression": self.progression,
322
+ "sid": THEME_IDS[self.theme],
323
+ "cm": str(self.child_mode).lower(),
324
+ "session": self.session_id,
325
+ "signature": self.signature
326
+ }
327
+ self.win = False
328
+ self.id_proposition = ""
329
+
330
+ try:
331
+ response = self.session.post(url, data=data)
332
+ self.__handler(response)
333
+ except Exception as e:
334
+ raise RuntimeError("Failed to exclude the proposition.") from e
335
+
336
+ def choose(self):
337
+ """
338
+ Chooses the current proposition as the answer to the game.
339
+
340
+ .. note::
341
+
342
+ This method can only be called after Akinator has proposed a win. If the game is already finished, it will raise a `RuntimeError`.
343
+ """
344
+ if not self.win:
345
+ raise RuntimeError("You can only choose a proposition after Akinator has proposed a win.")
346
+
347
+ url = f"https://{self.language}.akinator.com/choice"
348
+ data = {
349
+ "step": self.step,
350
+ "sid": THEME_IDS[self.theme],
351
+ "session": self.session_id,
352
+ "signature": self.signature,
353
+ "identifiant": self.identifiant,
354
+ "pid": self.id_proposition,
355
+ "charac_name": self.name_proposition,
356
+ "charac_description": self.description_proposition,
357
+ "pflag_photo": self.flag_photo
358
+ }
359
+
360
+ try:
361
+ response = self.session.post(url, data=data, allow_redirects=True)
362
+ if response.status_code not in range(200, 400):
363
+ response.raise_for_status()
364
+
365
+ self.finished = True
366
+ self.win = True
367
+ self.akitude = "triomphe.png"
368
+ self.id_proposition = ""
369
+ except Exception as e:
370
+ raise RuntimeError("Failed to choose the proposition.") from e
371
+
372
+ try:
373
+ text = response.text
374
+ win_message = unescape(search(r'<span class="win-sentence">(.+?)<\/span>', text).group(1))
375
+ already_played = unescape(search(r'let tokenDejaJoue = "([\w\s]+)";', text).group(1))
376
+ times_selected = search(r'let timesSelected = "(\d+)";', text).group(1)
377
+ times = unescape(search(r'<span id="timesselected"><\/span>\s+([\w\s]+)<\/span>', text).group(1))
378
+ self.question = f"{win_message}\n{already_played} {times_selected} {times}"
379
+ except Exception:
380
+ pass
381
+
382
+ self.progression = 100
383
+
384
+ def defeat(self):
385
+ """
386
+ Handles the defeat scenario in the game.
387
+ """
388
+ self.finished = True
389
+ self.win = False
390
+ self.akitude = "deception.png"
391
+ self.id_proposition = ""
392
+
393
+ questions = {
394
+ "en": "Bravo, you have defeated me !\nShare your feat with your friends.",
395
+ "ar": "أحسنت، لقد هزمتني !\nشارك إنجازك مع أصدقائك.",
396
+ "cn": "太棒了,你打败了我!\n与朋友分享你的成就吧。",
397
+ "de": "Bravo, du hast mich besiegt !\nTeile deinen Erfolg mit deinen Freunden.",
398
+ "es": "¡Bravo, me has derrotado !\nComparte tu hazaña con tus amigos.",
399
+ "fr": "Bravo, tu m'as vaincu !\nPartage ton exploit avec tes amis.",
400
+ "il": "כל הכבוד, הצלחת להביס אותי !\nשתף את ההישג שלך עם חברים.",
401
+ "it": "Bravo, mi hai sconfitto !\nCondividi la tua impresa con i tuoi amici.",
402
+ "jp": "すごい、あなたは私を倒しました!\nこの偉業を友達と共有しましょう。",
403
+ "kr": "브라보, 당신이 저를 이겼습니다 !\n당신의 업적을 친구들과 공유하세요.",
404
+ "nl": "Bravo, je hebt me verslagen !\nDeel je prestatie met je vrienden.",
405
+ "pl": "Brawo, pokonałeś mnie !\nPodziel się swoim wyczynem ze znajomymi.",
406
+ "pt": "Bravo, você me derrotou !\nCompartilhe sua conquista com seus amigos.",
407
+ "ru": "Браво, ты победил меня !\nПоделись своим достижением с друзьями.",
408
+ "tr": "Bravo, beni yendin !\nBu başarını arkadaşlarınla paylaş.",
409
+ "id": "Hebat, kamu mengalahkanku !\nBagikan pencapaianmu kepada teman-temanmu.",
410
+ }
411
+
412
+ self.question = questions[self.language]
413
+ self.progression = 100
414
+
415
+ @property
416
+ def confidence(self) -> float:
417
+ """
418
+ Returns the confidence level of the current question.
419
+ """
420
+ return self.progression / 100
421
+
422
+ @property
423
+ def theme_id(self) -> int:
424
+ """
425
+ Returns the ID of the current theme.
426
+ """
427
+ return THEME_IDS[self.theme]
428
+
429
+ @property
430
+ def theme_name(self) -> str:
431
+ """
432
+ Returns the name of the current theme.
433
+ """
434
+ return "Characters" if self.theme == "c" else "Animals" if self.theme == "a" else "Objects"
435
+
436
+ @property
437
+ def akitude_url(self) -> str:
438
+ """
439
+ Returns the URL of the current akitude image.
440
+ """
441
+ return f"https://{self.language}.akinator.com/assets/img/akitudes_670x1096/{self.akitude}"
442
+
443
+ def __str__(self):
444
+ if self.win and not self.finished:
445
+ return f"{self.proposition} {self.name_proposition} ({self.description_proposition})"
446
+ return self.question
447
+
448
+ def __repr__(self):
449
+ return f"<Akinator Client (Language: {self.language}, Theme: {self.theme_name}, Step: {self.step}, Progression: {self.progression}%)>"
450
+
451
+ class Akinator(Client):
452
+ """
453
+ A class identical to `Client`, but created for compatibility with the previous version of the library.
454
+ """
akinator.py/akinator/exceptions.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MIT License
3
+
4
+ Copyright (c) 2025 Omkaar
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ """
24
+
25
+
26
+ class AkinatorException(Exception):
27
+ """Base exception for Akinator-related errors."""
28
+
29
+ class CantGoBackAnyFurther(AkinatorException):
30
+ """Raised when the user tries to go back but cannot."""
31
+ def __init__(self, message: str = "You are already at the first question."):
32
+ super().__init__(message)
33
+
34
+ class InvalidLanguageError(AkinatorException):
35
+ """Raised when an invalid language is specified."""
36
+ def __init__(self, message: str = "Invalid language specified."):
37
+ super().__init__(message)
38
+
39
+ class InvalidChoiceError(AkinatorException):
40
+ """Raised when an invalid choice is made."""
41
+ def __init__(self, message: str = "Invalid choice. Please choose a valid option."):
42
+ super().__init__(message)
43
+
44
+ class InvalidThemeError(AkinatorException):
45
+ """Raised when an invalid theme is specified."""
46
+ def __init__(self, message: str = "Invalid theme specified."):
47
+ super().__init__(message)
akinator.py/banner.png ADDED

Git LFS Details

  • SHA256: b96f7190fd6040c4d8615986928da099b5e03acf185766a9b10ac97516bc8b0c
  • Pointer size: 132 Bytes
  • Size of remote file: 2.61 MB
akinator.py/docs/conf.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pylint: skip-file
2
+
3
+ import os
4
+ import sys
5
+
6
+ sys.path.insert(0, os.path.abspath("."))
7
+ sys.path.insert(0, os.path.abspath(".."))
8
+
9
+ on_rtd = os.environ.get("READTHEDOCS") == "True"
10
+ project = "akinator.py"
11
+ copyright = "2025, Omkaar"
12
+ author = "Ombucha"
13
+ release = "2.0.1"
14
+
15
+ extensions = ["sphinx.ext.autodoc"]
akinator.py/docs/index.rst ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ akinator.py
2
+ ==========
3
+
4
+ Installation
5
+ ------------
6
+
7
+ **Python 3.3 or higher is required.**
8
+
9
+ To install the stable version, do the following:
10
+
11
+ .. code-block:: sh
12
+
13
+ # Unix / macOS
14
+ python3 -m pip install "akinator"
15
+
16
+ # Windows
17
+ py -m pip install "akinator"
18
+
19
+
20
+ To install the development version, do the following:
21
+
22
+ .. code-block:: sh
23
+
24
+ $ git clone https://github.com/Ombucha/akinator.py
25
+
26
+ Make sure you have the latest version of Python installed, or if you prefer, a Python version of 3.9 or greater.
27
+
28
+ If you have have any other issues feel free to search for duplicates and then create a new issue on GitHub with as much detail as possible. Include the output in your terminal, your OS details and Python version.
29
+
30
+
31
+ Client
32
+ -----
33
+
34
+ .. autoclass:: akinator.Client
35
+ :members:
36
+
37
+ .. autoclass:: akinator.Akinator
38
+ :members:
39
+
40
+ .. autoclass:: akinator.AsyncClient
41
+ :members:
42
+
43
+ .. autoclass:: akinator.AsyncAkinator
44
+ :members:
45
+
46
+ Exceptions
47
+ ---------------
48
+
49
+ .. autoclass:: akinator.AkinatorException
50
+ :members:
51
+
52
+ .. autoclass:: akinator.CantGoBackAnyFurther
53
+ :members:
54
+
55
+ .. autoclass:: akinator.InvalidChoiceError
56
+ :members:
57
+
58
+ .. autoclass:: akinator.InvalidThemeError
59
+ :members:
60
+
61
+ .. autoclass:: akinator.InvalidLanguageError
62
+ :members:
akinator.py/docs/requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ sphinx
akinator.py/examples/basic.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pylint: skip-file
2
+
3
+ import akinator
4
+
5
+ aki = akinator.Akinator()
6
+ aki.start_game()
7
+
8
+ while not aki.finished:
9
+ print(f"\nQuestion: {str(aki)}")
10
+ user_input = input(
11
+ "Your answer ([y]es/[n]o/[i] don't know/[p]robably/[pn] probably not, [b]ack): "
12
+ ).strip().lower()
13
+ if user_input == "b":
14
+ try:
15
+ aki.back()
16
+ except akinator.CantGoBackAnyFurther:
17
+ print("You can't go back any further!")
18
+ else:
19
+ try:
20
+ aki.answer(user_input)
21
+ except akinator.InvalidChoiceError:
22
+ print("Invalid answer. Please try again.")
23
+
24
+ print("\n--- Game Over ---")
25
+ print(f"Proposition: {aki.name_proposition}")
26
+ print(f"Description: {aki.description_proposition}")
27
+ print(f"Pseudo: {aki.pseudo}")
28
+ print(f"Photo: {aki.photo}")
29
+ print(f"Final Message: {aki.question}")
akinator.py/examples/basic_async.py ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pylint: skip-file
2
+
3
+ import asyncio
4
+ import akinator
5
+
6
+ aki = akinator.Akinator()
7
+
8
+ async def play():
9
+ await aki.start_game()
10
+
11
+ while not aki.finished:
12
+ print(f"\nQuestion: {str(aki)}")
13
+ user_input = input(
14
+ "Your answer ([y]es/[n]o/[i] don't know/[p]robably/[pn] probably not, [b]ack): "
15
+ ).strip().lower()
16
+ if user_input == "b":
17
+ try:
18
+ await aki.back()
19
+ except akinator.CantGoBackAnyFurther:
20
+ print("You can't go back any further!")
21
+ else:
22
+ try:
23
+ await aki.answer(user_input)
24
+ except akinator.InvalidChoiceError:
25
+ print("Invalid answer. Please try again.")
26
+
27
+ print("\n--- Game Over ---")
28
+ print(f"Proposition: {aki.name_proposition}")
29
+ print(f"Description: {aki.description_proposition}")
30
+ print(f"Pseudo: {aki.pseudo}")
31
+ print(f"Photo: {aki.photo}")
32
+ print(f"Final Message: {aki.question}")
33
+
34
+ asyncio.run(play())
akinator.py/examples/discord_bot.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pylint: skip-file
2
+
3
+ import discord
4
+ import akinator
5
+
6
+ from discord.ext import commands
7
+
8
+ intents = discord.Intents.default()
9
+ bot = commands.Bot(command_prefix="!", intents=intents)
10
+
11
+ class AkiView(discord.ui.View):
12
+ def __init__(self, aki, user):
13
+ super().__init__(timeout=120)
14
+ self.aki = aki
15
+ self.user = user
16
+ self.value = None
17
+
18
+ async def interaction_check(self, interaction: discord.Interaction) -> bool:
19
+ return interaction.user == self.user
20
+
21
+ @discord.ui.button(label="Yes", style=discord.ButtonStyle.green, row=0)
22
+ async def yes(self, interaction: discord.Interaction, button: discord.ui.Button):
23
+ await interaction.response.defer()
24
+ self.value = "yes"
25
+ self.stop()
26
+
27
+ @discord.ui.button(label="No", style=discord.ButtonStyle.red, row=0)
28
+ async def no(self, interaction: discord.Interaction, button: discord.ui.Button):
29
+ await interaction.response.defer()
30
+ self.value = "no"
31
+ self.stop()
32
+
33
+ @discord.ui.button(label="Probably", style=discord.ButtonStyle.blurple, row=1)
34
+ async def probably(self, interaction: discord.Interaction, button: discord.ui.Button):
35
+ await interaction.response.defer()
36
+ self.value = "p"
37
+ self.stop()
38
+
39
+ @discord.ui.button(label="Probably Not", style=discord.ButtonStyle.blurple, row=1)
40
+ async def probably_not(self, interaction: discord.Interaction, button: discord.ui.Button):
41
+ await interaction.response.defer()
42
+ self.value = "pn"
43
+ self.stop()
44
+
45
+ @discord.ui.button(label="I don't know", style=discord.ButtonStyle.gray, row=1)
46
+ async def idk(self, interaction: discord.Interaction, button: discord.ui.Button):
47
+ await interaction.response.defer()
48
+ self.value = "i"
49
+ self.stop()
50
+
51
+ @discord.ui.button(label="Back", style=discord.ButtonStyle.secondary, row=2)
52
+ async def back(self, interaction: discord.Interaction, button: discord.ui.Button):
53
+ await interaction.response.defer()
54
+ self.value = "back"
55
+ self.stop()
56
+
57
+ class GuessView(discord.ui.View):
58
+ def __init__(self, user):
59
+ super().__init__(timeout=60)
60
+ self.user = user
61
+ self.value = None
62
+
63
+ async def interaction_check(self, interaction: discord.Interaction) -> bool:
64
+ return interaction.user == self.user
65
+
66
+ @discord.ui.button(label="Yes", style=discord.ButtonStyle.green, row=0)
67
+ async def yes(self, interaction: discord.Interaction, button: discord.ui.Button):
68
+ await interaction.response.defer()
69
+ self.value = "yes"
70
+ self.stop()
71
+
72
+ @discord.ui.button(label="No", style=discord.ButtonStyle.red, row=0)
73
+ async def no(self, interaction: discord.Interaction, button: discord.ui.Button):
74
+ await interaction.response.defer()
75
+ self.value = "no"
76
+ self.stop()
77
+
78
+ @bot.tree.command(name="akinator", description="Play Akinator in Discord!")
79
+ async def akinator_slash(interaction: discord.Interaction):
80
+ await interaction.response.defer()
81
+ aki = akinator.AsyncAkinator()
82
+ await aki.start_game()
83
+ embed = discord.Embed(title="Akinator", description=aki.question, color=discord.Color.blurple())
84
+ embed.set_image(url=aki.akitude_url)
85
+ view = AkiView(aki, interaction.user)
86
+ message = await interaction.edit_original_response(embed=embed, view=view)
87
+ while not aki.finished:
88
+ await view.wait()
89
+ if view.value == "back":
90
+ try:
91
+ await aki.back()
92
+ except akinator.CantGoBackAnyFurther:
93
+ await message.edit(content="You can't go back any further!", embed=None, view=None)
94
+ return
95
+ elif view.value in ["yes", "no", "i", "p", "pn"]:
96
+ try:
97
+ await aki.answer(view.value)
98
+ except akinator.InvalidChoiceError:
99
+ await message.edit(content="Invalid answer. Please try again.", embed=None, view=None)
100
+ return
101
+ else:
102
+ await message.edit(content="Game timed out or cancelled.", embed=None, view=None)
103
+ return
104
+ desc = str(aki)
105
+ embed = discord.Embed(title="Akinator", description=desc, color=discord.Color.blurple())
106
+ embed.set_image(url=aki.akitude_url)
107
+ if desc.strip().lower().startswith("i think of"):
108
+ view = GuessView(interaction.user)
109
+ else:
110
+ view = AkiView(aki, interaction.user)
111
+ await message.edit(embed=embed, view=view)
112
+ result = discord.Embed(title="Akinator's Guess!", color=discord.Color.gold())
113
+ result.add_field(name="Proposition", value=aki.name_proposition or "?", inline=False)
114
+ result.add_field(name="Description", value=aki.description_proposition or "?", inline=False)
115
+ if aki.photo:
116
+ result.set_image(url=aki.photo)
117
+ result.set_footer(text=aki.question)
118
+ await message.edit(embed=result, view=None)
119
+
120
+ @bot.event
121
+ async def on_ready():
122
+ await bot.tree.sync()
123
+ print(f"Logged in as {bot.user}")
124
+
125
+ bot.run('TOKEN')
akinator.py/pyproject.toml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [tool.poetry]
2
+ name = "akinator.py"
3
+ version = "2.0.1"
4
+ description = "A basic API wrapper for Akinator."
5
+ authors = ["Ombucha <omkaar.nerurkar@gmail.com>"]
6
+ license = "MIT"
7
+ readme = "README.rst"
8
+ repository = "https://github.com/Ombucha/akinator.py/"
9
+ documentation = "https://akinator.readthedocs.io/"
10
+ keywords = ["python", "akinator"]
11
+ classifiers = [
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python",
14
+ "Programming Language :: Python :: 3",
15
+ ]
16
+ packages = ["akinator"]
17
+
18
+ [tool.poetry.dependencies]
19
+ cloudscraper = "*"
20
+
21
+ [tool.poetry.urls]
22
+ "Bug Tracker" = "https://github.com/Ombucha/akinator.py/issues"
akinator.py/requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ cloudscraper
akinator.py/setup.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pylint: skip-file
2
+
3
+ from pathlib import Path
4
+
5
+ from setuptools import setup
6
+
7
+ HERE = Path(__file__).resolve().parent
8
+ README = (HERE / "README.rst").read_text()
9
+
10
+ setup(
11
+ name = "akinator",
12
+ version = "2.0.1",
13
+ description = "A basic API wrapper for Akinator.",
14
+ long_description = README,
15
+ long_description_content_type = "text/x-rst",
16
+ url = "https://github.com/Ombucha/akinator.py",
17
+ author = "Omkaar",
18
+ author_email = "omkaar.nerurkar@gmail.com",
19
+ license = "MIT",
20
+ classifiers = [
21
+ "License :: OSI Approved :: MIT License",
22
+ "Natural Language :: English",
23
+ "Programming Language :: Python",
24
+ "Programming Language :: Python :: 3",
25
+ ],
26
+ python_requires='>= 3.9.0',
27
+ packages = ["akinator"],
28
+ include_package_data = True,
29
+ install_requires = ["cloudscraper"]
30
+ )
akinator.py/tests/test_async_client.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MIT License
3
+
4
+ Copyright (c) 2025 Omkaar
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ """
24
+
25
+
26
+ # pylint: skip-file
27
+
28
+ import unittest
29
+
30
+ from akinator import AsyncClient, CantGoBackAnyFurther, InvalidLanguageError, InvalidChoiceError, InvalidThemeError
31
+
32
+
33
+ class TestAkinatorAsyncClient(unittest.IsolatedAsyncioTestCase):
34
+ async def asyncSetUp(self):
35
+ self.client = AsyncClient()
36
+
37
+ async def test_init_defaults(self):
38
+ c = AsyncClient()
39
+ self.assertIsNotNone(c.session)
40
+ self.assertIsNone(c.language)
41
+ self.assertIsNone(c.theme)
42
+ self.assertFalse(c.child_mode)
43
+ self.assertFalse(c.finished)
44
+ self.assertFalse(c.win)
45
+
46
+ async def test_language_validation(self):
47
+ with self.assertRaises(InvalidLanguageError):
48
+ await self.client.start_game(language="notalanguage")
49
+ await self.client.start_game(language="en")
50
+ await self.client.start_game(language="english")
51
+
52
+ async def test_theme_validation(self):
53
+ with self.assertRaises(InvalidThemeError):
54
+ await self.client.start_game(theme="z")
55
+ with self.assertRaises(InvalidThemeError):
56
+ await self.client.start_game(language="ar", theme="a")
57
+ await self.client.start_game(theme="c")
58
+
59
+ async def test_answer_validation(self):
60
+ await self.client.start_game()
61
+ with self.assertRaises(InvalidChoiceError):
62
+ await self.client.answer("notananswer")
63
+
64
+ async def test_full_game_flow(self):
65
+ await self.client.start_game(language="en", theme="c")
66
+ steps = 0
67
+ while not self.client.finished and steps < 10:
68
+ try:
69
+ await self.client.answer("yes")
70
+ except InvalidChoiceError:
71
+ await self.client.answer("no")
72
+ steps += 1
73
+ self.assertTrue(self.client.finished or steps == 10)
74
+
75
+ async def test_back_and_exclude(self):
76
+ await self.client.start_game(language="en", theme="c")
77
+ with self.assertRaises(CantGoBackAnyFurther):
78
+ await self.client.back()
79
+ await self.client.answer("yes")
80
+ try:
81
+ await self.client.back()
82
+ except Exception:
83
+ self.fail("back() raised unexpectedly after moving forward")
84
+ self.client.win = False
85
+ with self.assertRaises(RuntimeError):
86
+ await self.client.exclude()
87
+
88
+ async def test_properties_and_str(self):
89
+ self.client.theme = "c"
90
+ self.client.progression = 50
91
+ self.assertEqual(self.client.confidence, 0.5)
92
+ self.assertEqual(self.client.theme_id, 1)
93
+ self.assertEqual(self.client.theme_name, "Characters")
94
+ self.client.language = "en"
95
+ self.client.akitude = "defi.png"
96
+ self.assertIn("defi.png", self.client.akitude_url)
97
+ self.client.win = True
98
+ self.client.finished = False
99
+ self.client.proposition = "prop"
100
+ self.client.name_proposition = "name"
101
+ self.client.description_proposition = "desc"
102
+ self.assertIn("name", str(self.client))
103
+ self.client.finished = True
104
+ self.client.question = "Q?"
105
+ self.assertEqual(str(self.client), "Q?")
106
+ self.client.theme = "a"
107
+ self.assertEqual(self.client.theme_name, "Animals")
108
+ self.client.theme = "o"
109
+ self.assertEqual(self.client.theme_name, "Objects")
110
+
111
+ async def test_repr(self):
112
+ self.client.language = "en"
113
+ self.client.theme = "c"
114
+ self.client.step = 5
115
+ self.client.progression = 80
116
+ rep = repr(self.client)
117
+ self.assertIn("Akinator Client", rep)
118
+ self.assertIn("Language: en", rep)
119
+ self.assertIn("Theme: Characters", rep)
120
+ self.assertIn("Step: 5", rep)
121
+ self.assertIn("Progression: 80", rep)
122
+
123
+ async def test_defeat_sets_state(self):
124
+ self.client.language = "en"
125
+ await self.client.defeat()
126
+ self.assertTrue(self.client.finished)
127
+ self.assertFalse(self.client.win)
128
+ self.assertEqual(self.client.akitude, "deception.png")
129
+ self.assertEqual(self.client.question, "Bravo, you have defeated me !\nShare your feat with your friends.")
130
+ self.assertEqual(self.client.progression, 100)
131
+
132
+ async def test_choose_without_win(self):
133
+ await self.client.start_game(language="en", theme="c")
134
+ self.client.win = False
135
+ with self.assertRaises(RuntimeError):
136
+ await self.client.choose()
137
+
138
+ async def test_exclude_after_win_and_finished(self):
139
+ await self.client.start_game(language="en", theme="c")
140
+ self.client.win = True
141
+ self.client.finished = True
142
+ self.client.language = "en"
143
+ await self.client.defeat()
144
+ self.assertTrue(self.client.finished)
145
+ self.assertFalse(self.client.win)
146
+ self.assertEqual(self.client.akitude, "deception.png")
147
+
148
+ async def test_theme_ids_and_names(self):
149
+ self.client.theme = "c"
150
+ self.assertEqual(self.client.theme_id, 1)
151
+ self.assertEqual(self.client.theme_name, "Characters")
152
+ self.client.theme = "a"
153
+ self.assertEqual(self.client.theme_id, 14)
154
+ self.assertEqual(self.client.theme_name, "Animals")
155
+ self.client.theme = "o"
156
+ self.assertEqual(self.client.theme_id, 2)
157
+ self.assertEqual(self.client.theme_name, "Objects")
158
+
159
+ async def test_repr_and_str_edge_cases(self):
160
+ self.client.language = None
161
+ self.client.theme = None
162
+ self.client.step = None
163
+ self.client.progression = None
164
+ rep = repr(self.client)
165
+ self.assertIn("Akinator Client", rep)
166
+ self.client.win = False
167
+ self.client.finished = False
168
+ self.client.question = "TestQ"
169
+ self.assertEqual(str(self.client), "TestQ")
170
+
171
+ if __name__ == "__main__":
172
+ unittest.main()
akinator.py/tests/test_client.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MIT License
3
+
4
+ Copyright (c) 2025 Omkaar
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+ """
24
+
25
+
26
+ # pylint: skip-file
27
+
28
+ import unittest
29
+
30
+ from akinator import Client, CantGoBackAnyFurther, InvalidLanguageError, InvalidChoiceError, InvalidThemeError
31
+
32
+
33
+ class TestAkinatorClient(unittest.TestCase):
34
+ def setUp(self):
35
+ self.client = Client()
36
+
37
+ def test_init_defaults(self):
38
+ c = Client()
39
+ self.assertIsNotNone(c.session)
40
+ self.assertIsNone(c.language)
41
+ self.assertIsNone(c.theme)
42
+ self.assertFalse(c.child_mode)
43
+ self.assertFalse(c.finished)
44
+ self.assertFalse(c.win)
45
+
46
+ def test_language_validation(self):
47
+ with self.assertRaises(InvalidLanguageError):
48
+ self.client.start_game(language="notalanguage")
49
+ # Should not raise
50
+ self.client.start_game(language="en")
51
+ self.client.start_game(language="english")
52
+
53
+ def test_theme_validation(self):
54
+ with self.assertRaises(InvalidThemeError):
55
+ self.client.start_game(theme="z")
56
+ with self.assertRaises(InvalidThemeError):
57
+ self.client.start_game(language="ar", theme="a") # 'a' not in ar
58
+ # Should not raise
59
+ self.client.start_game(theme="c")
60
+
61
+ def test_answer_validation(self):
62
+ self.client.start_game()
63
+ with self.assertRaises(InvalidChoiceError):
64
+ self.client.answer("notananswer")
65
+
66
+ def test_full_game_flow(self):
67
+ self.client.start_game(language="en", theme="c")
68
+ steps = 0
69
+ while not self.client.finished and steps < 10:
70
+ # Always answer 'yes' for simplicity
71
+ try:
72
+ self.client.answer("yes")
73
+ except InvalidChoiceError:
74
+ self.client.answer("no")
75
+ steps += 1
76
+ self.assertTrue(self.client.finished or steps == 10)
77
+
78
+ def test_back_and_exclude(self):
79
+ self.client.start_game(language="en", theme="c")
80
+ with self.assertRaises(CantGoBackAnyFurther):
81
+ self.client.back()
82
+ # Move forward one step
83
+ self.client.answer("yes")
84
+ try:
85
+ self.client.back()
86
+ except Exception:
87
+ self.fail("back() raised unexpectedly after moving forward")
88
+ # Exclude only after win
89
+ self.client.win = False
90
+ with self.assertRaises(RuntimeError):
91
+ self.client.exclude()
92
+
93
+ def test_properties_and_str(self):
94
+ self.client.theme = "c"
95
+ self.client.progression = 50
96
+ self.assertEqual(self.client.confidence, 0.5)
97
+ self.assertEqual(self.client.theme_id, 1)
98
+ self.assertEqual(self.client.theme_name, "Characters")
99
+ self.client.language = "en"
100
+ self.client.akitude = "defi.png"
101
+ self.assertIn("defi.png", self.client.akitude_url)
102
+ self.client.win = True
103
+ self.client.finished = False
104
+ self.client.proposition = "prop"
105
+ self.client.name_proposition = "name"
106
+ self.client.description_proposition = "desc"
107
+ self.assertIn("name", str(self.client))
108
+ self.client.finished = True
109
+ self.client.question = "Q?"
110
+ self.assertEqual(str(self.client), "Q?")
111
+ self.client.theme = "a"
112
+ self.assertEqual(self.client.theme_name, "Animals")
113
+ self.client.theme = "o"
114
+ self.assertEqual(self.client.theme_name, "Objects")
115
+
116
+ def test_repr(self):
117
+ self.client.language = "en"
118
+ self.client.theme = "c"
119
+ self.client.step = 5
120
+ self.client.progression = 80
121
+ rep = repr(self.client)
122
+ self.assertIn("Akinator Client", rep)
123
+ self.assertIn("Language: en", rep)
124
+ self.assertIn("Theme: Characters", rep)
125
+ self.assertIn("Step: 5", rep)
126
+ self.assertIn("Progression: 80", rep)
127
+
128
+ def test_defeat_sets_state(self):
129
+ self.client.language = "en"
130
+ self.client.defeat()
131
+ self.assertTrue(self.client.finished)
132
+ self.assertFalse(self.client.win)
133
+ self.assertEqual(self.client.akitude, "deception.png")
134
+ self.assertEqual(self.client.question, "Bravo, you have defeated me !\nShare your feat with your friends.")
135
+ self.assertEqual(self.client.progression, 100)
136
+
137
+ def test_choose_without_win(self):
138
+ self.client.start_game(language="en", theme="c")
139
+ self.client.win = False
140
+ with self.assertRaises(RuntimeError):
141
+ self.client.choose()
142
+
143
+ def test_exclude_after_win_and_finished(self):
144
+ self.client.start_game(language="en", theme="c")
145
+ self.client.win = True
146
+ self.client.finished = True
147
+ # Should call defeat and set finished/win
148
+ self.client.language = "en"
149
+ self.client.defeat()
150
+ self.assertTrue(self.client.finished)
151
+ self.assertFalse(self.client.win)
152
+ self.assertEqual(self.client.akitude, "deception.png")
153
+
154
+ def test_theme_ids_and_names(self):
155
+ self.client.theme = "c"
156
+ self.assertEqual(self.client.theme_id, 1)
157
+ self.assertEqual(self.client.theme_name, "Characters")
158
+ self.client.theme = "a"
159
+ self.assertEqual(self.client.theme_id, 14)
160
+ self.assertEqual(self.client.theme_name, "Animals")
161
+ self.client.theme = "o"
162
+ self.assertEqual(self.client.theme_id, 2)
163
+ self.assertEqual(self.client.theme_name, "Objects")
164
+
165
+ def test_repr_and_str_edge_cases(self):
166
+ self.client.language = None
167
+ self.client.theme = None
168
+ self.client.step = None
169
+ self.client.progression = None
170
+ rep = repr(self.client)
171
+ self.assertIn("Akinator Client", rep)
172
+ self.client.win = False
173
+ self.client.finished = False
174
+ self.client.question = "TestQ"
175
+ self.assertEqual(str(self.client), "TestQ")
176
+
177
+ if __name__ == "__main__":
178
+ unittest.main()