├── .gitignore ├── requirements.txt ├── .devcontainer ├── startup.sh ├── Dockerfile └── devcontainer.json ├── .github ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md ├── workflows │ └── main.yml └── ISSUE_TEMPLATE.md ├── extensions └── cat-trap-game-ui-1.0.0.vsix ├── CONTRIBUTING.md ├── .vscode └── settings.json ├── NOTICE ├── README.md ├── src ├── main.py ├── Ch01 │ ├── 01_07 │ │ ├── main.py │ │ └── cat_trap_algorithms.py │ └── 01_08 │ │ ├── main.py │ │ └── cat_trap_algorithms.py ├── Ch02 │ ├── 02_05 │ │ ├── main.py │ │ └── cat_trap_algorithms.py │ ├── 02_06 │ │ ├── main.py │ │ └── cat_trap_algorithms.py │ ├── 02_09 │ │ ├── main.py │ │ └── cat_trap_algorithms.py │ └── 02_10 │ │ ├── main.py │ │ └── cat_trap_algorithms.py ├── Ch03 │ ├── 03_04 │ │ └── main.py │ ├── 03_05 │ │ ├── main.py │ │ └── cat_trap_algorithms.py │ ├── 03_06 │ │ └── main.py │ └── 03_07 │ │ ├── main.py │ │ └── cat_trap_algorithms.py └── Ch04 │ ├── 04_03 │ └── main.py │ └── 04_04 │ └── main.py └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | .tmp 4 | npm-debug.log 5 | __pycache__/ -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests>=2.28 2 | websockets==10.1 3 | numpy==1.21.4 4 | asyncio==3.4.3 -------------------------------------------------------------------------------- /.devcontainer/startup.sh: -------------------------------------------------------------------------------- 1 | if [ -f requirements.txt ]; then 2 | pip install --user -r requirements.txt 3 | fi 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Codeowners for these exercise files: 2 | # * (asterisk) denotes "all files and folders" 3 | # Example: * @producer @instructor 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /extensions/cat-trap-game-ui-1.0.0.vsix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LinkedInLearning/ai-algorithms-for-game-design-with-python-3933112/main/extensions/cat-trap-game-ui-1.0.0.vsix -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Copy To Branches 2 | on: 3 | workflow_dispatch: 4 | jobs: 5 | copy-to-branches: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | with: 10 | fetch-depth: 0 11 | - name: Copy To Branches Action 12 | uses: planetoftheweb/copy-to-branches@v1.2 13 | env: 14 | key: main 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | Contribution Agreement 3 | ====================== 4 | 5 | This repository does not accept pull requests (PRs). All pull requests will be closed. 6 | 7 | However, if any contributions (through pull requests, issues, feedback or otherwise) are provided, as a contributor, you represent that the code you submit is your original work or that of your employer (in which case you represent you have the right to bind your employer). By submitting code (or otherwise providing feedback), you (and, if applicable, your employer) are licensing the submitted code (and/or feedback) to LinkedIn and the open source community subject to the BSD 2-Clause license. 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.bracketPairColorization.enabled": true, 3 | "editor.cursorBlinking": "solid", 4 | "editor.fontFamily": "ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace", 5 | "editor.fontLigatures": false, 6 | "editor.fontSize": 22, 7 | "editor.formatOnPaste": true, 8 | "editor.formatOnSave": true, 9 | "editor.lineNumbers": "on", 10 | "editor.matchBrackets": "always", 11 | "editor.minimap.enabled": false, 12 | "editor.smoothScrolling": true, 13 | "editor.tabSize": 2, 14 | "editor.useTabStops": true, 15 | "editor.wordWrap": "on", 16 | "emmet.triggerExpansionOnTab": true, 17 | "explorer.openEditors.visible": 1, 18 | "files.autoSave": "afterDelay", 19 | "screencastMode.onlyKeyboardShortcuts": true, 20 | "terminal.integrated.fontSize": 18, 21 | "workbench.colorTheme": "Visual Studio Dark", 22 | "workbench.fontAliasing": "antialiased", 23 | "workbench.statusBar.visible": true, 24 | "explorer.compactFolders": false 25 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 7 | 8 | ## Issue Overview 9 | 10 | 11 | ## Describe your environment 12 | 13 | 14 | ## Steps to Reproduce 15 | 16 | 1. 17 | 2. 18 | 3. 19 | 4. 20 | 21 | ## Expected Behavior 22 | 23 | 24 | ## Current Behavior 25 | 26 | 27 | ## Possible Solution 28 | 29 | 30 | ## Screenshots / Video 31 | 32 | 33 | ## Related Issues 34 | 35 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/python-3/.devcontainer/base.Dockerfile 2 | 3 | # [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster 4 | ARG VARIANT="3.10" 5 | FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT} 6 | 7 | # [Choice] Node.js version: none, lts/*, 16, 14, 12, 10 8 | ARG NODE_VERSION="none" 9 | RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi 10 | 11 | # [Optional] If your pip requirements rarely change, uncomment this section to add them to the image. 12 | # COPY requirements.txt /tmp/pip-tmp/ 13 | # RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \ 14 | # && rm -rf /tmp/pip-tmp 15 | 16 | # [Optional] Uncomment this section to install additional OS packages. 17 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 18 | # && apt-get -y install --no-install-recommends 19 | 20 | # [Optional] Uncomment this line to install global node packages. 21 | # RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g " 2>&1 22 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2025 LinkedIn Corporation 2 | All Rights Reserved. 3 | 4 | Licensed under the LinkedIn Learning Exercise File License (the "License"). 5 | See LICENSE in the project root for license information. 6 | 7 | ATTRIBUTIONS: 8 | 9 | -------------------- 10 | **requests** 11 | https://github.com/psf/requests 12 | License: Apache 2.0 13 | http://www.apache.org/licenses/ 14 | -------------------- 15 | **websockets** 16 | [https://github.com/python-websockets/websockets](https://github.com/python-websockets/websockets) 17 | License: BSD-3-Clause 18 | https://opensource.org/licenses/BSD-3-Clause 19 | -------------------- 20 | **numpy** 21 | [https://github.com/numpy/numpy](https://github.com/numpy/numpy) 22 | License: BSD-3-Clause 23 | https://opensource.org/licenses/BSD-3-Clause 24 | -------------------- 25 | **asyncio** 26 | [https://github.com/python/asyncio](https://github.com/python/asyncio) 27 | License: Apache 2.0 28 | http://www.apache.org/licenses/ 29 | -------------------- 30 | 31 | Please note, this project may automatically load third party code from external 32 | repositories (for example, NPM modules, Composer packages, or other dependencies). 33 | If so, such third party code may be subject to other license terms than as set 34 | forth above. In addition, such third party code may also depend on and load 35 | multiple tiers of dependencies. Please review the applicable licenses of the 36 | additional dependencies. 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Algorithms for Game Design with Python 2 | This is the repository for the LinkedIn Learning course AI Algorithms for Game Design with Python. The full course is available from [LinkedIn Learning][lil-course-url]. 3 | 4 | ![lil-thumbnail-url] 5 | 6 | ## Course Description 7 | 8 | In this intermediate-level course, Eduardo Corpeño explores AI algorithms for game design. Dive into powerful strategies like minimax, alpha-beta pruning, and iterative deepening. Learn how to implement these techniques in Python and enhance your game development skills. Discover the historical context, such as the algorithms used in IBM's Deep Blue, which defeated world chess champion Garry Kasparov. By engaging with real-world examples and hands-on coding exercises, you will develop and strengthen your ability to create intelligent game algorithms. Your learning journey is made seamless with GitHub Codespaces, ensuring no setup hassles. Whether you are a game developer, AI enthusiast, or a programmer looking to sharpen your skills, this course offers in-depth knowledge and practical skills to effectively utilize AI in game design. 9 | 10 | ## Instructor 11 | 12 | Eduardo Corpeño 13 | 14 | Electrical Engineer, Computer Programmer, and Teacher for 15+ years 15 | 16 | 17 | Check out my other courses on [LinkedIn Learning](https://www.linkedin.com/learning/instructors/eduardo-corpeno?u=104). 18 | 19 | [0]: # (Replace these placeholder URLs with actual course URLs) 20 | 21 | [lil-course-url]: https://www.linkedin.com/learning/ai-algorithms-for-game-design-with-python 22 | [lil-thumbnail-url]: https://media.licdn.com/dms/image/v2/D4D0DAQH4bShKGGNQXg/learning-public-crop_675_1200/B4DZU6ZPk9GcAY-/0/1740441451638?e=2147483647&v=beta&t=cRaFvuwqpOmSazGJajDAZsx48kPio2N0bP8ZXNOga4g 23 | 24 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AI Algorithms for Game Design", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "context": "..", 6 | "args": { 7 | "VARIANT": "3.10", // Set Python version here 8 | "NODE_VERSION": "lts/*" 9 | } 10 | }, 11 | "customizations": { 12 | "codespaces": { 13 | "openFiles": [ 14 | "README.md" 15 | ] 16 | }, 17 | "vscode": { 18 | // Set *default* container specific settings.json values on container create. 19 | "settings": { 20 | "terminal.integrated.shell.linux": "/bin/bash", 21 | "python.defaultInterpreterPath": "/usr/local/bin/python", 22 | "python.linting.enabled": true, 23 | "python.linting.pylintEnabled": true, 24 | "python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8", 25 | "python.formatting.blackPath": "/usr/local/py-utils/bin/black", 26 | "python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf", 27 | "python.linting.banditPath": "/usr/local/py-utils/bin/bandit", 28 | "python.linting.flake8Path": "/usr/local/py-utils/bin/flake8", 29 | "python.linting.mypyPath": "/usr/local/py-utils/bin/mypy", 30 | "python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle", 31 | "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", 32 | "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint", 33 | "python.linting.pylintArgs": [ 34 | "--disable=C0111" 35 | ] 36 | }, 37 | // Add the IDs of extensions you want installed when the container is created. 38 | "extensions": [ 39 | "GitHub.github-vscode-theme", 40 | "ms-python.python", 41 | "ms-python.vscode-pylance" 42 | // Additional Extensions Here 43 | ] 44 | } 45 | }, 46 | "remoteUser": "vscode", 47 | // Update welcome text and set terminal prompt to '$ ' 48 | "onCreateCommand": "echo PS1='\"$ \"' >> ~/.bashrc", 49 | // Pull all branches 50 | "postAttachCommand": "git pull --all && code --install-extension extensions/cat-trap-game-ui-1.0.0.vsix", 51 | "postCreateCommand": "sh .devcontainer/startup.sh" 52 | } -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /src/Ch01/01_07/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /src/Ch01/01_08/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /src/Ch02/02_05/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /src/Ch02/02_06/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /src/Ch02/02_09/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /src/Ch02/02_10/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /src/Ch03/03_04/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /src/Ch03/03_05/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /src/Ch03/03_06/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /src/Ch03/03_07/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /src/Ch04/04_03/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /src/Ch04/04_04/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | main.py: Entry Point for the Cat Trap Game Server 3 | 4 | This script initializes and runs the Cat Trap game server, handling client 5 | connections and managing game state updates through WebSocket communication. 6 | The client is the Cat Trap GUI VSCode extension. 7 | 8 | Usage: 9 | Run this file to start the game server. 10 | Start the Cat Trap GUI Extension: (Ctrl+Shift+P, then "Start Cat Trap Game") 11 | 12 | Dependencies: 13 | - cat_trap_algorithms: Contains the game logic and algorithms. 14 | - websockets: Used for WebSocket server communication. 15 | - asyncio: Enables asynchronous operations. 16 | """ 17 | 18 | import asyncio 19 | import json 20 | import websockets 21 | import numpy as np 22 | from cat_trap_algorithms import * 23 | from enum import Enum 24 | 25 | class GameStatus(Enum): 26 | GAME_ON = 0 27 | PLAYER_WINS = 1 28 | CAT_WINS = 2 29 | CAT_TIMEOUT = 3 30 | 31 | game_status = GameStatus.GAME_ON 32 | game = None 33 | debug_mode = False 34 | 35 | async def handler(websocket, path): 36 | global game 37 | global game_status 38 | global debug_mode 39 | try: 40 | async for message in websocket: 41 | if debug_mode: 42 | print(f'Received message: {message}') # Debug log 43 | data = json.loads(message) 44 | if data['command'] == 'new_game': 45 | game_status = GameStatus.GAME_ON 46 | game = CatTrapGame(data['size']) 47 | game.initialize_random_hexgrid() 48 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 49 | elif data['command'] == 'move': 50 | if not game: 51 | # Recreate the game using the provided grid 52 | size = len(data['grid']) 53 | game = CatTrapGame(size) 54 | game.set_hexgrid(np.array(data['grid'], dtype=int)) 55 | if game_status == GameStatus.GAME_ON: 56 | game.block_tile(data['clicked_tile']) 57 | strategy = data['strategy'] 58 | random_cat = (strategy == 'random') 59 | minimax = (strategy == 'minimax') 60 | depth_limited = (strategy == 'limited') 61 | iterative_deepening = (strategy == 'iterative') 62 | max_depth = data['depth'] 63 | alpha_beta = data['alpha_beta_pruning'] 64 | allotted_time = data['deadline'] 65 | r, c = game.cat 66 | if r == 0 or r == game.size - 1 or c == 0 or c == game.size - 1: 67 | game_status = GameStatus.CAT_WINS 68 | else: 69 | new_cat = game.select_cat_move(random_cat,minimax, alpha_beta, depth_limited, max_depth, iterative_deepening, allotted_time) 70 | if new_cat == TIMEOUT: 71 | game_status = GameStatus.CAT_TIMEOUT 72 | else: 73 | if new_cat == game.cat: 74 | game_status = GameStatus.PLAYER_WINS 75 | game.move_cat(new_cat) 76 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 77 | elif data['command'] == 'edit': 78 | if not game: 79 | # Recreate the game using the provided grid 80 | size = len(data['grid']) 81 | game = CatTrapGame(size) 82 | game.hexgrid = np.array(data['grid'], dtype=int) 83 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 84 | if cat.size > 0: # Cat may be absent in edit mode 85 | game.cat = list(cat[0]) 86 | action = data['action'] 87 | if action == 'block': 88 | game.block_tile(data['tile']) 89 | elif action == 'unblock': 90 | game.unblock_tile(data['tile']) 91 | elif action == 'place_cat': 92 | game.place_cat(data['tile']) 93 | game_status = GameStatus.GAME_ON 94 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 95 | elif data['command'] == 'request_grid': 96 | if not game: 97 | # Recreate the game using the provided grid 98 | size = len(data['grid']) 99 | if size > 0: 100 | game = CatTrapGame(size) 101 | game.hexgrid = np.array(data['grid'], dtype=int) 102 | cat = np.argwhere(game.hexgrid == CAT_TILE) # Find the cat 103 | if cat.size > 0: # Cat may be absent in edit mode 104 | game.cat = list(cat[0]) 105 | else: 106 | game = CatTrapGame(7) 107 | game.initialize_random_hexgrid() 108 | game_status = GameStatus.GAME_ON 109 | await websocket.send(json.dumps({'command': 'updateGrid', 'data': json.dumps(game.hexgrid.tolist())})) 110 | 111 | if game and (game_status != GameStatus.GAME_ON): 112 | await websocket.send(json.dumps({'command': 'endgame', 'reason': game_status.value})) 113 | if game_status == GameStatus.CAT_TIMEOUT: 114 | game_status = GameStatus.GAME_ON 115 | 116 | except websockets.exceptions.ConnectionClosedError as e: 117 | print(f"Connection closed: {e}") 118 | except asyncio.CancelledError: 119 | print("WebSocket handler task was cancelled.") 120 | except Exception as e: 121 | print(f"Unexpected error: {e}") 122 | finally: 123 | print("Cleaning up after the connection.") 124 | 125 | 126 | async def main(): 127 | async with websockets.serve(handler, 'localhost', 8765): 128 | await asyncio.Future() # Run forever 129 | 130 | if __name__ == '__main__': 131 | asyncio.run(main()) 132 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | LinkedIn Learning Exercise Files License Agreement 2 | ================================================== 3 | 4 | This License Agreement (the "Agreement") is a binding legal agreement 5 | between you (as an individual or entity, as applicable) and LinkedIn 6 | Corporation (“LinkedIn”). By downloading or using the LinkedIn Learning 7 | exercise files in this repository (“Licensed Materials”), you agree to 8 | be bound by the terms of this Agreement. If you do not agree to these 9 | terms, do not download or use the Licensed Materials. 10 | 11 | 1. License. 12 | - a. Subject to the terms of this Agreement, LinkedIn hereby grants LinkedIn 13 | members during their LinkedIn Learning subscription a non-exclusive, 14 | non-transferable copyright license, for internal use only, to 1) make a 15 | reasonable number of copies of the Licensed Materials, and 2) make 16 | derivative works of the Licensed Materials for the sole purpose of 17 | practicing skills taught in LinkedIn Learning courses. 18 | - b. Distribution. Unless otherwise noted in the Licensed Materials, subject 19 | to the terms of this Agreement, LinkedIn hereby grants LinkedIn members 20 | with a LinkedIn Learning subscription a non-exclusive, non-transferable 21 | copyright license to distribute the Licensed Materials, except the 22 | Licensed Materials may not be included in any product or service (or 23 | otherwise used) to instruct or educate others. 24 | 25 | 2. Restrictions and Intellectual Property. 26 | - a. You may not to use, modify, copy, make derivative works of, publish, 27 | distribute, rent, lease, sell, sublicense, assign or otherwise transfer the 28 | Licensed Materials, except as expressly set forth above in Section 1. 29 | - b. Linkedin (and its licensors) retains its intellectual property rights 30 | in the Licensed Materials. Except as expressly set forth in Section 1, 31 | LinkedIn grants no licenses. 32 | - c. You indemnify LinkedIn and its licensors and affiliates for i) any 33 | alleged infringement or misappropriation of any intellectual property rights 34 | of any third party based on modifications you make to the Licensed Materials, 35 | ii) any claims arising from your use or distribution of all or part of the 36 | Licensed Materials and iii) a breach of this Agreement. You will defend, hold 37 | harmless, and indemnify LinkedIn and its affiliates (and our and their 38 | respective employees, shareholders, and directors) from any claim or action 39 | brought by a third party, including all damages, liabilities, costs and 40 | expenses, including reasonable attorneys’ fees, to the extent resulting from, 41 | alleged to have resulted from, or in connection with: (a) your breach of your 42 | obligations herein; or (b) your use or distribution of any Licensed Materials. 43 | 44 | 3. Open source. This code may include open source software, which may be 45 | subject to other license terms as provided in the files. 46 | 47 | 4. Warranty Disclaimer. LINKEDIN PROVIDES THE LICENSED MATERIALS ON AN “AS IS” 48 | AND “AS AVAILABLE” BASIS. LINKEDIN MAKES NO REPRESENTATION OR WARRANTY, 49 | WHETHER EXPRESS OR IMPLIED, ABOUT THE LICENSED MATERIALS, INCLUDING ANY 50 | REPRESENTATION THAT THE LICENSED MATERIALS WILL BE FREE OF ERRORS, BUGS OR 51 | INTERRUPTIONS, OR THAT THE LICENSED MATERIALS ARE ACCURATE, COMPLETE OR 52 | OTHERWISE VALID. TO THE FULLEST EXTENT PERMITTED BY LAW, LINKEDIN AND ITS 53 | AFFILIATES DISCLAIM ANY IMPLIED OR STATUTORY WARRANTY OR CONDITION, INCLUDING 54 | ANY IMPLIED WARRANTY OR CONDITION OF MERCHANTABILITY OR FITNESS FOR A 55 | PARTICULAR PURPOSE, AVAILABILITY, SECURITY, TITLE AND/OR NON-INFRINGEMENT. 56 | YOUR USE OF THE LICENSED MATERIALS IS AT YOUR OWN DISCRETION AND RISK, AND 57 | YOU WILL BE SOLELY RESPONSIBLE FOR ANY DAMAGE THAT RESULTS FROM USE OF THE 58 | LICENSED MATERIALS TO YOUR COMPUTER SYSTEM OR LOSS OF DATA. NO ADVICE OR 59 | INFORMATION, WHETHER ORAL OR WRITTEN, OBTAINED BY YOU FROM US OR THROUGH OR 60 | FROM THE LICENSED MATERIALS WILL CREATE ANY WARRANTY OR CONDITION NOT 61 | EXPRESSLY STATED IN THESE TERMS. 62 | 63 | 5. Limitation of Liability. LINKEDIN SHALL NOT BE LIABLE FOR ANY INDIRECT, 64 | INCIDENTAL, SPECIAL, PUNITIVE, CONSEQUENTIAL OR EXEMPLARY DAMAGES, INCLUDING 65 | BUT NOT LIMITED TO, DAMAGES FOR LOSS OF PROFITS, GOODWILL, USE, DATA OR OTHER 66 | INTANGIBLE LOSSES . IN NO EVENT WILL LINKEDIN'S AGGREGATE LIABILITY TO YOU 67 | EXCEED $100. THIS LIMITATION OF LIABILITY SHALL: 68 | - i. APPLY REGARDLESS OF WHETHER (A) YOU BASE YOUR CLAIM ON CONTRACT, TORT, 69 | STATUTE, OR ANY OTHER LEGAL THEORY, (B) WE KNEW OR SHOULD HAVE KNOWN ABOUT 70 | THE POSSIBILITY OF SUCH DAMAGES, OR (C) THE LIMITED REMEDIES PROVIDED IN THIS 71 | SECTION FAIL OF THEIR ESSENTIAL PURPOSE; AND 72 | - ii. NOT APPLY TO ANY DAMAGE THAT LINKEDIN MAY CAUSE YOU INTENTIONALLY OR 73 | KNOWINGLY IN VIOLATION OF THESE TERMS OR APPLICABLE LAW, OR AS OTHERWISE 74 | MANDATED BY APPLICABLE LAW THAT CANNOT BE DISCLAIMED IN THESE TERMS. 75 | 76 | 6. Termination. This Agreement automatically terminates upon your breach of 77 | this Agreement or termination of your LinkedIn Learning subscription. On 78 | termination, all licenses granted under this Agreement will terminate 79 | immediately and you will delete the Licensed Materials. Sections 2-7 of this 80 | Agreement survive any termination of this Agreement. LinkedIn may discontinue 81 | the availability of some or all of the Licensed Materials at any time for any 82 | reason. 83 | 84 | 7. Miscellaneous. This Agreement will be governed by and construed in 85 | accordance with the laws of the State of California without regard to conflict 86 | of laws principles. The exclusive forum for any disputes arising out of or 87 | relating to this Agreement shall be an appropriate federal or state court 88 | sitting in the County of Santa Clara, State of California. If LinkedIn does 89 | not act to enforce a breach of this Agreement, that does not mean that 90 | LinkedIn has waived its right to enforce this Agreement. The Agreement does 91 | not create a partnership, agency relationship, or joint venture between the 92 | parties. Neither party has the power or authority to bind the other or to 93 | create any obligation or responsibility on behalf of the other. You may not, 94 | without LinkedIn’s prior written consent, assign or delegate any rights or 95 | obligations under these terms, including in connection with a change of 96 | control. Any purported assignment and delegation shall be ineffective. The 97 | Agreement shall bind and inure to the benefit of the parties, their respective 98 | successors and permitted assigns. If any provision of the Agreement is 99 | unenforceable, that provision will be modified to render it enforceable to the 100 | extent possible to give effect to the parties’ intentions and the remaining 101 | provisions will not be affected. This Agreement is the only agreement between 102 | you and LinkedIn regarding the Licensed Materials, and supersedes all prior 103 | agreements relating to the Licensed Materials. 104 | 105 | Last Updated: March 2019 106 | -------------------------------------------------------------------------------- /src/Ch01/01_07/cat_trap_algorithms.py: -------------------------------------------------------------------------------- 1 | """ 2 | 01_07 - The Python setting for the cat trap 3 | 4 | Cat Trap Algorithms 5 | 6 | This is the relevant code for the LinkedIn Learning Course 7 | AI Algorithms for Game Design with Python, by Eduardo Corpeño. 8 | 9 | For the GUI, this code uses the Cat Trap UI VSCode extension 10 | included in the extensions folder. 11 | """ 12 | 13 | import random 14 | import copy 15 | import time 16 | import numpy as np 17 | 18 | # Constants 19 | CAT_TILE = 6 20 | BLOCKED_TILE = 1 21 | EMPTY_TILE = 0 22 | LAST_CALL_MS = 0.5 23 | VERBOSE = True 24 | TIMEOUT = [-1, -1] 25 | 26 | class CatTrapGame: 27 | """ 28 | Represents a Cat Trap game state. Includes methods for managing game state 29 | and selecting moves for the cat using different AI algorithms. 30 | """ 31 | 32 | size = 0 33 | start_time = time.time() 34 | deadline = time.time() 35 | terminated = False 36 | 37 | def __init__(self, size): 38 | self.cat = [size // 2] * 2 39 | self.hexgrid = np.full((size, size), EMPTY_TILE) 40 | self.hexgrid[tuple(self.cat)] = CAT_TILE 41 | CatTrapGame.size = size 42 | 43 | def initialize_random_hexgrid(self): 44 | """Randomly initialize blocked hexgrid.""" 45 | tiles = CatTrapGame.size ** 2 46 | num_blocks = random.randint(round(0.067 * tiles), round(0.13 * tiles)) 47 | count = 0 48 | self.hexgrid[tuple(self.cat)] = CAT_TILE 49 | 50 | while count < num_blocks: 51 | r = random.randint(0, CatTrapGame.size - 1) 52 | c = random.randint(0, CatTrapGame.size - 1) 53 | if self.hexgrid[r, c] == EMPTY_TILE: 54 | self.hexgrid[r, c] = BLOCKED_TILE 55 | count += 1 56 | if VERBOSE: 57 | print('\n======= NEW GAME =======') 58 | self.print_hexgrid() 59 | 60 | def set_hexgrid(self, hexgrid): 61 | """Copy incoming hexgrid.""" 62 | self.hexgrid = hexgrid 63 | self.cat = list(np.argwhere(self.hexgrid == CAT_TILE)[0]) # Find the cat 64 | if VERBOSE: 65 | print('\n======= NEW GAME =======') 66 | self.print_hexgrid() 67 | 68 | def block_tile(self, coord): 69 | self.hexgrid[tuple(coord)] = BLOCKED_TILE 70 | 71 | def unblock_tile(self, coord): 72 | self.hexgrid[tuple(coord)] = EMPTY_TILE 73 | 74 | def place_cat(self, coord): 75 | self.hexgrid[tuple(coord)] = CAT_TILE 76 | self.cat = coord 77 | 78 | def move_cat(self, coord): 79 | self.hexgrid[tuple(self.cat)] = EMPTY_TILE # Clear previous cat position 80 | self.place_cat(coord) 81 | 82 | def get_cat_moves(self): 83 | """ 84 | Get a list of valid moves for the cat. 85 | """ 86 | hexgrid = self.hexgrid 87 | r, c = self.cat 88 | n = CatTrapGame.size 89 | col_offset = r % 2 # Offset for columns based on row parity 90 | moves = [] 91 | 92 | # Directions with column adjustments 93 | directions = { 94 | 'E': (0, 1), 95 | 'W': (0, -1), 96 | 'NE': (-1, col_offset), 97 | 'NW': (-1, -1 + col_offset), 98 | 'SE': (1, col_offset), 99 | 'SW': (1, -1 + col_offset), 100 | } 101 | 102 | for dr, dc in directions.values(): 103 | tr, tc = r + dr, c + dc # Calculate target row and column 104 | if 0 <= tr < n and 0 <= tc < n and hexgrid[tr, tc] == EMPTY_TILE: 105 | moves.append([tr, tc]) 106 | 107 | return moves 108 | 109 | def apply_move(self, move, cat_turn): 110 | """ 111 | Apply a move to the game state. 112 | """ 113 | if self.hexgrid[tuple(move)] != EMPTY_TILE: 114 | action_str = "move cat to" if cat_turn else "block" 115 | self.print_hexgrid() 116 | print('\n=====================================') 117 | print(f'Attempting to {action_str} {move} = {self.hexgrid[tuple(move)]}') 118 | print('Invalid Move! Check your code.') 119 | print('=====================================\n') 120 | 121 | if cat_turn: 122 | self.move_cat(move) 123 | else: 124 | self.hexgrid[tuple(move)] = BLOCKED_TILE 125 | 126 | def time_left(self): 127 | """ 128 | Calculate the time remaining before the deadline. 129 | """ 130 | return (CatTrapGame.deadline - time.time()) * 1000 131 | 132 | def print_hexgrid(self): 133 | """ 134 | Print the current state of the game board using special characters. 135 | """ 136 | tile_map = { 137 | EMPTY_TILE: ' ⬡', # Alternative: '-' 138 | BLOCKED_TILE: ' ⬢', # Alternative: 'X' 139 | CAT_TILE: '🐈' # Alternative: 'C' 140 | } 141 | for r in range(CatTrapGame.size): 142 | # Add a leading space for odd rows for staggered effect 143 | prefix = ' ' if r % 2 != 0 else '' 144 | row_display = ' '.join(tile_map[cell] for cell in self.hexgrid[r]) 145 | print(prefix + row_display) 146 | return 147 | 148 | 149 | # ===================== Intelligent Agents ===================== 150 | """ 151 | Intelligent Agents for the Cat Trap game. These agents examine the game 152 | state, and return the new position of the cat (a move). 153 | Two special return values for failure may be returned (timeout or trapped). 154 | 155 | Parameters: 156 | - random_cat: A random move for the cat. 157 | - minimax: Use the Minimax algorithm. 158 | - alpha_beta: Use Alpha-Beta Pruning. 159 | - depth_limited: Use Depth-Limited Search. 160 | - max_depth: Maximum depth to explore for Depth-Limited Search. 161 | - iterative_deepening: Use Iterative Deepening. 162 | - allotted_time: Maximum time in seconds for the cat to respond. 163 | 164 | If no algorithm is selected, the cat gives up (as if trapped). 165 | """ 166 | 167 | def select_cat_move(self, random_cat, minimax, alpha_beta, depth_limited, 168 | max_depth, iterative_deepening, allotted_time): 169 | """Select a move for the cat based on the chosen algorithm.""" 170 | CatTrapGame.start_time = time.time() 171 | CatTrapGame.deadline = CatTrapGame.start_time + allotted_time 172 | CatTrapGame.terminated = False 173 | move = self.cat 174 | 175 | if VERBOSE: 176 | print('\n======= NEW MOVE =======') 177 | 178 | if random_cat: 179 | move = self.random_cat_move() 180 | elif minimax: 181 | # Select a move using the Minimax algorithm. 182 | move = self.alpha_beta() if alpha_beta else self.minimax() 183 | elif depth_limited: 184 | # Select a move using Depth-Limited Search. 185 | self.placeholder_warning() 186 | move = self.random_cat_move() 187 | elif iterative_deepening: 188 | # Select a move using the Iterative Deepening algorithm. 189 | move = self.iterative_deepening(alpha_beta) 190 | 191 | elapsed_time = (time.time() - CatTrapGame.start_time) * 1000 192 | if VERBOSE: 193 | print(f'Elapsed time: {elapsed_time:.3f}ms ') 194 | print(f'New cat coordinates: {move}') 195 | temp = copy.deepcopy(self) 196 | if move != TIMEOUT: 197 | temp.move_cat(move) 198 | temp.print_hexgrid() 199 | return move 200 | 201 | def random_cat_move(self): 202 | """Randomly select a move for the cat.""" 203 | moves = self.get_cat_moves() 204 | if moves: 205 | return random.choice(moves) 206 | return self.cat 207 | 208 | def max_value(self, depth): 209 | """ 210 | Calculate the maximum value for the current game in the Minimax algorithm. 211 | """ 212 | self.placeholder_warning() 213 | return self.random_cat_move(), 0 214 | 215 | def minimax(self): 216 | """ 217 | Perform the Minimax algorithm to determine the best move. 218 | """ 219 | best_move, _ = self.max_value(depth = 0) 220 | return best_move 221 | 222 | def alpha_beta_max_value(self, alpha, beta, depth): 223 | """ 224 | Calculate the maximum value for the current game state 225 | using Alpha-Beta pruning. 226 | """ 227 | self.placeholder_warning() 228 | return self.random_cat_move(), 0 229 | 230 | def alpha_beta(self): 231 | """ 232 | Perform the Alpha-Beta pruning algorithm to determine the best move. 233 | """ 234 | alpha = float('-inf') 235 | beta = float('inf') 236 | best_move, _ = self.alpha_beta_max_value(alpha, beta, depth = 0) 237 | return best_move 238 | 239 | def iterative_deepening(self, alpha_beta): 240 | """ 241 | Perform iterative deepening search with an option to use Alpha-Beta pruning. 242 | """ 243 | self.placeholder_warning() 244 | return self.random_cat_move() 245 | 246 | def placeholder_warning(self): 247 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 248 | print(f'{signs} {signs}') 249 | print(' WARNING') 250 | print('This is a temporary implementation using') 251 | print("the random algorithm. You're supposed to") 252 | print('write code to solve a challenge.') 253 | print('Did you run the wrong version of main.py?') 254 | print('Double-check its path.') 255 | print(f'{signs} {signs}') 256 | 257 | if __name__ == '__main__': 258 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 259 | print(f'\n{signs} {signs}') 260 | print(' WARNING') 261 | print('You ran cat_trap_algorithms.py') 262 | print('This file contains the AI algorithms') 263 | print('and classes for the intelligent cat.') 264 | print('Did you mean to run main.py?') 265 | print(f'{signs} {signs}\n') 266 | -------------------------------------------------------------------------------- /src/Ch01/01_08/cat_trap_algorithms.py: -------------------------------------------------------------------------------- 1 | """ 2 | 01_08 - Code Example: A random cat 3 | 4 | Go to line 203 for the code! 5 | 6 | Cat Trap Algorithms 7 | 8 | This is the relevant code for the LinkedIn Learning Course 9 | AI Algorithms for Game Design with Python, by Eduardo Corpeño. 10 | 11 | For the GUI, this code uses the Cat Trap UI VSCode extension 12 | included in the extensions folder. 13 | """ 14 | 15 | import random 16 | import copy 17 | import time 18 | import numpy as np 19 | 20 | # Constants 21 | CAT_TILE = 6 22 | BLOCKED_TILE = 1 23 | EMPTY_TILE = 0 24 | LAST_CALL_MS = 0.5 25 | VERBOSE = True 26 | TIMEOUT = [-1, -1] 27 | 28 | class CatTrapGame: 29 | """ 30 | Represents a Cat Trap game state. Includes methods for managing game state 31 | and selecting moves for the cat using different AI algorithms. 32 | """ 33 | 34 | size = 0 35 | start_time = time.time() 36 | deadline = time.time() 37 | terminated = False 38 | 39 | def __init__(self, size): 40 | self.cat = [size // 2] * 2 41 | self.hexgrid = np.full((size, size), EMPTY_TILE) 42 | self.hexgrid[tuple(self.cat)] = CAT_TILE 43 | CatTrapGame.size = size 44 | 45 | def initialize_random_hexgrid(self): 46 | """Randomly initialize blocked hexgrid.""" 47 | tiles = CatTrapGame.size ** 2 48 | num_blocks = random.randint(round(0.067 * tiles), round(0.13 * tiles)) 49 | count = 0 50 | self.hexgrid[tuple(self.cat)] = CAT_TILE 51 | 52 | while count < num_blocks: 53 | r = random.randint(0, CatTrapGame.size - 1) 54 | c = random.randint(0, CatTrapGame.size - 1) 55 | if self.hexgrid[r, c] == EMPTY_TILE: 56 | self.hexgrid[r, c] = BLOCKED_TILE 57 | count += 1 58 | if VERBOSE: 59 | print('\n======= NEW GAME =======') 60 | self.print_hexgrid() 61 | 62 | def set_hexgrid(self, hexgrid): 63 | """Copy incoming hexgrid.""" 64 | self.hexgrid = hexgrid 65 | self.cat = list(np.argwhere(self.hexgrid == CAT_TILE)[0]) # Find the cat 66 | if VERBOSE: 67 | print('\n======= NEW GAME =======') 68 | self.print_hexgrid() 69 | 70 | def block_tile(self, coord): 71 | self.hexgrid[tuple(coord)] = BLOCKED_TILE 72 | 73 | def unblock_tile(self, coord): 74 | self.hexgrid[tuple(coord)] = EMPTY_TILE 75 | 76 | def place_cat(self, coord): 77 | self.hexgrid[tuple(coord)] = CAT_TILE 78 | self.cat = coord 79 | 80 | def move_cat(self, coord): 81 | self.hexgrid[tuple(self.cat)] = EMPTY_TILE # Clear previous cat position 82 | self.place_cat(coord) 83 | 84 | def get_cat_moves(self): 85 | """ 86 | Get a list of valid moves for the cat. 87 | """ 88 | hexgrid = self.hexgrid 89 | r, c = self.cat 90 | n = CatTrapGame.size 91 | col_offset = r % 2 # Offset for columns based on row parity 92 | moves = [] 93 | 94 | # Directions with column adjustments 95 | directions = { 96 | 'E': (0, 1), 97 | 'W': (0, -1), 98 | 'NE': (-1, col_offset), 99 | 'NW': (-1, -1 + col_offset), 100 | 'SE': (1, col_offset), 101 | 'SW': (1, -1 + col_offset), 102 | } 103 | 104 | for dr, dc in directions.values(): 105 | tr, tc = r + dr, c + dc # Calculate target row and column 106 | if 0 <= tr < n and 0 <= tc < n and hexgrid[tr, tc] == EMPTY_TILE: 107 | moves.append([tr, tc]) 108 | 109 | return moves 110 | 111 | def apply_move(self, move, cat_turn): 112 | """ 113 | Apply a move to the game state. 114 | """ 115 | if self.hexgrid[tuple(move)] != EMPTY_TILE: 116 | action_str = "move cat to" if cat_turn else "block" 117 | self.print_hexgrid() 118 | print('\n=====================================') 119 | print(f'Attempting to {action_str} {move} = {self.hexgrid[tuple(move)]}') 120 | print('Invalid Move! Check your code.') 121 | print('=====================================\n') 122 | 123 | if cat_turn: 124 | self.move_cat(move) 125 | else: 126 | self.hexgrid[tuple(move)] = BLOCKED_TILE 127 | 128 | def time_left(self): 129 | """ 130 | Calculate the time remaining before the deadline. 131 | """ 132 | return (CatTrapGame.deadline - time.time()) * 1000 133 | 134 | def print_hexgrid(self): 135 | """ 136 | Print the current state of the game board using special characters. 137 | """ 138 | tile_map = { 139 | EMPTY_TILE: ' ⬡', # Alternative: '-' 140 | BLOCKED_TILE: ' ⬢', # Alternative: 'X' 141 | CAT_TILE: '🐈' # Alternative: 'C' 142 | } 143 | for r in range(CatTrapGame.size): 144 | # Add a leading space for odd rows for staggered effect 145 | prefix = ' ' if r % 2 != 0 else '' 146 | row_display = ' '.join(tile_map[cell] for cell in self.hexgrid[r]) 147 | print(prefix + row_display) 148 | return 149 | 150 | 151 | # ===================== Intelligent Agents ===================== 152 | """ 153 | Intelligent Agents for the Cat Trap game. These agents examine the game 154 | state, and return the new position of the cat (a move). 155 | Two special return values for failure may be returned (timeout or trapped). 156 | 157 | Parameters: 158 | - random_cat: A random move for the cat. 159 | - minimax: Use the Minimax algorithm. 160 | - alpha_beta: Use Alpha-Beta Pruning. 161 | - depth_limited: Use Depth-Limited Search. 162 | - max_depth: Maximum depth to explore for Depth-Limited Search. 163 | - iterative_deepening: Use Iterative Deepening. 164 | - allotted_time: Maximum time in seconds for the cat to respond. 165 | 166 | If no algorithm is selected, the cat gives up (as if trapped). 167 | """ 168 | 169 | def select_cat_move(self, random_cat, minimax, alpha_beta, depth_limited, 170 | max_depth, iterative_deepening, allotted_time): 171 | """Select a move for the cat based on the chosen algorithm.""" 172 | CatTrapGame.start_time = time.time() 173 | CatTrapGame.deadline = CatTrapGame.start_time + allotted_time 174 | CatTrapGame.terminated = False 175 | move = self.cat 176 | 177 | if VERBOSE: 178 | print('\n======= NEW MOVE =======') 179 | 180 | if random_cat: 181 | move = self.random_cat_move() 182 | elif minimax: 183 | # Select a move using the Minimax algorithm. 184 | move = self.alpha_beta() if alpha_beta else self.minimax() 185 | elif depth_limited: 186 | # Select a move using Depth-Limited Search. 187 | self.placeholder_warning() 188 | move = self.random_cat_move() 189 | elif iterative_deepening: 190 | # Select a move using the Iterative Deepening algorithm. 191 | move = self.iterative_deepening(alpha_beta) 192 | 193 | elapsed_time = (time.time() - CatTrapGame.start_time) * 1000 194 | if VERBOSE: 195 | print(f'Elapsed time: {elapsed_time:.3f}ms ') 196 | print(f'New cat coordinates: {move}') 197 | temp = copy.deepcopy(self) 198 | if move != TIMEOUT: 199 | temp.move_cat(move) 200 | temp.print_hexgrid() 201 | return move 202 | 203 | def random_cat_move(self): 204 | """Randomly select a move for the cat.""" 205 | moves = self.get_cat_moves() 206 | if moves: 207 | return random.choice(moves) 208 | return self.cat 209 | 210 | def max_value(self, depth): 211 | """ 212 | Calculate the maximum value for the current game in the Minimax algorithm. 213 | """ 214 | self.placeholder_warning() 215 | return self.random_cat_move(), 0 216 | 217 | def minimax(self): 218 | """ 219 | Perform the Minimax algorithm to determine the best move. 220 | """ 221 | best_move, _ = self.max_value(depth = 0) 222 | return best_move 223 | 224 | def alpha_beta_max_value(self, alpha, beta, depth): 225 | """ 226 | Calculate the maximum value for the current game state 227 | using Alpha-Beta pruning. 228 | """ 229 | self.placeholder_warning() 230 | return self.random_cat_move(), 0 231 | 232 | def alpha_beta(self): 233 | """ 234 | Perform the Alpha-Beta pruning algorithm to determine the best move. 235 | """ 236 | alpha = float('-inf') 237 | beta = float('inf') 238 | best_move, _ = self.alpha_beta_max_value(alpha, beta, depth = 0) 239 | return best_move 240 | 241 | def iterative_deepening(self, alpha_beta): 242 | """ 243 | Perform iterative deepening search with an option to use Alpha-Beta pruning. 244 | """ 245 | self.placeholder_warning() 246 | return self.random_cat_move() 247 | 248 | def placeholder_warning(self): 249 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 250 | print(f'{signs} {signs}') 251 | print(' WARNING') 252 | print('This is a temporary implementation using') 253 | print("the random algorithm. You're supposed to") 254 | print('write code to solve a challenge.') 255 | print('Did you run the wrong version of main.py?') 256 | print('Double-check its path.') 257 | print(f'{signs} {signs}') 258 | 259 | if __name__ == '__main__': 260 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 261 | print(f'\n{signs} {signs}') 262 | print(' WARNING') 263 | print('You ran cat_trap_algorithms.py') 264 | print('This file contains the AI algorithms') 265 | print('and classes for the intelligent cat.') 266 | print('Did you mean to run main.py?') 267 | print(f'{signs} {signs}\n') 268 | -------------------------------------------------------------------------------- /src/Ch02/02_06/cat_trap_algorithms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cat Trap Algorithms 3 | 4 | This is the relevant code for the LinkedIn Learning Course 5 | AI Algorithms for Game Design with Python, by Eduardo Corpeño. 6 | 7 | For the GUI, this code uses the Cat Trap UI VSCode extension 8 | included in the extensions folder. 9 | """ 10 | 11 | import random 12 | import copy 13 | import time 14 | import numpy as np 15 | 16 | # Constants 17 | CAT_TILE = 6 18 | BLOCKED_TILE = 1 19 | EMPTY_TILE = 0 20 | LAST_CALL_MS = 0.5 21 | VERBOSE = True 22 | TIMEOUT = [-1, -1] 23 | 24 | class CatTrapGame: 25 | """ 26 | Represents a Cat Trap game state. Includes methods for managing game state 27 | and selecting moves for the cat using different AI algorithms. 28 | """ 29 | 30 | size = 0 31 | start_time = time.time() 32 | deadline = time.time() 33 | terminated = False 34 | 35 | def __init__(self, size): 36 | self.cat = [size // 2] * 2 37 | self.hexgrid = np.full((size, size), EMPTY_TILE) 38 | self.hexgrid[tuple(self.cat)] = CAT_TILE 39 | CatTrapGame.size = size 40 | 41 | def initialize_random_hexgrid(self): 42 | """Randomly initialize blocked hexgrid.""" 43 | tiles = CatTrapGame.size ** 2 44 | num_blocks = random.randint(round(0.067 * tiles), round(0.13 * tiles)) 45 | count = 0 46 | self.hexgrid[tuple(self.cat)] = CAT_TILE 47 | 48 | while count < num_blocks: 49 | r = random.randint(0, CatTrapGame.size - 1) 50 | c = random.randint(0, CatTrapGame.size - 1) 51 | if self.hexgrid[r, c] == EMPTY_TILE: 52 | self.hexgrid[r, c] = BLOCKED_TILE 53 | count += 1 54 | if VERBOSE: 55 | print('\n======= NEW GAME =======') 56 | self.print_hexgrid() 57 | 58 | def set_hexgrid(self, hexgrid): 59 | """Copy incoming hexgrid.""" 60 | self.hexgrid = hexgrid 61 | self.cat = list(np.argwhere(self.hexgrid == CAT_TILE)[0]) # Find the cat 62 | if VERBOSE: 63 | print('\n======= NEW GAME =======') 64 | self.print_hexgrid() 65 | 66 | def block_tile(self, coord): 67 | self.hexgrid[tuple(coord)] = BLOCKED_TILE 68 | 69 | def unblock_tile(self, coord): 70 | self.hexgrid[tuple(coord)] = EMPTY_TILE 71 | 72 | def place_cat(self, coord): 73 | self.hexgrid[tuple(coord)] = CAT_TILE 74 | self.cat = coord 75 | 76 | def move_cat(self, coord): 77 | self.hexgrid[tuple(self.cat)] = EMPTY_TILE # Clear previous cat position 78 | self.place_cat(coord) 79 | 80 | def get_cat_moves(self): 81 | """ 82 | Get a list of valid moves for the cat. 83 | """ 84 | hexgrid = self.hexgrid 85 | r, c = self.cat 86 | n = CatTrapGame.size 87 | col_offset = r % 2 # Offset for columns based on row parity 88 | moves = [] 89 | 90 | # Directions with column adjustments 91 | directions = { 92 | 'E': (0, 1), 93 | 'W': (0, -1), 94 | 'NE': (-1, col_offset), 95 | 'NW': (-1, -1 + col_offset), 96 | 'SE': (1, col_offset), 97 | 'SW': (1, -1 + col_offset), 98 | } 99 | 100 | for dr, dc in directions.values(): 101 | tr, tc = r + dr, c + dc # Calculate target row and column 102 | if 0 <= tr < n and 0 <= tc < n and hexgrid[tr, tc] == EMPTY_TILE: 103 | moves.append([tr, tc]) 104 | 105 | return moves 106 | 107 | def apply_move(self, move, cat_turn): 108 | """ 109 | Apply a move to the game state. 110 | """ 111 | if self.hexgrid[tuple(move)] != EMPTY_TILE: 112 | action_str = "move cat to" if cat_turn else "block" 113 | self.print_hexgrid() 114 | print('\n=====================================') 115 | print(f'Attempting to {action_str} {move} = {self.hexgrid[tuple(move)]}') 116 | print('Invalid Move! Check your code.') 117 | print('=====================================\n') 118 | 119 | if cat_turn: 120 | self.move_cat(move) 121 | else: 122 | self.hexgrid[tuple(move)] = BLOCKED_TILE 123 | 124 | def time_left(self): 125 | """ 126 | Calculate the time remaining before the deadline. 127 | """ 128 | return (CatTrapGame.deadline - time.time()) * 1000 129 | 130 | def print_hexgrid(self): 131 | """ 132 | Print the current state of the game board using special characters. 133 | """ 134 | tile_map = { 135 | EMPTY_TILE: ' ⬡', # Alternative: '-' 136 | BLOCKED_TILE: ' ⬢', # Alternative: 'X' 137 | CAT_TILE: '🐈' # Alternative: 'C' 138 | } 139 | for r in range(CatTrapGame.size): 140 | # Add a leading space for odd rows for staggered effect 141 | prefix = ' ' if r % 2 != 0 else '' 142 | row_display = ' '.join(tile_map[cell] for cell in self.hexgrid[r]) 143 | print(prefix + row_display) 144 | return 145 | 146 | 147 | # ===================== Intelligent Agents ===================== 148 | """ 149 | Intelligent Agents for the Cat Trap game. These agents examine the game 150 | state, and return the new position of the cat (a move). 151 | Two special return values for failure may be returned (timeout or trapped). 152 | 153 | Parameters: 154 | - random_cat: A random move for the cat. 155 | - minimax: Use the Minimax algorithm. 156 | - alpha_beta: Use Alpha-Beta Pruning. 157 | - depth_limited: Use Depth-Limited Search. 158 | - max_depth: Maximum depth to explore for Depth-Limited Search. 159 | - iterative_deepening: Use Iterative Deepening. 160 | - allotted_time: Maximum time in seconds for the cat to respond. 161 | 162 | If no algorithm is selected, the cat gives up (as if trapped). 163 | """ 164 | 165 | def select_cat_move(self, random_cat, minimax, alpha_beta, depth_limited, 166 | max_depth, iterative_deepening, allotted_time): 167 | """Select a move for the cat based on the chosen algorithm.""" 168 | CatTrapGame.start_time = time.time() 169 | CatTrapGame.deadline = CatTrapGame.start_time + allotted_time 170 | CatTrapGame.terminated = False 171 | move = self.cat 172 | 173 | if VERBOSE: 174 | print('\n======= NEW MOVE =======') 175 | 176 | if random_cat: 177 | move = self.random_cat_move() 178 | elif minimax: 179 | # Select a move using the Minimax algorithm. 180 | move = self.alpha_beta() if alpha_beta else self.minimax() 181 | elif depth_limited: 182 | # Select a move using Depth-Limited Search. 183 | self.placeholder_warning() 184 | move = self.random_cat_move() 185 | elif iterative_deepening: 186 | # Select a move using the Iterative Deepening algorithm. 187 | move = self.iterative_deepening(alpha_beta) 188 | 189 | elapsed_time = (time.time() - CatTrapGame.start_time) * 1000 190 | if VERBOSE: 191 | print(f'Elapsed time: {elapsed_time:.3f}ms ') 192 | print(f'New cat coordinates: {move}') 193 | temp = copy.deepcopy(self) 194 | if move != TIMEOUT: 195 | temp.move_cat(move) 196 | temp.print_hexgrid() 197 | return move 198 | 199 | def random_cat_move(self): 200 | """Randomly select a move for the cat.""" 201 | moves = self.get_cat_moves() 202 | if moves: 203 | return random.choice(moves) 204 | return self.cat 205 | 206 | def max_value(self, depth): 207 | """ 208 | Calculate the maximum value for the current game in the Minimax algorithm. 209 | """ 210 | if self.time_left() < LAST_CALL_MS: 211 | CatTrapGame.terminated = True 212 | return TIMEOUT, 0 213 | 214 | # Check if terminal state 215 | legal_moves = self.get_cat_moves() # Possible directions: E, W, NE, NW, SE, SW 216 | if not legal_moves: 217 | max_turns = 2 * (CatTrapGame.size ** 2) 218 | utility = (max_turns - depth) * (-500) # Utility for cat's defeat 219 | return self.cat, utility 220 | 221 | best_value = float('-inf') 222 | best_move = legal_moves[0] 223 | for move in legal_moves: 224 | next_game = copy.deepcopy(self) 225 | next_game.apply_move(move, cat_turn = True) 226 | value = next_game.min_value(depth + 1) 227 | 228 | if CatTrapGame.terminated: 229 | return TIMEOUT, 0 230 | 231 | if value > best_value: 232 | best_value = value 233 | best_move = move 234 | 235 | return best_move, best_value 236 | 237 | def min_value(self, depth): 238 | """ 239 | Calculate the minimum value for the current game in the Minimax algorithm. 240 | 241 | Unlike max_value, min_value does not iterate over specific 242 | directions (E, W, NE, NW, etc.). Instead, it examines every 243 | possible free tile on the board. 244 | """ 245 | if self.time_left() < LAST_CALL_MS: 246 | CatTrapGame.terminated = True 247 | return 0 248 | 249 | # Check if terminal state 250 | r, c = self.cat 251 | n = CatTrapGame.size 252 | if ( 253 | r == 0 or r == n - 1 or 254 | c == 0 or c == n - 1 255 | ): 256 | max_turns = 2 * (CatTrapGame.size ** 2) 257 | return (max_turns - depth) * (500) # Utility for cat's victory 258 | 259 | best_value = float('inf') 260 | 261 | # Iterate through all legal moves for the player (empty tiles) 262 | legal_moves = [list(rc) for rc in np.argwhere(self.hexgrid == EMPTY_TILE)] 263 | for move in legal_moves: 264 | next_game = copy.deepcopy(self) 265 | next_game.apply_move(move, cat_turn = False) 266 | _, value = next_game.max_value(depth + 1) 267 | 268 | if CatTrapGame.terminated: 269 | return 0 270 | 271 | best_value = min(best_value, value) 272 | 273 | return best_value 274 | 275 | def minimax(self): 276 | """ 277 | Perform the Minimax algorithm to determine the best move. 278 | """ 279 | best_move, _ = self.max_value(depth = 0) 280 | return best_move 281 | 282 | def alpha_beta_max_value(self, alpha, beta, depth): 283 | """ 284 | Calculate the maximum value for the current game state 285 | using Alpha-Beta pruning. 286 | """ 287 | self.placeholder_warning() 288 | return self.random_cat_move(), 0 289 | 290 | def alpha_beta(self): 291 | """ 292 | Perform the Alpha-Beta pruning algorithm to determine the best move. 293 | """ 294 | alpha = float('-inf') 295 | beta = float('inf') 296 | best_move, _ = self.alpha_beta_max_value(alpha, beta, depth = 0) 297 | return best_move 298 | 299 | def iterative_deepening(self, alpha_beta): 300 | """ 301 | Perform iterative deepening search with an option to use Alpha-Beta pruning. 302 | """ 303 | self.placeholder_warning() 304 | return self.random_cat_move() 305 | 306 | def placeholder_warning(self): 307 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 308 | print(f'{signs} {signs}') 309 | print(' WARNING') 310 | print('This is a temporary implementation using') 311 | print("the random algorithm. You're supposed to") 312 | print('write code to solve a challenge.') 313 | print('Did you run the wrong version of main.py?') 314 | print('Double-check its path.') 315 | print(f'{signs} {signs}') 316 | 317 | if __name__ == '__main__': 318 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 319 | print(f'\n{signs} {signs}') 320 | print(' WARNING') 321 | print('You ran cat_trap_algorithms.py') 322 | print('This file contains the AI algorithms') 323 | print('and classes for the intelligent cat.') 324 | print('Did you mean to run main.py?') 325 | print(f'{signs} {signs}\n') 326 | -------------------------------------------------------------------------------- /src/Ch02/02_05/cat_trap_algorithms.py: -------------------------------------------------------------------------------- 1 | """ 2 | 02_05 - Challenge: A perfect cat in a small world 3 | 4 | Go to line 210 for the challenge! 5 | 6 | Cat Trap Algorithms 7 | 8 | This is the relevant code for the LinkedIn Learning Course 9 | AI Algorithms for Game Design with Python, by Eduardo Corpeño. 10 | 11 | For the GUI, this code uses the Cat Trap UI VSCode extension 12 | included in the extensions folder. 13 | """ 14 | 15 | import random 16 | import copy 17 | import time 18 | import numpy as np 19 | 20 | # Constants 21 | CAT_TILE = 6 22 | BLOCKED_TILE = 1 23 | EMPTY_TILE = 0 24 | LAST_CALL_MS = 0.5 25 | VERBOSE = True 26 | TIMEOUT = [-1, -1] 27 | 28 | class CatTrapGame: 29 | """ 30 | Represents a Cat Trap game state. Includes methods for managing game state 31 | and selecting moves for the cat using different AI algorithms. 32 | """ 33 | 34 | size = 0 35 | start_time = time.time() 36 | deadline = time.time() 37 | terminated = False 38 | 39 | def __init__(self, size): 40 | self.cat = [size // 2] * 2 41 | self.hexgrid = np.full((size, size), EMPTY_TILE) 42 | self.hexgrid[tuple(self.cat)] = CAT_TILE 43 | CatTrapGame.size = size 44 | 45 | def initialize_random_hexgrid(self): 46 | """Randomly initialize blocked hexgrid.""" 47 | tiles = CatTrapGame.size ** 2 48 | num_blocks = random.randint(round(0.067 * tiles), round(0.13 * tiles)) 49 | count = 0 50 | self.hexgrid[tuple(self.cat)] = CAT_TILE 51 | 52 | while count < num_blocks: 53 | r = random.randint(0, CatTrapGame.size - 1) 54 | c = random.randint(0, CatTrapGame.size - 1) 55 | if self.hexgrid[r, c] == EMPTY_TILE: 56 | self.hexgrid[r, c] = BLOCKED_TILE 57 | count += 1 58 | if VERBOSE: 59 | print('\n======= NEW GAME =======') 60 | self.print_hexgrid() 61 | 62 | def set_hexgrid(self, hexgrid): 63 | """Copy incoming hexgrid.""" 64 | self.hexgrid = hexgrid 65 | self.cat = list(np.argwhere(self.hexgrid == CAT_TILE)[0]) # Find the cat 66 | if VERBOSE: 67 | print('\n======= NEW GAME =======') 68 | self.print_hexgrid() 69 | 70 | def block_tile(self, coord): 71 | self.hexgrid[tuple(coord)] = BLOCKED_TILE 72 | 73 | def unblock_tile(self, coord): 74 | self.hexgrid[tuple(coord)] = EMPTY_TILE 75 | 76 | def place_cat(self, coord): 77 | self.hexgrid[tuple(coord)] = CAT_TILE 78 | self.cat = coord 79 | 80 | def move_cat(self, coord): 81 | self.hexgrid[tuple(self.cat)] = EMPTY_TILE # Clear previous cat position 82 | self.place_cat(coord) 83 | 84 | def get_cat_moves(self): 85 | """ 86 | Get a list of valid moves for the cat. 87 | """ 88 | hexgrid = self.hexgrid 89 | r, c = self.cat 90 | n = CatTrapGame.size 91 | col_offset = r % 2 # Offset for columns based on row parity 92 | moves = [] 93 | 94 | # Directions with column adjustments 95 | directions = { 96 | 'E': (0, 1), 97 | 'W': (0, -1), 98 | 'NE': (-1, col_offset), 99 | 'NW': (-1, -1 + col_offset), 100 | 'SE': (1, col_offset), 101 | 'SW': (1, -1 + col_offset), 102 | } 103 | 104 | for dr, dc in directions.values(): 105 | tr, tc = r + dr, c + dc # Calculate target row and column 106 | if 0 <= tr < n and 0 <= tc < n and hexgrid[tr, tc] == EMPTY_TILE: 107 | moves.append([tr, tc]) 108 | 109 | return moves 110 | 111 | def apply_move(self, move, cat_turn): 112 | """ 113 | Apply a move to the game state. 114 | """ 115 | if self.hexgrid[tuple(move)] != EMPTY_TILE: 116 | action_str = "move cat to" if cat_turn else "block" 117 | self.print_hexgrid() 118 | print('\n=====================================') 119 | print(f'Attempting to {action_str} {move} = {self.hexgrid[tuple(move)]}') 120 | print('Invalid Move! Check your code.') 121 | print('=====================================\n') 122 | 123 | if cat_turn: 124 | self.move_cat(move) 125 | else: 126 | self.hexgrid[tuple(move)] = BLOCKED_TILE 127 | 128 | def time_left(self): 129 | """ 130 | Calculate the time remaining before the deadline. 131 | """ 132 | return (CatTrapGame.deadline - time.time()) * 1000 133 | 134 | def print_hexgrid(self): 135 | """ 136 | Print the current state of the game board using special characters. 137 | """ 138 | tile_map = { 139 | EMPTY_TILE: ' ⬡', # Alternative: '-' 140 | BLOCKED_TILE: ' ⬢', # Alternative: 'X' 141 | CAT_TILE: '🐈' # Alternative: 'C' 142 | } 143 | for r in range(CatTrapGame.size): 144 | # Add a leading space for odd rows for staggered effect 145 | prefix = ' ' if r % 2 != 0 else '' 146 | row_display = ' '.join(tile_map[cell] for cell in self.hexgrid[r]) 147 | print(prefix + row_display) 148 | return 149 | 150 | 151 | # ===================== Intelligent Agents ===================== 152 | """ 153 | Intelligent Agents for the Cat Trap game. These agents examine the game 154 | state, and return the new position of the cat (a move). 155 | Two special return values for failure may be returned (timeout or trapped). 156 | 157 | Parameters: 158 | - random_cat: A random move for the cat. 159 | - minimax: Use the Minimax algorithm. 160 | - alpha_beta: Use Alpha-Beta Pruning. 161 | - depth_limited: Use Depth-Limited Search. 162 | - max_depth: Maximum depth to explore for Depth-Limited Search. 163 | - iterative_deepening: Use Iterative Deepening. 164 | - allotted_time: Maximum time in seconds for the cat to respond. 165 | 166 | If no algorithm is selected, the cat gives up (as if trapped). 167 | """ 168 | 169 | def select_cat_move(self, random_cat, minimax, alpha_beta, depth_limited, 170 | max_depth, iterative_deepening, allotted_time): 171 | """Select a move for the cat based on the chosen algorithm.""" 172 | CatTrapGame.start_time = time.time() 173 | CatTrapGame.deadline = CatTrapGame.start_time + allotted_time 174 | CatTrapGame.terminated = False 175 | move = self.cat 176 | 177 | if VERBOSE: 178 | print('\n======= NEW MOVE =======') 179 | 180 | if random_cat: 181 | move = self.random_cat_move() 182 | elif minimax: 183 | # Select a move using the Minimax algorithm. 184 | move = self.alpha_beta() if alpha_beta else self.minimax() 185 | elif depth_limited: 186 | # Select a move using Depth-Limited Search. 187 | self.placeholder_warning() 188 | move = self.random_cat_move() 189 | elif iterative_deepening: 190 | # Select a move using the Iterative Deepening algorithm. 191 | move = self.iterative_deepening(alpha_beta) 192 | 193 | elapsed_time = (time.time() - CatTrapGame.start_time) * 1000 194 | if VERBOSE: 195 | print(f'Elapsed time: {elapsed_time:.3f}ms ') 196 | print(f'New cat coordinates: {move}') 197 | temp = copy.deepcopy(self) 198 | if move != TIMEOUT: 199 | temp.move_cat(move) 200 | temp.print_hexgrid() 201 | return move 202 | 203 | def random_cat_move(self): 204 | """Randomly select a move for the cat.""" 205 | moves = self.get_cat_moves() 206 | if moves: 207 | return random.choice(moves) 208 | return self.cat 209 | 210 | def max_value(self, depth): 211 | """ 212 | Calculate the maximum value for the current game in the Minimax algorithm. 213 | 214 | 02_05 - Challenge: A perfect cat in a small world 215 | 216 | Your task is to implement the minimax algorithm. 217 | You will do this by adding code to max_value() and min_value(). 218 | 219 | Make sure to take care of the following considerations: 220 | 1) Remove the placeholder code immediately below these instructions. 221 | 2) Read through the skeleton code provided below for both methods. 222 | 3) Fill in the blanks following the instructions in the "TODO:" comments. 223 | 4) If you're stuck, you may ask in the course's Q&A or consult the 224 | solution in the next folder to unblock yourself without spoiling too 225 | much of the fun. 226 | """ 227 | # TODO: Remove the following 2 lines to enable your minimax implementation. 228 | self.placeholder_warning() 229 | return self.random_cat_move(), 0 230 | 231 | # Skeleton Code - Minimax 232 | # HINT: There are 6 "TODO:" comments below. 233 | 234 | if self.time_left() < LAST_CALL_MS: 235 | CatTrapGame.terminated = True 236 | return TIMEOUT, 0 237 | 238 | # Check if terminal state 239 | legal_moves = self.get_cat_moves() # Possible directions: E, W, NE, NW, SE, SW 240 | 241 | # TODO: Complete the code for the first step of the algorithm: 242 | # if Terminal(state) then return Utility(state) 243 | # HINT: To determine if this is a terminal state, look at legal_moves. 244 | if True: # Replace with the condition for a terminal state. 245 | max_turns = 2 * (CatTrapGame.size ** 2) 246 | utility = (max_turns - depth) * (-500) # Utility for cat's defeat 247 | return self.cat, utility 248 | 249 | best_value = float('-inf') 250 | best_move = legal_moves[0] 251 | for move in legal_moves: 252 | next_game = copy.deepcopy(self) 253 | # TODO: Apply the current move to next_game. 254 | # HINT: Remember this is a max-node, so it's the cat's turn. 255 | pass # Replace with a call to next_game.apply_move() 256 | 257 | # TODO: Calculate the min-node value for this move. 258 | # HINT: Call next_game.min_value(). 259 | # HINT: Don't forget to send the right depth to min_value()! 260 | value = 0 # Replace with a call to next_game.min_value() 261 | 262 | if CatTrapGame.terminated: 263 | return TIMEOUT, 0 264 | 265 | # Updating the best move and value to return 266 | if value > best_value: 267 | best_value = value 268 | best_move = move 269 | 270 | return best_move, best_value 271 | 272 | def min_value(self, depth): 273 | """ 274 | Calculate the minimum value for the current game in the Minimax algorithm. 275 | 276 | Unlike max_value, min_value does not iterate over specific 277 | directions (E, W, NE, NW, etc.). Instead, it examines every 278 | possible free tile on the board. 279 | """ 280 | if self.time_left() < LAST_CALL_MS: 281 | CatTrapGame.terminated = True 282 | return 0 283 | 284 | # TODO: Complete the code for the first step of the algorithm: 285 | # if Terminal(state) then return Utility(state) 286 | # HINT: To determine if this is a terminal state, check if the cat 287 | # is at an edge tile. 288 | r, c = self.cat 289 | n = CatTrapGame.size 290 | if False: # Replace with the condition for a terminal state. 291 | max_turns = 2 * (CatTrapGame.size ** 2) 292 | return (max_turns - depth) * (500) # Utility for cat's victory 293 | 294 | best_value = float('inf') 295 | 296 | # Iterate through all legal moves for the player (empty tiles) 297 | legal_moves = [list(rc) for rc in np.argwhere(self.hexgrid == EMPTY_TILE)] 298 | for move in legal_moves: 299 | next_game = copy.deepcopy(self) 300 | # TODO: Apply the current move to next_game. 301 | # HINT: Remember this is a min-node, so it's the human player's turn. 302 | pass # Replace with a call to next_game.apply_move() 303 | 304 | # TODO: Calculate the max-node value for this move. 305 | # HINT: Call next_game.max_value(). 306 | # HINT: Don't forget to send the right depth to max_value()! 307 | _, value = [0,0], 0 # Replace with a call to next_game.max_value() 308 | 309 | if CatTrapGame.terminated: 310 | return 0 311 | 312 | # Updating the best value to return 313 | best_value = min(best_value, value) 314 | 315 | return best_value 316 | 317 | def minimax(self): 318 | """ 319 | Perform the Minimax algorithm to determine the best move. 320 | """ 321 | best_move, _ = self.max_value(depth = 0) 322 | return best_move 323 | 324 | def alpha_beta_max_value(self, alpha, beta, depth): 325 | """ 326 | Calculate the maximum value for the current game state 327 | using Alpha-Beta pruning. 328 | """ 329 | self.placeholder_warning() 330 | return self.random_cat_move(), 0 331 | 332 | def alpha_beta(self): 333 | """ 334 | Perform the Alpha-Beta pruning algorithm to determine the best move. 335 | """ 336 | alpha = float('-inf') 337 | beta = float('inf') 338 | best_move, _ = self.alpha_beta_max_value(alpha, beta, depth = 0) 339 | return best_move 340 | 341 | def iterative_deepening(self, alpha_beta): 342 | """ 343 | Perform iterative deepening search with an option to use Alpha-Beta pruning. 344 | """ 345 | self.placeholder_warning() 346 | return self.random_cat_move() 347 | 348 | def placeholder_warning(self): 349 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 350 | print(f'{signs} {signs}') 351 | print(' WARNING') 352 | print('This is a temporary implementation using') 353 | print("the random algorithm. You're supposed to") 354 | print('write code to solve a challenge.') 355 | print('Did you run the wrong version of main.py?') 356 | print('Double-check its path.') 357 | print(f'{signs} {signs}') 358 | 359 | if __name__ == '__main__': 360 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 361 | print(f'\n{signs} {signs}') 362 | print(' WARNING') 363 | print('You ran cat_trap_algorithms.py') 364 | print('This file contains the AI algorithms') 365 | print('and classes for the intelligent cat.') 366 | print('Did you mean to run main.py?') 367 | print(f'{signs} {signs}\n') 368 | -------------------------------------------------------------------------------- /src/Ch02/02_10/cat_trap_algorithms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cat Trap Algorithms 3 | 4 | This is the relevant code for the LinkedIn Learning Course 5 | AI Algorithms for Game Design with Python, by Eduardo Corpeño. 6 | 7 | For the GUI, this code uses the Cat Trap UI VSCode extension 8 | included in the extensions folder. 9 | """ 10 | 11 | import random 12 | import copy 13 | import time 14 | import numpy as np 15 | 16 | # Constants 17 | CAT_TILE = 6 18 | BLOCKED_TILE = 1 19 | EMPTY_TILE = 0 20 | LAST_CALL_MS = 0.5 21 | VERBOSE = True 22 | TIMEOUT = [-1, -1] 23 | 24 | class CatTrapGame: 25 | """ 26 | Represents a Cat Trap game state. Includes methods for managing game state 27 | and selecting moves for the cat using different AI algorithms. 28 | """ 29 | 30 | size = 0 31 | start_time = time.time() 32 | deadline = time.time() 33 | terminated = False 34 | 35 | def __init__(self, size): 36 | self.cat = [size // 2] * 2 37 | self.hexgrid = np.full((size, size), EMPTY_TILE) 38 | self.hexgrid[tuple(self.cat)] = CAT_TILE 39 | CatTrapGame.size = size 40 | 41 | def initialize_random_hexgrid(self): 42 | """Randomly initialize blocked hexgrid.""" 43 | tiles = CatTrapGame.size ** 2 44 | num_blocks = random.randint(round(0.067 * tiles), round(0.13 * tiles)) 45 | count = 0 46 | self.hexgrid[tuple(self.cat)] = CAT_TILE 47 | 48 | while count < num_blocks: 49 | r = random.randint(0, CatTrapGame.size - 1) 50 | c = random.randint(0, CatTrapGame.size - 1) 51 | if self.hexgrid[r, c] == EMPTY_TILE: 52 | self.hexgrid[r, c] = BLOCKED_TILE 53 | count += 1 54 | if VERBOSE: 55 | print('\n======= NEW GAME =======') 56 | self.print_hexgrid() 57 | 58 | def set_hexgrid(self, hexgrid): 59 | """Copy incoming hexgrid.""" 60 | self.hexgrid = hexgrid 61 | self.cat = list(np.argwhere(self.hexgrid == CAT_TILE)[0]) # Find the cat 62 | if VERBOSE: 63 | print('\n======= NEW GAME =======') 64 | self.print_hexgrid() 65 | 66 | def block_tile(self, coord): 67 | self.hexgrid[tuple(coord)] = BLOCKED_TILE 68 | 69 | def unblock_tile(self, coord): 70 | self.hexgrid[tuple(coord)] = EMPTY_TILE 71 | 72 | def place_cat(self, coord): 73 | self.hexgrid[tuple(coord)] = CAT_TILE 74 | self.cat = coord 75 | 76 | def move_cat(self, coord): 77 | self.hexgrid[tuple(self.cat)] = EMPTY_TILE # Clear previous cat position 78 | self.place_cat(coord) 79 | 80 | def get_cat_moves(self): 81 | """ 82 | Get a list of valid moves for the cat. 83 | """ 84 | hexgrid = self.hexgrid 85 | r, c = self.cat 86 | n = CatTrapGame.size 87 | col_offset = r % 2 # Offset for columns based on row parity 88 | moves = [] 89 | 90 | # Directions with column adjustments 91 | directions = { 92 | 'E': (0, 1), 93 | 'W': (0, -1), 94 | 'NE': (-1, col_offset), 95 | 'NW': (-1, -1 + col_offset), 96 | 'SE': (1, col_offset), 97 | 'SW': (1, -1 + col_offset), 98 | } 99 | 100 | for dr, dc in directions.values(): 101 | tr, tc = r + dr, c + dc # Calculate target row and column 102 | if 0 <= tr < n and 0 <= tc < n and hexgrid[tr, tc] == EMPTY_TILE: 103 | moves.append([tr, tc]) 104 | 105 | return moves 106 | 107 | def apply_move(self, move, cat_turn): 108 | """ 109 | Apply a move to the game state. 110 | """ 111 | if self.hexgrid[tuple(move)] != EMPTY_TILE: 112 | action_str = "move cat to" if cat_turn else "block" 113 | self.print_hexgrid() 114 | print('\n=====================================') 115 | print(f'Attempting to {action_str} {move} = {self.hexgrid[tuple(move)]}') 116 | print('Invalid Move! Check your code.') 117 | print('=====================================\n') 118 | 119 | if cat_turn: 120 | self.move_cat(move) 121 | else: 122 | self.hexgrid[tuple(move)] = BLOCKED_TILE 123 | 124 | def time_left(self): 125 | """ 126 | Calculate the time remaining before the deadline. 127 | """ 128 | return (CatTrapGame.deadline - time.time()) * 1000 129 | 130 | def print_hexgrid(self): 131 | """ 132 | Print the current state of the game board using special characters. 133 | """ 134 | tile_map = { 135 | EMPTY_TILE: ' ⬡', # Alternative: '-' 136 | BLOCKED_TILE: ' ⬢', # Alternative: 'X' 137 | CAT_TILE: '🐈' # Alternative: 'C' 138 | } 139 | for r in range(CatTrapGame.size): 140 | # Add a leading space for odd rows for staggered effect 141 | prefix = ' ' if r % 2 != 0 else '' 142 | row_display = ' '.join(tile_map[cell] for cell in self.hexgrid[r]) 143 | print(prefix + row_display) 144 | return 145 | 146 | 147 | # ===================== Intelligent Agents ===================== 148 | """ 149 | Intelligent Agents for the Cat Trap game. These agents examine the game 150 | state, and return the new position of the cat (a move). 151 | Two special return values for failure may be returned (timeout or trapped). 152 | 153 | Parameters: 154 | - random_cat: A random move for the cat. 155 | - minimax: Use the Minimax algorithm. 156 | - alpha_beta: Use Alpha-Beta Pruning. 157 | - depth_limited: Use Depth-Limited Search. 158 | - max_depth: Maximum depth to explore for Depth-Limited Search. 159 | - iterative_deepening: Use Iterative Deepening. 160 | - allotted_time: Maximum time in seconds for the cat to respond. 161 | 162 | If no algorithm is selected, the cat gives up (as if trapped). 163 | """ 164 | 165 | def select_cat_move(self, random_cat, minimax, alpha_beta, depth_limited, 166 | max_depth, iterative_deepening, allotted_time): 167 | """Select a move for the cat based on the chosen algorithm.""" 168 | CatTrapGame.start_time = time.time() 169 | CatTrapGame.deadline = CatTrapGame.start_time + allotted_time 170 | CatTrapGame.terminated = False 171 | move = self.cat 172 | 173 | if VERBOSE: 174 | print('\n======= NEW MOVE =======') 175 | 176 | if random_cat: 177 | move = self.random_cat_move() 178 | elif minimax: 179 | # Select a move using the Minimax algorithm. 180 | move = self.alpha_beta() if alpha_beta else self.minimax() 181 | elif depth_limited: 182 | # Select a move using Depth-Limited Search. 183 | self.placeholder_warning() 184 | move = self.random_cat_move() 185 | elif iterative_deepening: 186 | # Select a move using the Iterative Deepening algorithm. 187 | move = self.iterative_deepening(alpha_beta) 188 | 189 | elapsed_time = (time.time() - CatTrapGame.start_time) * 1000 190 | if VERBOSE: 191 | print(f'Elapsed time: {elapsed_time:.3f}ms ') 192 | print(f'New cat coordinates: {move}') 193 | temp = copy.deepcopy(self) 194 | if move != TIMEOUT: 195 | temp.move_cat(move) 196 | temp.print_hexgrid() 197 | return move 198 | 199 | def random_cat_move(self): 200 | """Randomly select a move for the cat.""" 201 | moves = self.get_cat_moves() 202 | if moves: 203 | return random.choice(moves) 204 | return self.cat 205 | 206 | def max_value(self, depth): 207 | """ 208 | Calculate the maximum value for the current game in the Minimax algorithm. 209 | """ 210 | if self.time_left() < LAST_CALL_MS: 211 | CatTrapGame.terminated = True 212 | return TIMEOUT, 0 213 | 214 | # Check if terminal state 215 | legal_moves = self.get_cat_moves() # Possible directions: E, W, NE, NW, SE, SW 216 | if not legal_moves: 217 | max_turns = 2 * (CatTrapGame.size ** 2) 218 | utility = (max_turns - depth) * (-500) # Utility for cat's defeat 219 | return self.cat, utility 220 | 221 | best_value = float('-inf') 222 | best_move = legal_moves[0] 223 | for move in legal_moves: 224 | next_game = copy.deepcopy(self) 225 | next_game.apply_move(move, cat_turn = True) 226 | value = next_game.min_value(depth + 1) 227 | 228 | if CatTrapGame.terminated: 229 | return TIMEOUT, 0 230 | 231 | if value > best_value: 232 | best_value = value 233 | best_move = move 234 | 235 | return best_move, best_value 236 | 237 | def min_value(self, depth): 238 | """ 239 | Calculate the minimum value for the current game in the Minimax algorithm. 240 | 241 | Unlike max_value, min_value does not iterate over specific 242 | directions (E, W, NE, NW, etc.). Instead, it examines every 243 | possible free tile on the board. 244 | """ 245 | if self.time_left() < LAST_CALL_MS: 246 | CatTrapGame.terminated = True 247 | return 0 248 | 249 | # Check if terminal state 250 | r, c = self.cat 251 | n = CatTrapGame.size 252 | if ( 253 | r == 0 or r == n - 1 or 254 | c == 0 or c == n - 1 255 | ): 256 | max_turns = 2 * (CatTrapGame.size ** 2) 257 | return (max_turns - depth) * (500) # Utility for cat's victory 258 | 259 | best_value = float('inf') 260 | 261 | # Iterate through all legal moves for the player (empty tiles) 262 | legal_moves = [list(rc) for rc in np.argwhere(self.hexgrid == EMPTY_TILE)] 263 | for move in legal_moves: 264 | next_game = copy.deepcopy(self) 265 | next_game.apply_move(move, cat_turn = False) 266 | _, value = next_game.max_value(depth + 1) 267 | 268 | if CatTrapGame.terminated: 269 | return 0 270 | 271 | best_value = min(best_value, value) 272 | 273 | return best_value 274 | 275 | def minimax(self): 276 | """ 277 | Perform the Minimax algorithm to determine the best move. 278 | """ 279 | best_move, _ = self.max_value(depth = 0) 280 | return best_move 281 | 282 | def alpha_beta_max_value(self, alpha, beta, depth): 283 | """ 284 | Calculate the maximum value for the current game state 285 | using Alpha-Beta pruning. 286 | """ 287 | if self.time_left() < LAST_CALL_MS: 288 | CatTrapGame.terminated = True 289 | return TIMEOUT, 0 290 | 291 | # Check if terminal state 292 | legal_moves = self.get_cat_moves() # Possible directions: E, W, NE, NW, SE, SW 293 | if not legal_moves: 294 | max_turns = 2 * (CatTrapGame.size ** 2) 295 | utility = (max_turns - depth) * (-500) # Utility for cat's defeat 296 | return self.cat, utility 297 | 298 | best_value = float('-inf') 299 | best_move = legal_moves[0] 300 | for move in legal_moves: 301 | next_game = copy.deepcopy(self) 302 | next_game.apply_move(move, cat_turn = True) 303 | value = next_game.alpha_beta_min_value(alpha, beta, depth + 1) 304 | 305 | if CatTrapGame.terminated: 306 | return TIMEOUT, 0 307 | 308 | if value > best_value: 309 | best_value = value 310 | best_move = move 311 | 312 | if best_value >= beta: # Pruning 313 | return best_move, best_value 314 | alpha = max(alpha, best_value) 315 | 316 | return best_move, best_value 317 | 318 | def alpha_beta_min_value(self, alpha, beta, depth): 319 | """ 320 | Calculate the minimum value for the current game state 321 | using Alpha-Beta pruning. 322 | 323 | Unlike max_value, min_value does not iterate over specific 324 | directions (E, W, NE, NW, etc.). Instead, it examines every 325 | possible free tile on the board. 326 | """ 327 | if self.time_left() < LAST_CALL_MS: 328 | CatTrapGame.terminated = True 329 | return 0 330 | 331 | # Check if terminal state 332 | r, c = self.cat 333 | n = CatTrapGame.size 334 | if ( 335 | r == 0 or r == n - 1 or 336 | c == 0 or c == n - 1 337 | ): 338 | max_turns = 2 * (CatTrapGame.size ** 2) 339 | return (max_turns - depth) * (500) # Utility for cat's victory 340 | 341 | best_value = float('inf') 342 | 343 | # Iterate through all legal moves for the player (empty tiles) 344 | legal_moves = [list(rc) for rc in np.argwhere(self.hexgrid == EMPTY_TILE)] 345 | for move in legal_moves: 346 | next_game = copy.deepcopy(self) 347 | next_game.apply_move(move, cat_turn = False) 348 | _, value = next_game.alpha_beta_max_value(alpha, beta, depth + 1) 349 | 350 | if CatTrapGame.terminated: 351 | return 0 352 | 353 | best_value = min(best_value, value) 354 | 355 | if best_value <= alpha: # Pruning 356 | return best_value 357 | beta = min(beta, best_value) 358 | 359 | return best_value 360 | 361 | def alpha_beta(self): 362 | """ 363 | Perform the Alpha-Beta pruning algorithm to determine the best move. 364 | """ 365 | alpha = float('-inf') 366 | beta = float('inf') 367 | best_move, _ = self.alpha_beta_max_value(alpha, beta, depth = 0) 368 | return best_move 369 | 370 | def iterative_deepening(self, alpha_beta): 371 | """ 372 | Perform iterative deepening search with an option to use Alpha-Beta pruning. 373 | """ 374 | self.placeholder_warning() 375 | return self.random_cat_move() 376 | 377 | def placeholder_warning(self): 378 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 379 | print(f'{signs} {signs}') 380 | print(' WARNING') 381 | print('This is a temporary implementation using') 382 | print("the random algorithm. You're supposed to") 383 | print('write code to solve a challenge.') 384 | print('Did you run the wrong version of main.py?') 385 | print('Double-check its path.') 386 | print(f'{signs} {signs}') 387 | 388 | if __name__ == '__main__': 389 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 390 | print(f'\n{signs} {signs}') 391 | print(' WARNING') 392 | print('You ran cat_trap_algorithms.py') 393 | print('This file contains the AI algorithms') 394 | print('and classes for the intelligent cat.') 395 | print('Did you mean to run main.py?') 396 | print(f'{signs} {signs}\n') 397 | -------------------------------------------------------------------------------- /src/Ch02/02_09/cat_trap_algorithms.py: -------------------------------------------------------------------------------- 1 | """ 2 | 02_09 - Challenge: A pruning cat 3 | 4 | Go to line 286 for the challenge! 5 | 6 | Cat Trap Algorithms 7 | 8 | This is the relevant code for the LinkedIn Learning Course 9 | AI Algorithms for Game Design with Python, by Eduardo Corpeño. 10 | 11 | For the GUI, this code uses the Cat Trap UI VSCode extension 12 | included in the extensions folder. 13 | """ 14 | 15 | import random 16 | import copy 17 | import time 18 | import numpy as np 19 | 20 | # Constants 21 | CAT_TILE = 6 22 | BLOCKED_TILE = 1 23 | EMPTY_TILE = 0 24 | LAST_CALL_MS = 0.5 25 | VERBOSE = True 26 | TIMEOUT = [-1, -1] 27 | 28 | class CatTrapGame: 29 | """ 30 | Represents a Cat Trap game state. Includes methods for managing game state 31 | and selecting moves for the cat using different AI algorithms. 32 | """ 33 | 34 | size = 0 35 | start_time = time.time() 36 | deadline = time.time() 37 | terminated = False 38 | 39 | def __init__(self, size): 40 | self.cat = [size // 2] * 2 41 | self.hexgrid = np.full((size, size), EMPTY_TILE) 42 | self.hexgrid[tuple(self.cat)] = CAT_TILE 43 | CatTrapGame.size = size 44 | 45 | def initialize_random_hexgrid(self): 46 | """Randomly initialize blocked hexgrid.""" 47 | tiles = CatTrapGame.size ** 2 48 | num_blocks = random.randint(round(0.067 * tiles), round(0.13 * tiles)) 49 | count = 0 50 | self.hexgrid[tuple(self.cat)] = CAT_TILE 51 | 52 | while count < num_blocks: 53 | r = random.randint(0, CatTrapGame.size - 1) 54 | c = random.randint(0, CatTrapGame.size - 1) 55 | if self.hexgrid[r, c] == EMPTY_TILE: 56 | self.hexgrid[r, c] = BLOCKED_TILE 57 | count += 1 58 | if VERBOSE: 59 | print('\n======= NEW GAME =======') 60 | self.print_hexgrid() 61 | 62 | def set_hexgrid(self, hexgrid): 63 | """Copy incoming hexgrid.""" 64 | self.hexgrid = hexgrid 65 | self.cat = list(np.argwhere(self.hexgrid == CAT_TILE)[0]) # Find the cat 66 | if VERBOSE: 67 | print('\n======= NEW GAME =======') 68 | self.print_hexgrid() 69 | 70 | def block_tile(self, coord): 71 | self.hexgrid[tuple(coord)] = BLOCKED_TILE 72 | 73 | def unblock_tile(self, coord): 74 | self.hexgrid[tuple(coord)] = EMPTY_TILE 75 | 76 | def place_cat(self, coord): 77 | self.hexgrid[tuple(coord)] = CAT_TILE 78 | self.cat = coord 79 | 80 | def move_cat(self, coord): 81 | self.hexgrid[tuple(self.cat)] = EMPTY_TILE # Clear previous cat position 82 | self.place_cat(coord) 83 | 84 | def get_cat_moves(self): 85 | """ 86 | Get a list of valid moves for the cat. 87 | """ 88 | hexgrid = self.hexgrid 89 | r, c = self.cat 90 | n = CatTrapGame.size 91 | col_offset = r % 2 # Offset for columns based on row parity 92 | moves = [] 93 | 94 | # Directions with column adjustments 95 | directions = { 96 | 'E': (0, 1), 97 | 'W': (0, -1), 98 | 'NE': (-1, col_offset), 99 | 'NW': (-1, -1 + col_offset), 100 | 'SE': (1, col_offset), 101 | 'SW': (1, -1 + col_offset), 102 | } 103 | 104 | for dr, dc in directions.values(): 105 | tr, tc = r + dr, c + dc # Calculate target row and column 106 | if 0 <= tr < n and 0 <= tc < n and hexgrid[tr, tc] == EMPTY_TILE: 107 | moves.append([tr, tc]) 108 | 109 | return moves 110 | 111 | def apply_move(self, move, cat_turn): 112 | """ 113 | Apply a move to the game state. 114 | """ 115 | if self.hexgrid[tuple(move)] != EMPTY_TILE: 116 | action_str = "move cat to" if cat_turn else "block" 117 | self.print_hexgrid() 118 | print('\n=====================================') 119 | print(f'Attempting to {action_str} {move} = {self.hexgrid[tuple(move)]}') 120 | print('Invalid Move! Check your code.') 121 | print('=====================================\n') 122 | 123 | if cat_turn: 124 | self.move_cat(move) 125 | else: 126 | self.hexgrid[tuple(move)] = BLOCKED_TILE 127 | 128 | def time_left(self): 129 | """ 130 | Calculate the time remaining before the deadline. 131 | """ 132 | return (CatTrapGame.deadline - time.time()) * 1000 133 | 134 | def print_hexgrid(self): 135 | """ 136 | Print the current state of the game board using special characters. 137 | """ 138 | tile_map = { 139 | EMPTY_TILE: ' ⬡', # Alternative: '-' 140 | BLOCKED_TILE: ' ⬢', # Alternative: 'X' 141 | CAT_TILE: '🐈' # Alternative: 'C' 142 | } 143 | for r in range(CatTrapGame.size): 144 | # Add a leading space for odd rows for staggered effect 145 | prefix = ' ' if r % 2 != 0 else '' 146 | row_display = ' '.join(tile_map[cell] for cell in self.hexgrid[r]) 147 | print(prefix + row_display) 148 | return 149 | 150 | 151 | # ===================== Intelligent Agents ===================== 152 | """ 153 | Intelligent Agents for the Cat Trap game. These agents examine the game 154 | state, and return the new position of the cat (a move). 155 | Two special return values for failure may be returned (timeout or trapped). 156 | 157 | Parameters: 158 | - random_cat: A random move for the cat. 159 | - minimax: Use the Minimax algorithm. 160 | - alpha_beta: Use Alpha-Beta Pruning. 161 | - depth_limited: Use Depth-Limited Search. 162 | - max_depth: Maximum depth to explore for Depth-Limited Search. 163 | - iterative_deepening: Use Iterative Deepening. 164 | - allotted_time: Maximum time in seconds for the cat to respond. 165 | 166 | If no algorithm is selected, the cat gives up (as if trapped). 167 | """ 168 | 169 | def select_cat_move(self, random_cat, minimax, alpha_beta, depth_limited, 170 | max_depth, iterative_deepening, allotted_time): 171 | """Select a move for the cat based on the chosen algorithm.""" 172 | CatTrapGame.start_time = time.time() 173 | CatTrapGame.deadline = CatTrapGame.start_time + allotted_time 174 | CatTrapGame.terminated = False 175 | move = self.cat 176 | 177 | if VERBOSE: 178 | print('\n======= NEW MOVE =======') 179 | 180 | if random_cat: 181 | move = self.random_cat_move() 182 | elif minimax: 183 | # Select a move using the Minimax algorithm. 184 | move = self.alpha_beta() if alpha_beta else self.minimax() 185 | elif depth_limited: 186 | # Select a move using Depth-Limited Search. 187 | self.placeholder_warning() 188 | move = self.random_cat_move() 189 | elif iterative_deepening: 190 | # Select a move using the Iterative Deepening algorithm. 191 | move = self.iterative_deepening(alpha_beta) 192 | 193 | elapsed_time = (time.time() - CatTrapGame.start_time) * 1000 194 | if VERBOSE: 195 | print(f'Elapsed time: {elapsed_time:.3f}ms ') 196 | print(f'New cat coordinates: {move}') 197 | temp = copy.deepcopy(self) 198 | if move != TIMEOUT: 199 | temp.move_cat(move) 200 | temp.print_hexgrid() 201 | return move 202 | 203 | def random_cat_move(self): 204 | """Randomly select a move for the cat.""" 205 | moves = self.get_cat_moves() 206 | if moves: 207 | return random.choice(moves) 208 | return self.cat 209 | 210 | def max_value(self, depth): 211 | """ 212 | Calculate the maximum value for the current game in the Minimax algorithm. 213 | """ 214 | if self.time_left() < LAST_CALL_MS: 215 | CatTrapGame.terminated = True 216 | return TIMEOUT, 0 217 | 218 | # Check if terminal state 219 | legal_moves = self.get_cat_moves() # Possible directions: E, W, NE, NW, SE, SW 220 | if not legal_moves: 221 | max_turns = 2 * (CatTrapGame.size ** 2) 222 | utility = (max_turns - depth) * (-500) # Utility for cat's defeat 223 | return self.cat, utility 224 | 225 | best_value = float('-inf') 226 | best_move = legal_moves[0] 227 | for move in legal_moves: 228 | next_game = copy.deepcopy(self) 229 | next_game.apply_move(move, cat_turn = True) 230 | value = next_game.min_value(depth + 1) 231 | 232 | if CatTrapGame.terminated: 233 | return TIMEOUT, 0 234 | 235 | if value > best_value: 236 | best_value = value 237 | best_move = move 238 | 239 | return best_move, best_value 240 | 241 | def min_value(self, depth): 242 | """ 243 | Calculate the minimum value for the current game in the Minimax algorithm. 244 | 245 | Unlike max_value, min_value does not iterate over specific 246 | directions (E, W, NE, NW, etc.). Instead, it examines every 247 | possible free tile on the board. 248 | """ 249 | if self.time_left() < LAST_CALL_MS: 250 | CatTrapGame.terminated = True 251 | return 0 252 | 253 | # Check if terminal state 254 | r, c = self.cat 255 | n = CatTrapGame.size 256 | if ( 257 | r == 0 or r == n - 1 or 258 | c == 0 or c == n - 1 259 | ): 260 | max_turns = 2 * (CatTrapGame.size ** 2) 261 | return (max_turns - depth) * (500) # Utility for cat's victory 262 | 263 | best_value = float('inf') 264 | 265 | # Iterate through all legal moves for the player (empty tiles) 266 | legal_moves = [list(rc) for rc in np.argwhere(self.hexgrid == EMPTY_TILE)] 267 | for move in legal_moves: 268 | next_game = copy.deepcopy(self) 269 | next_game.apply_move(move, cat_turn = False) 270 | _, value = next_game.max_value(depth + 1) 271 | 272 | if CatTrapGame.terminated: 273 | return 0 274 | 275 | best_value = min(best_value, value) 276 | 277 | return best_value 278 | 279 | def minimax(self): 280 | """ 281 | Perform the Minimax algorithm to determine the best move. 282 | """ 283 | best_move, _ = self.max_value(depth = 0) 284 | return best_move 285 | 286 | def alpha_beta_max_value(self, alpha, beta, depth): 287 | """ 288 | Calculate the maximum value for the current game state 289 | using Alpha-Beta pruning. 290 | 291 | 02_09 - Challenge: A pruning cat 292 | 293 | Your task is to implement the alpha-beta pruning algorithm. 294 | You will do this by adding code to alpha_beta_max_value() and 295 | alpha_beta_min_value(), which are currently exact copies of 296 | max_value() and min_value() respectively (except for the alpha 297 | and beta parameters). 298 | 299 | Make sure to take care of the following considerations: 300 | 1) Remove the placeholder code immediately below these instructions. 301 | 2) Read through the skeleton code provided below for both methods. 302 | 3) Fill in the blanks following the instructions in the "TODO:" comments. 303 | 4) If you're stuck, you may ask in the course's Q&A or consult the 304 | solution in the next folder to unblock yourself without spoiling too 305 | much of the fun. 306 | """ 307 | # TODO: Remove the following 2 lines to enable your alpha-beta implementation. 308 | self.placeholder_warning() 309 | return self.random_cat_move(), 0 310 | 311 | # Skeleton Code - Alpha-Beta Pruning 312 | # HINT: There are only 2 "TODO:" comments below. 313 | 314 | if self.time_left() < LAST_CALL_MS: 315 | CatTrapGame.terminated = True 316 | return TIMEOUT, 0 317 | 318 | # Check if terminal state 319 | legal_moves = self.get_cat_moves() # Possible directions: E, W, NE, NW, SE, SW 320 | if not legal_moves: 321 | max_turns = 2 * (CatTrapGame.size ** 2) 322 | utility = (max_turns - depth) * (-500) # Utility for cat's defeat 323 | return self.cat, utility 324 | 325 | best_value = float('-inf') 326 | best_move = legal_moves[0] 327 | for move in legal_moves: 328 | next_game = copy.deepcopy(self) 329 | next_game.apply_move(move, cat_turn = True) 330 | value = next_game.alpha_beta_min_value(alpha, beta, depth + 1) 331 | 332 | if CatTrapGame.terminated: 333 | return TIMEOUT, 0 334 | 335 | if value > best_value: 336 | best_value = value 337 | best_move = move 338 | 339 | # TODO: Write the alpha-beta updating code. 340 | # HINT: Look at steps 6 and 7 of the algorithm in the 341 | # alpha-beta search algorithm video. 342 | 343 | return best_move, best_value 344 | 345 | def alpha_beta_min_value(self, alpha, beta, depth): 346 | """ 347 | Calculate the minimum value for the current game state 348 | using Alpha-Beta pruning. 349 | 350 | Unlike max_value, min_value does not iterate over specific 351 | directions (E, W, NE, NW, etc.). Instead, it examines every 352 | possible free tile on the board. 353 | """ 354 | if self.time_left() < LAST_CALL_MS: 355 | CatTrapGame.terminated = True 356 | return 0 357 | 358 | # Check if terminal state 359 | r, c = self.cat 360 | n = CatTrapGame.size 361 | if ( 362 | r == 0 or r == n - 1 or 363 | c == 0 or c == n - 1 364 | ): 365 | max_turns = 2 * (CatTrapGame.size ** 2) 366 | return (max_turns - depth) * (500) # Utility for cat's victory 367 | 368 | best_value = float('inf') 369 | 370 | # Iterate through all legal moves for the player (empty tiles) 371 | legal_moves = [list(rc) for rc in np.argwhere(self.hexgrid == EMPTY_TILE)] 372 | for move in legal_moves: 373 | next_game = copy.deepcopy(self) 374 | next_game.apply_move(move, cat_turn = False) 375 | _, value = next_game.alpha_beta_max_value(alpha, beta, depth + 1) 376 | 377 | if CatTrapGame.terminated: 378 | return 0 379 | 380 | best_value = min(best_value, value) 381 | # TODO: Write the alpha-beta updating code. 382 | # HINT: Look at steps 6 and 7 of the algorithm in the 383 | # alpha-beta search algorithm video. 384 | 385 | return best_value 386 | 387 | def alpha_beta(self): 388 | """ 389 | Perform the Alpha-Beta pruning algorithm to determine the best move. 390 | """ 391 | alpha = float('-inf') 392 | beta = float('inf') 393 | best_move, _ = self.alpha_beta_max_value(alpha, beta, depth = 0) 394 | return best_move 395 | 396 | def iterative_deepening(self, alpha_beta): 397 | """ 398 | Perform iterative deepening search with an option to use Alpha-Beta pruning. 399 | """ 400 | self.placeholder_warning() 401 | return self.random_cat_move() 402 | 403 | def placeholder_warning(self): 404 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 405 | print(f'{signs} {signs}') 406 | print(' WARNING') 407 | print('This is a temporary implementation using') 408 | print("the random algorithm. You're supposed to") 409 | print('write code to solve a challenge.') 410 | print('Did you run the wrong version of main.py?') 411 | print('Double-check its path.') 412 | print(f'{signs} {signs}') 413 | 414 | if __name__ == '__main__': 415 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 416 | print(f'\n{signs} {signs}') 417 | print(' WARNING') 418 | print('You ran cat_trap_algorithms.py') 419 | print('This file contains the AI algorithms') 420 | print('and classes for the intelligent cat.') 421 | print('Did you mean to run main.py?') 422 | print(f'{signs} {signs}\n') 423 | -------------------------------------------------------------------------------- /src/Ch03/03_05/cat_trap_algorithms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cat Trap Algorithms 3 | 4 | This is the relevant code for the LinkedIn Learning Course 5 | AI Algorithms for Game Design with Python, by Eduardo Corpeño. 6 | 7 | For the GUI, this code uses the Cat Trap UI VSCode extension 8 | included in the extensions folder. 9 | """ 10 | 11 | import random 12 | import copy 13 | import time 14 | import numpy as np 15 | 16 | # Constants 17 | CAT_TILE = 6 18 | BLOCKED_TILE = 1 19 | EMPTY_TILE = 0 20 | LAST_CALL_MS = 0.5 21 | VERBOSE = True 22 | TIMEOUT = [-1, -1] 23 | 24 | class CatTrapGame: 25 | """ 26 | Represents a Cat Trap game state. Includes methods for managing game state 27 | and selecting moves for the cat using different AI algorithms. 28 | """ 29 | 30 | size = 0 31 | start_time = time.time() 32 | deadline = time.time() 33 | terminated = False 34 | max_depth = float('inf') 35 | reached_max_depth = False 36 | 37 | def __init__(self, size): 38 | self.cat = [size // 2] * 2 39 | self.hexgrid = np.full((size, size), EMPTY_TILE) 40 | self.hexgrid[tuple(self.cat)] = CAT_TILE 41 | CatTrapGame.size = size 42 | 43 | def initialize_random_hexgrid(self): 44 | """Randomly initialize blocked hexgrid.""" 45 | tiles = CatTrapGame.size ** 2 46 | num_blocks = random.randint(round(0.067 * tiles), round(0.13 * tiles)) 47 | count = 0 48 | self.hexgrid[tuple(self.cat)] = CAT_TILE 49 | 50 | while count < num_blocks: 51 | r = random.randint(0, CatTrapGame.size - 1) 52 | c = random.randint(0, CatTrapGame.size - 1) 53 | if self.hexgrid[r, c] == EMPTY_TILE: 54 | self.hexgrid[r, c] = BLOCKED_TILE 55 | count += 1 56 | if VERBOSE: 57 | print('\n======= NEW GAME =======') 58 | self.print_hexgrid() 59 | 60 | def set_hexgrid(self, hexgrid): 61 | """Copy incoming hexgrid.""" 62 | self.hexgrid = hexgrid 63 | self.cat = list(np.argwhere(self.hexgrid == CAT_TILE)[0]) # Find the cat 64 | if VERBOSE: 65 | print('\n======= NEW GAME =======') 66 | self.print_hexgrid() 67 | 68 | def block_tile(self, coord): 69 | self.hexgrid[tuple(coord)] = BLOCKED_TILE 70 | 71 | def unblock_tile(self, coord): 72 | self.hexgrid[tuple(coord)] = EMPTY_TILE 73 | 74 | def place_cat(self, coord): 75 | self.hexgrid[tuple(coord)] = CAT_TILE 76 | self.cat = coord 77 | 78 | def move_cat(self, coord): 79 | self.hexgrid[tuple(self.cat)] = EMPTY_TILE # Clear previous cat position 80 | self.place_cat(coord) 81 | 82 | def get_cat_moves(self): 83 | """ 84 | Get a list of valid moves for the cat. 85 | """ 86 | hexgrid = self.hexgrid 87 | r, c = self.cat 88 | n = CatTrapGame.size 89 | col_offset = r % 2 # Offset for columns based on row parity 90 | moves = [] 91 | 92 | # Directions with column adjustments 93 | directions = { 94 | 'E': (0, 1), 95 | 'W': (0, -1), 96 | 'NE': (-1, col_offset), 97 | 'NW': (-1, -1 + col_offset), 98 | 'SE': (1, col_offset), 99 | 'SW': (1, -1 + col_offset), 100 | } 101 | 102 | for dr, dc in directions.values(): 103 | tr, tc = r + dr, c + dc # Calculate target row and column 104 | if 0 <= tr < n and 0 <= tc < n and hexgrid[tr, tc] == EMPTY_TILE: 105 | moves.append([tr, tc]) 106 | 107 | return moves 108 | 109 | def apply_move(self, move, cat_turn): 110 | """ 111 | Apply a move to the game state. 112 | """ 113 | if self.hexgrid[tuple(move)] != EMPTY_TILE: 114 | action_str = "move cat to" if cat_turn else "block" 115 | self.print_hexgrid() 116 | print('\n=====================================') 117 | print(f'Attempting to {action_str} {move} = {self.hexgrid[tuple(move)]}') 118 | print('Invalid Move! Check your code.') 119 | print('=====================================\n') 120 | 121 | if cat_turn: 122 | self.move_cat(move) 123 | else: 124 | self.hexgrid[tuple(move)] = BLOCKED_TILE 125 | 126 | def time_left(self): 127 | """ 128 | Calculate the time remaining before the deadline. 129 | """ 130 | return (CatTrapGame.deadline - time.time()) * 1000 131 | 132 | def print_hexgrid(self): 133 | """ 134 | Print the current state of the game board using special characters. 135 | """ 136 | tile_map = { 137 | EMPTY_TILE: ' ⬡', # Alternative: '-' 138 | BLOCKED_TILE: ' ⬢', # Alternative: 'X' 139 | CAT_TILE: '🐈' # Alternative: 'C' 140 | } 141 | for r in range(CatTrapGame.size): 142 | # Add a leading space for odd rows for staggered effect 143 | prefix = ' ' if r % 2 != 0 else '' 144 | row_display = ' '.join(tile_map[cell] for cell in self.hexgrid[r]) 145 | print(prefix + row_display) 146 | return 147 | 148 | def evaluation(self, cat_turn): 149 | """ 150 | Evaluation function. 151 | Options: 'moves', 'straight_exit', 'custom'. 152 | """ 153 | evaluation_function = 'straight_exit' 154 | 155 | if evaluation_function == 'moves': 156 | return self.eval_moves(cat_turn) 157 | elif evaluation_function == 'straight_exit': 158 | return self.eval_straight_exit(cat_turn) 159 | elif evaluation_function == 'custom': 160 | return self.eval_custom(cat_turn) 161 | return 0 162 | 163 | def eval_moves(self, cat_turn): 164 | """ 165 | Evaluate based on the number of valid moves available for the cat. 166 | """ 167 | cat_moves = self.get_cat_moves() 168 | return len(cat_moves) if cat_turn else len(cat_moves) - 1 169 | 170 | def get_target_position(self, scout, direction): 171 | """ 172 | Get the target position based on the specified direction. 173 | """ 174 | r, c = scout 175 | col_offset = r % 2 # Offset for columns based on row parity 176 | 177 | if direction == 'E': 178 | return [r, c + 1] 179 | elif direction == 'W': 180 | return [r, c - 1] 181 | elif direction == 'NE': 182 | return [r - 1, c + col_offset] 183 | elif direction == 'NW': 184 | return [r - 1, c - 1 + col_offset] 185 | elif direction == 'SE': 186 | return [r + 1, c + col_offset] 187 | elif direction == 'SW': 188 | return [r + 1, c - 1 + col_offset] 189 | return [r, c] 190 | 191 | def eval_straight_exit(self, cat_turn): 192 | """ 193 | Evaluate the cat's access to the nearest board edge along straight paths. 194 | """ 195 | distances = [] 196 | directions = ['E', 'W', 'NE', 'NW', 'SE', 'SW'] 197 | n = CatTrapGame.size 198 | for dir in directions: 199 | distance = 0 200 | r, c = self.cat 201 | while not (r < 0 or r >= n or c < 0 or c >= n): 202 | if self.hexgrid[r, c] == BLOCKED_TILE: 203 | distance += n # Increase cost for blocked paths 204 | break 205 | distance += 1 206 | r, c = self.get_target_position([r, c], dir) 207 | distances.append(distance) 208 | 209 | distances.sort() # Ascending order, so distances[0] is the best 210 | return CatTrapGame.size - (distances[0] if cat_turn else distances[1]) 211 | 212 | def eval_custom(self, cat_turn): 213 | """ 214 | Placeholder for a custom evaluation function. 215 | """ 216 | return 2 if cat_turn else 1 217 | 218 | # ===================== Intelligent Agents ===================== 219 | """ 220 | Intelligent Agents for the Cat Trap game. These agents examine the game 221 | state, and return the new position of the cat (a move). 222 | Two special return values for failure may be returned (timeout or trapped). 223 | 224 | Parameters: 225 | - random_cat: A random move for the cat. 226 | - minimax: Use the Minimax algorithm. 227 | - alpha_beta: Use Alpha-Beta Pruning. 228 | - depth_limited: Use Depth-Limited Search. 229 | - max_depth: Maximum depth to explore for Depth-Limited Search. 230 | - iterative_deepening: Use Iterative Deepening. 231 | - allotted_time: Maximum time in seconds for the cat to respond. 232 | 233 | If no algorithm is selected, the cat gives up (as if trapped). 234 | """ 235 | 236 | def select_cat_move(self, random_cat, minimax, alpha_beta, depth_limited, 237 | max_depth, iterative_deepening, allotted_time): 238 | """Select a move for the cat based on the chosen algorithm.""" 239 | CatTrapGame.start_time = time.time() 240 | CatTrapGame.deadline = CatTrapGame.start_time + allotted_time 241 | CatTrapGame.terminated = False 242 | CatTrapGame.max_depth = float('inf') 243 | CatTrapGame.reached_max_depth = False 244 | move = self.cat 245 | 246 | if VERBOSE: 247 | print('\n======= NEW MOVE =======') 248 | 249 | if random_cat: 250 | move = self.random_cat_move() 251 | elif minimax: 252 | # Select a move using the Minimax algorithm. 253 | move = self.alpha_beta() if alpha_beta else self.minimax() 254 | elif depth_limited: 255 | # Select a move using Depth-Limited Search. 256 | CatTrapGame.max_depth = max_depth 257 | move = self.alpha_beta() if alpha_beta else self.minimax() 258 | elif iterative_deepening: 259 | # Select a move using the Iterative Deepening algorithm. 260 | move = self.iterative_deepening(alpha_beta) 261 | 262 | elapsed_time = (time.time() - CatTrapGame.start_time) * 1000 263 | if VERBOSE: 264 | print(f'Elapsed time: {elapsed_time:.3f}ms ') 265 | print(f'New cat coordinates: {move}') 266 | temp = copy.deepcopy(self) 267 | if move != TIMEOUT: 268 | temp.move_cat(move) 269 | temp.print_hexgrid() 270 | return move 271 | 272 | def random_cat_move(self): 273 | """Randomly select a move for the cat.""" 274 | moves = self.get_cat_moves() 275 | if moves: 276 | return random.choice(moves) 277 | return self.cat 278 | 279 | def max_value(self, depth): 280 | """ 281 | Calculate the maximum value for the current game in the Minimax algorithm. 282 | """ 283 | if self.time_left() < LAST_CALL_MS: 284 | CatTrapGame.terminated = True 285 | return TIMEOUT, 0 286 | 287 | legal_moves = self.get_cat_moves() # Possible directions: E, W, NE, NW, SE, SW 288 | if not legal_moves: 289 | max_turns = 2 * (CatTrapGame.size ** 2) 290 | utility = (max_turns - depth) * (-500) # Utility for cat's defeat 291 | return self.cat, utility 292 | 293 | if depth == CatTrapGame.max_depth: 294 | CatTrapGame.reached_max_depth = True 295 | return self.cat, self.evaluation(cat_turn = True) 296 | 297 | best_value = float('-inf') 298 | best_move = legal_moves[0] 299 | for move in legal_moves: 300 | next_game = copy.deepcopy(self) 301 | next_game.apply_move(move, cat_turn = True) 302 | value = next_game.min_value(depth + 1) 303 | 304 | if CatTrapGame.terminated: 305 | return TIMEOUT, 0 306 | 307 | if value > best_value: 308 | best_value = value 309 | best_move = move 310 | 311 | return best_move, best_value 312 | 313 | def min_value(self, depth): 314 | """ 315 | Calculate the minimum value for the current game in the Minimax algorithm. 316 | 317 | Unlike max_value, min_value does not iterate over specific 318 | directions (E, W, NE, NW, etc.). Instead, it examines every 319 | possible free tile on the board. 320 | """ 321 | if self.time_left() < LAST_CALL_MS: 322 | CatTrapGame.terminated = True 323 | return 0 324 | 325 | # Check if terminal state 326 | r, c = self.cat 327 | n = CatTrapGame.size 328 | if ( 329 | r == 0 or r == n - 1 or 330 | c == 0 or c == n - 1 331 | ): 332 | max_turns = 2 * (CatTrapGame.size ** 2) 333 | return (max_turns - depth) * (500) # Utility for cat's victory 334 | 335 | if depth == CatTrapGame.max_depth: 336 | CatTrapGame.reached_max_depth = True 337 | return self.evaluation(cat_turn = False) 338 | 339 | best_value = float('inf') 340 | 341 | # Iterate through all legal moves for the player (empty tiles) 342 | legal_moves = [list(rc) for rc in np.argwhere(self.hexgrid == EMPTY_TILE)] 343 | for move in legal_moves: 344 | next_game = copy.deepcopy(self) 345 | next_game.apply_move(move, cat_turn = False) 346 | _, value = next_game.max_value(depth + 1) 347 | 348 | if CatTrapGame.terminated: 349 | return 0 350 | 351 | best_value = min(best_value, value) 352 | 353 | return best_value 354 | 355 | def minimax(self): 356 | """ 357 | Perform the Minimax algorithm to determine the best move. 358 | """ 359 | best_move, _ = self.max_value(depth = 0) 360 | return best_move 361 | 362 | def alpha_beta_max_value(self, alpha, beta, depth): 363 | """ 364 | Calculate the maximum value for the current game state 365 | using Alpha-Beta pruning. 366 | """ 367 | if self.time_left() < LAST_CALL_MS: 368 | CatTrapGame.terminated = True 369 | return TIMEOUT, 0 370 | 371 | legal_moves = self.get_cat_moves() # Possible directions: E, W, NE, NW, SE, SW 372 | if not legal_moves: 373 | max_turns = 2 * (CatTrapGame.size ** 2) 374 | utility = (max_turns - depth) * (-500) # Utility for cat's defeat 375 | return self.cat, utility 376 | 377 | if depth == CatTrapGame.max_depth: 378 | CatTrapGame.reached_max_depth = True 379 | return self.cat, self.evaluation(cat_turn = True) 380 | 381 | best_value = float('-inf') 382 | best_move = legal_moves[0] 383 | for move in legal_moves: 384 | next_game = copy.deepcopy(self) 385 | next_game.apply_move(move, cat_turn = True) 386 | value = next_game.alpha_beta_min_value(alpha, beta, depth + 1) 387 | 388 | if CatTrapGame.terminated: 389 | return TIMEOUT, 0 390 | 391 | if value > best_value: 392 | best_value = value 393 | best_move = move 394 | 395 | if best_value >= beta: # Pruning 396 | return best_move, best_value 397 | alpha = max(alpha, best_value) 398 | 399 | return best_move, best_value 400 | 401 | def alpha_beta_min_value(self, alpha, beta, depth): 402 | """ 403 | Calculate the minimum value for the current game state 404 | using Alpha-Beta pruning. 405 | 406 | Unlike max_value, min_value does not iterate over specific 407 | directions (E, W, NE, NW, etc.). Instead, it examines every 408 | possible free tile on the board. 409 | """ 410 | if self.time_left() < LAST_CALL_MS: 411 | CatTrapGame.terminated = True 412 | return 0 413 | 414 | # Check if terminal state 415 | r, c = self.cat 416 | n = CatTrapGame.size 417 | if ( 418 | r == 0 or r == n - 1 or 419 | c == 0 or c == n - 1 420 | ): 421 | max_turns = 2 * (CatTrapGame.size ** 2) 422 | return (max_turns - depth) * (500) # Utility for cat's victory 423 | 424 | if depth == CatTrapGame.max_depth: 425 | CatTrapGame.reached_max_depth = True 426 | return self.evaluation(cat_turn = False) 427 | 428 | best_value = float('inf') 429 | 430 | # Iterate through all legal moves for the player (empty tiles) 431 | legal_moves = [list(rc) for rc in np.argwhere(self.hexgrid == EMPTY_TILE)] 432 | for move in legal_moves: 433 | next_game = copy.deepcopy(self) 434 | next_game.apply_move(move, cat_turn = False) 435 | _, value = next_game.alpha_beta_max_value(alpha, beta, depth + 1) 436 | 437 | if CatTrapGame.terminated: 438 | return 0 439 | 440 | best_value = min(best_value, value) 441 | 442 | if best_value <= alpha: # Pruning 443 | return best_value 444 | beta = min(beta, best_value) 445 | 446 | return best_value 447 | 448 | def alpha_beta(self): 449 | """ 450 | Perform the Alpha-Beta pruning algorithm to determine the best move. 451 | """ 452 | alpha = float('-inf') 453 | beta = float('inf') 454 | best_move, _ = self.alpha_beta_max_value(alpha, beta, depth = 0) 455 | return best_move 456 | 457 | def iterative_deepening(self, alpha_beta): 458 | """ 459 | Perform iterative deepening search with an option to use Alpha-Beta pruning. 460 | """ 461 | self.placeholder_warning() 462 | return self.random_cat_move() 463 | 464 | def placeholder_warning(self): 465 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 466 | print(f'{signs} {signs}') 467 | print(' WARNING') 468 | print('This is a temporary implementation using') 469 | print("the random algorithm. You're supposed to") 470 | print('write code to solve a challenge.') 471 | print('Did you run the wrong version of main.py?') 472 | print('Double-check its path.') 473 | print(f'{signs} {signs}') 474 | 475 | if __name__ == '__main__': 476 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 477 | print(f'\n{signs} {signs}') 478 | print(' WARNING') 479 | print('You ran cat_trap_algorithms.py') 480 | print('This file contains the AI algorithms') 481 | print('and classes for the intelligent cat.') 482 | print('Did you mean to run main.py?') 483 | print(f'{signs} {signs}\n') 484 | -------------------------------------------------------------------------------- /src/Ch03/03_07/cat_trap_algorithms.py: -------------------------------------------------------------------------------- 1 | """ 2 | Cat Trap Algorithms 3 | 4 | This is the relevant code for the LinkedIn Learning Course 5 | AI Algorithms for Game Design with Python, by Eduardo Corpeño. 6 | 7 | For the GUI, this code uses the Cat Trap UI VSCode extension 8 | included in the extensions folder. 9 | """ 10 | 11 | import random 12 | import copy 13 | import time 14 | import numpy as np 15 | 16 | # Constants 17 | CAT_TILE = 6 18 | BLOCKED_TILE = 1 19 | EMPTY_TILE = 0 20 | LAST_CALL_MS = 0.5 21 | VERBOSE = True 22 | TIMEOUT = [-1, -1] 23 | 24 | class CatTrapGame: 25 | """ 26 | Represents a Cat Trap game state. Includes methods for managing game state 27 | and selecting moves for the cat using different AI algorithms. 28 | """ 29 | 30 | size = 0 31 | start_time = time.time() 32 | deadline = time.time() 33 | terminated = False 34 | max_depth = float('inf') 35 | reached_max_depth = False 36 | 37 | def __init__(self, size): 38 | self.cat = [size // 2] * 2 39 | self.hexgrid = np.full((size, size), EMPTY_TILE) 40 | self.hexgrid[tuple(self.cat)] = CAT_TILE 41 | CatTrapGame.size = size 42 | 43 | def initialize_random_hexgrid(self): 44 | """Randomly initialize blocked hexgrid.""" 45 | tiles = CatTrapGame.size ** 2 46 | num_blocks = random.randint(round(0.067 * tiles), round(0.13 * tiles)) 47 | count = 0 48 | self.hexgrid[tuple(self.cat)] = CAT_TILE 49 | 50 | while count < num_blocks: 51 | r = random.randint(0, CatTrapGame.size - 1) 52 | c = random.randint(0, CatTrapGame.size - 1) 53 | if self.hexgrid[r, c] == EMPTY_TILE: 54 | self.hexgrid[r, c] = BLOCKED_TILE 55 | count += 1 56 | if VERBOSE: 57 | print('\n======= NEW GAME =======') 58 | self.print_hexgrid() 59 | 60 | def set_hexgrid(self, hexgrid): 61 | """Copy incoming hexgrid.""" 62 | self.hexgrid = hexgrid 63 | self.cat = list(np.argwhere(self.hexgrid == CAT_TILE)[0]) # Find the cat 64 | if VERBOSE: 65 | print('\n======= NEW GAME =======') 66 | self.print_hexgrid() 67 | 68 | def block_tile(self, coord): 69 | self.hexgrid[tuple(coord)] = BLOCKED_TILE 70 | 71 | def unblock_tile(self, coord): 72 | self.hexgrid[tuple(coord)] = EMPTY_TILE 73 | 74 | def place_cat(self, coord): 75 | self.hexgrid[tuple(coord)] = CAT_TILE 76 | self.cat = coord 77 | 78 | def move_cat(self, coord): 79 | self.hexgrid[tuple(self.cat)] = EMPTY_TILE # Clear previous cat position 80 | self.place_cat(coord) 81 | 82 | def get_cat_moves(self): 83 | """ 84 | Get a list of valid moves for the cat. 85 | """ 86 | hexgrid = self.hexgrid 87 | r, c = self.cat 88 | n = CatTrapGame.size 89 | col_offset = r % 2 # Offset for columns based on row parity 90 | moves = [] 91 | 92 | # Directions with column adjustments 93 | directions = { 94 | 'E': (0, 1), 95 | 'W': (0, -1), 96 | 'NE': (-1, col_offset), 97 | 'NW': (-1, -1 + col_offset), 98 | 'SE': (1, col_offset), 99 | 'SW': (1, -1 + col_offset), 100 | } 101 | 102 | for dr, dc in directions.values(): 103 | tr, tc = r + dr, c + dc # Calculate target row and column 104 | if 0 <= tr < n and 0 <= tc < n and hexgrid[tr, tc] == EMPTY_TILE: 105 | moves.append([tr, tc]) 106 | 107 | return moves 108 | 109 | def apply_move(self, move, cat_turn): 110 | """ 111 | Apply a move to the game state. 112 | """ 113 | if self.hexgrid[tuple(move)] != EMPTY_TILE: 114 | action_str = "move cat to" if cat_turn else "block" 115 | self.print_hexgrid() 116 | print('\n=====================================') 117 | print(f'Attempting to {action_str} {move} = {self.hexgrid[tuple(move)]}') 118 | print('Invalid Move! Check your code.') 119 | print('=====================================\n') 120 | 121 | if cat_turn: 122 | self.move_cat(move) 123 | else: 124 | self.hexgrid[tuple(move)] = BLOCKED_TILE 125 | 126 | def time_left(self): 127 | """ 128 | Calculate the time remaining before the deadline. 129 | """ 130 | return (CatTrapGame.deadline - time.time()) * 1000 131 | 132 | def print_hexgrid(self): 133 | """ 134 | Print the current state of the game board using special characters. 135 | """ 136 | tile_map = { 137 | EMPTY_TILE: ' ⬡', # Alternative: '-' 138 | BLOCKED_TILE: ' ⬢', # Alternative: 'X' 139 | CAT_TILE: '🐈' # Alternative: 'C' 140 | } 141 | for r in range(CatTrapGame.size): 142 | # Add a leading space for odd rows for staggered effect 143 | prefix = ' ' if r % 2 != 0 else '' 144 | row_display = ' '.join(tile_map[cell] for cell in self.hexgrid[r]) 145 | print(prefix + row_display) 146 | return 147 | 148 | def evaluation(self, cat_turn): 149 | """ 150 | Evaluation function. 151 | Options: 'moves', 'straight_exit', 'custom'. 152 | """ 153 | evaluation_function = 'custom' 154 | 155 | if evaluation_function == 'moves': 156 | return self.eval_moves(cat_turn) 157 | elif evaluation_function == 'straight_exit': 158 | return self.eval_straight_exit(cat_turn) 159 | elif evaluation_function == 'custom': 160 | return self.eval_custom(cat_turn) 161 | return 0 162 | 163 | def eval_moves(self, cat_turn): 164 | """ 165 | Evaluate based on the number of valid moves available for the cat. 166 | """ 167 | cat_moves = self.get_cat_moves() 168 | return len(cat_moves) if cat_turn else len(cat_moves) - 1 169 | 170 | def get_target_position(self, scout, direction): 171 | """ 172 | Get the target position based on the specified direction. 173 | """ 174 | r, c = scout 175 | col_offset = r % 2 # Offset for columns based on row parity 176 | 177 | if direction == 'E': 178 | return [r, c + 1] 179 | elif direction == 'W': 180 | return [r, c - 1] 181 | elif direction == 'NE': 182 | return [r - 1, c + col_offset] 183 | elif direction == 'NW': 184 | return [r - 1, c - 1 + col_offset] 185 | elif direction == 'SE': 186 | return [r + 1, c + col_offset] 187 | elif direction == 'SW': 188 | return [r + 1, c - 1 + col_offset] 189 | return [r, c] 190 | 191 | def eval_straight_exit(self, cat_turn): 192 | """ 193 | Evaluate the cat's access to the nearest board edge along straight paths. 194 | """ 195 | distances = [] 196 | directions = ['E', 'W', 'NE', 'NW', 'SE', 'SW'] 197 | n = CatTrapGame.size 198 | for dir in directions: 199 | distance = 0 200 | r, c = self.cat 201 | while not (r < 0 or r >= n or c < 0 or c >= n): 202 | if self.hexgrid[r, c] == BLOCKED_TILE: 203 | distance += n # Increase cost for blocked paths 204 | break 205 | distance += 1 206 | r, c = self.get_target_position([r, c], dir) 207 | distances.append(distance) 208 | 209 | distances.sort() # Ascending order, so distances[0] is the best 210 | return CatTrapGame.size - (distances[0] if cat_turn else distances[1]) 211 | 212 | def eval_custom(self, cat_turn): 213 | """ 214 | Custom evaluation function combining moves, proximity, and progress penalties. 215 | """ 216 | n = CatTrapGame.size 217 | # Move Score 218 | move_score = self.eval_moves(cat_turn) / 6.0 # Normalize for max 6 moves 219 | 220 | # Proximity Score 221 | proximity_score = self.eval_straight_exit(cat_turn) / n # Normalize 222 | 223 | # Calculate Penalty based on euclidean distance to center 224 | center_row, center_col = n // 2, n // 2 225 | cat_row, cat_col = self.cat 226 | distance = ((cat_row - center_row) ** 2 + (cat_col - center_col) ** 2) ** 0.5 227 | 228 | max_penalty = n * 0.75 229 | penalty = max_penalty - distance 230 | penalty = penalty / max_penalty # Normalize 231 | if not cat_turn: 232 | penalty += 0.2 233 | 234 | # Combine Scores 235 | score = 0.2 * move_score + proximity_score - 0.5 * penalty 236 | 237 | return score 238 | 239 | # ===================== Intelligent Agents ===================== 240 | """ 241 | Intelligent Agents for the Cat Trap game. These agents examine the game 242 | state, and return the new position of the cat (a move). 243 | Two special return values for failure may be returned (timeout or trapped). 244 | 245 | Parameters: 246 | - random_cat: A random move for the cat. 247 | - minimax: Use the Minimax algorithm. 248 | - alpha_beta: Use Alpha-Beta Pruning. 249 | - depth_limited: Use Depth-Limited Search. 250 | - max_depth: Maximum depth to explore for Depth-Limited Search. 251 | - iterative_deepening: Use Iterative Deepening. 252 | - allotted_time: Maximum time in seconds for the cat to respond. 253 | 254 | If no algorithm is selected, the cat gives up (as if trapped). 255 | """ 256 | 257 | def select_cat_move(self, random_cat, minimax, alpha_beta, depth_limited, 258 | max_depth, iterative_deepening, allotted_time): 259 | """Select a move for the cat based on the chosen algorithm.""" 260 | CatTrapGame.start_time = time.time() 261 | CatTrapGame.deadline = CatTrapGame.start_time + allotted_time 262 | CatTrapGame.terminated = False 263 | CatTrapGame.max_depth = float('inf') 264 | CatTrapGame.reached_max_depth = False 265 | move = self.cat 266 | 267 | if VERBOSE: 268 | print('\n======= NEW MOVE =======') 269 | 270 | if random_cat: 271 | move = self.random_cat_move() 272 | elif minimax: 273 | # Select a move using the Minimax algorithm. 274 | move = self.alpha_beta() if alpha_beta else self.minimax() 275 | elif depth_limited: 276 | # Select a move using Depth-Limited Search. 277 | CatTrapGame.max_depth = max_depth 278 | move = self.alpha_beta() if alpha_beta else self.minimax() 279 | elif iterative_deepening: 280 | # Select a move using the Iterative Deepening algorithm. 281 | move = self.iterative_deepening(alpha_beta) 282 | 283 | elapsed_time = (time.time() - CatTrapGame.start_time) * 1000 284 | if VERBOSE: 285 | print(f'Elapsed time: {elapsed_time:.3f}ms ') 286 | print(f'New cat coordinates: {move}') 287 | temp = copy.deepcopy(self) 288 | if move != TIMEOUT: 289 | temp.move_cat(move) 290 | temp.print_hexgrid() 291 | return move 292 | 293 | def random_cat_move(self): 294 | """Randomly select a move for the cat.""" 295 | moves = self.get_cat_moves() 296 | if moves: 297 | return random.choice(moves) 298 | return self.cat 299 | 300 | def max_value(self, depth): 301 | """ 302 | Calculate the maximum value for the current game in the Minimax algorithm. 303 | """ 304 | if self.time_left() < LAST_CALL_MS: 305 | CatTrapGame.terminated = True 306 | return TIMEOUT, 0 307 | 308 | legal_moves = self.get_cat_moves() # Possible directions: E, W, NE, NW, SE, SW 309 | if not legal_moves: 310 | max_turns = 2 * (CatTrapGame.size ** 2) 311 | utility = (max_turns - depth) * (-500) # Utility for cat's defeat 312 | return self.cat, utility 313 | 314 | if depth == CatTrapGame.max_depth: 315 | CatTrapGame.reached_max_depth = True 316 | return self.cat, self.evaluation(cat_turn = True) 317 | 318 | best_value = float('-inf') 319 | best_move = legal_moves[0] 320 | for move in legal_moves: 321 | next_game = copy.deepcopy(self) 322 | next_game.apply_move(move, cat_turn = True) 323 | value = next_game.min_value(depth + 1) 324 | 325 | if CatTrapGame.terminated: 326 | return TIMEOUT, 0 327 | 328 | if value > best_value: 329 | best_value = value 330 | best_move = move 331 | 332 | return best_move, best_value 333 | 334 | def min_value(self, depth): 335 | """ 336 | Calculate the minimum value for the current game in the Minimax algorithm. 337 | 338 | Unlike max_value, min_value does not iterate over specific 339 | directions (E, W, NE, NW, etc.). Instead, it examines every 340 | possible free tile on the board. 341 | """ 342 | if self.time_left() < LAST_CALL_MS: 343 | CatTrapGame.terminated = True 344 | return 0 345 | 346 | # Check if terminal state 347 | r, c = self.cat 348 | n = CatTrapGame.size 349 | if ( 350 | r == 0 or r == n - 1 or 351 | c == 0 or c == n - 1 352 | ): 353 | max_turns = 2 * (CatTrapGame.size ** 2) 354 | return (max_turns - depth) * (500) # Utility for cat's victory 355 | 356 | if depth == CatTrapGame.max_depth: 357 | CatTrapGame.reached_max_depth = True 358 | return self.evaluation(cat_turn = False) 359 | 360 | best_value = float('inf') 361 | 362 | # Iterate through all legal moves for the player (empty tiles) 363 | legal_moves = [list(rc) for rc in np.argwhere(self.hexgrid == EMPTY_TILE)] 364 | for move in legal_moves: 365 | next_game = copy.deepcopy(self) 366 | next_game.apply_move(move, cat_turn = False) 367 | _, value = next_game.max_value(depth + 1) 368 | 369 | if CatTrapGame.terminated: 370 | return 0 371 | 372 | best_value = min(best_value, value) 373 | 374 | return best_value 375 | 376 | def minimax(self): 377 | """ 378 | Perform the Minimax algorithm to determine the best move. 379 | """ 380 | best_move, _ = self.max_value(depth = 0) 381 | return best_move 382 | 383 | def alpha_beta_max_value(self, alpha, beta, depth): 384 | """ 385 | Calculate the maximum value for the current game state 386 | using Alpha-Beta pruning. 387 | """ 388 | if self.time_left() < LAST_CALL_MS: 389 | CatTrapGame.terminated = True 390 | return TIMEOUT, 0 391 | 392 | legal_moves = self.get_cat_moves() # Possible directions: E, W, NE, NW, SE, SW 393 | if not legal_moves: 394 | max_turns = 2 * (CatTrapGame.size ** 2) 395 | utility = (max_turns - depth) * (-500) # Utility for cat's defeat 396 | return self.cat, utility 397 | 398 | if depth == CatTrapGame.max_depth: 399 | CatTrapGame.reached_max_depth = True 400 | return self.cat, self.evaluation(cat_turn = True) 401 | 402 | best_value = float('-inf') 403 | best_move = legal_moves[0] 404 | for move in legal_moves: 405 | next_game = copy.deepcopy(self) 406 | next_game.apply_move(move, cat_turn = True) 407 | value = next_game.alpha_beta_min_value(alpha, beta, depth + 1) 408 | 409 | if CatTrapGame.terminated: 410 | return TIMEOUT, 0 411 | 412 | if value > best_value: 413 | best_value = value 414 | best_move = move 415 | 416 | if best_value >= beta: # Pruning 417 | return best_move, best_value 418 | alpha = max(alpha, best_value) 419 | 420 | return best_move, best_value 421 | 422 | def alpha_beta_min_value(self, alpha, beta, depth): 423 | """ 424 | Calculate the minimum value for the current game state 425 | using Alpha-Beta pruning. 426 | 427 | Unlike max_value, min_value does not iterate over specific 428 | directions (E, W, NE, NW, etc.). Instead, it examines every 429 | possible free tile on the board. 430 | """ 431 | if self.time_left() < LAST_CALL_MS: 432 | CatTrapGame.terminated = True 433 | return 0 434 | 435 | # Check if terminal state 436 | r, c = self.cat 437 | n = CatTrapGame.size 438 | if ( 439 | r == 0 or r == n - 1 or 440 | c == 0 or c == n - 1 441 | ): 442 | max_turns = 2 * (CatTrapGame.size ** 2) 443 | return (max_turns - depth) * (500) # Utility for cat's victory 444 | 445 | if depth == CatTrapGame.max_depth: 446 | CatTrapGame.reached_max_depth = True 447 | return self.evaluation(cat_turn = False) 448 | 449 | best_value = float('inf') 450 | 451 | # Iterate through all legal moves for the player (empty tiles) 452 | legal_moves = [list(rc) for rc in np.argwhere(self.hexgrid == EMPTY_TILE)] 453 | for move in legal_moves: 454 | next_game = copy.deepcopy(self) 455 | next_game.apply_move(move, cat_turn = False) 456 | _, value = next_game.alpha_beta_max_value(alpha, beta, depth + 1) 457 | 458 | if CatTrapGame.terminated: 459 | return 0 460 | 461 | best_value = min(best_value, value) 462 | 463 | if best_value <= alpha: # Pruning 464 | return best_value 465 | beta = min(beta, best_value) 466 | 467 | return best_value 468 | 469 | def alpha_beta(self): 470 | """ 471 | Perform the Alpha-Beta pruning algorithm to determine the best move. 472 | """ 473 | alpha = float('-inf') 474 | beta = float('inf') 475 | best_move, _ = self.alpha_beta_max_value(alpha, beta, depth = 0) 476 | return best_move 477 | 478 | def iterative_deepening(self, alpha_beta): 479 | """ 480 | Perform iterative deepening search with an option to use Alpha-Beta pruning. 481 | """ 482 | self.placeholder_warning() 483 | return self.random_cat_move() 484 | 485 | def placeholder_warning(self): 486 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 487 | print(f'{signs} {signs}') 488 | print(' WARNING') 489 | print('This is a temporary implementation using') 490 | print("the random algorithm. You're supposed to") 491 | print('write code to solve a challenge.') 492 | print('Did you run the wrong version of main.py?') 493 | print('Double-check its path.') 494 | print(f'{signs} {signs}') 495 | 496 | if __name__ == '__main__': 497 | signs = '⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️ ⚠️' 498 | print(f'\n{signs} {signs}') 499 | print(' WARNING') 500 | print('You ran cat_trap_algorithms.py') 501 | print('This file contains the AI algorithms') 502 | print('and classes for the intelligent cat.') 503 | print('Did you mean to run main.py?') 504 | print(f'{signs} {signs}\n') 505 | --------------------------------------------------------------------------------