diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/assignment3/Makefile b/assignment3/Makefile new file mode 100644 index 0000000..9fea87f --- /dev/null +++ b/assignment3/Makefile @@ -0,0 +1,5 @@ +all: report.pdf handin.zip +report.pdf: report.md + pandoc $< -o $@ --pdf-engine typst +handin.zip: *.py report.pdf + zip $@ $^ diff --git a/assignment3/handin.zip b/assignment3/handin.zip new file mode 100644 index 0000000..fde0a55 Binary files /dev/null and b/assignment3/handin.zip differ diff --git a/assignment3/report.md b/assignment3/report.md new file mode 100644 index 0000000..813e2c0 --- /dev/null +++ b/assignment3/report.md @@ -0,0 +1,212 @@ +--- +title: assignment 3 +date: 2025-10-21 +author: Fredrik Robertsen, Erlend Ulvund Skaarberg +--- + +## (a) code output + +### halving game output + +``` +The number is 5 and it is P1's turn +P1's action: -- +The number is 4 and it is P2's turn +P2's action: -- +The number is 3 and it is P1's turn +P1's action: /2 +The number is 1 and it is P2's turn +P2's action: -- +The number is 0 and P1 won +``` + +this is as expected (provided by the example code) + +### bucket game output + +``` +The state is (0, ['A', 'B', 'C']) and it is P1's turn +P1's action: B +The state is (1, [3, 1]) and it is P2's turn +P2's action: 1 +The state is (0, [1]) and P1's utility is 1 +``` + +the results follow the expected run-through of the minimax algorithm + +### tic tac toe output + +``` + | | +---+---+--- + | | +---+---+--- + | | + +It is P1's turn to move +P1's action: (0, 0) + + x | | +---+---+--- + | | +---+---+--- + | | + +It is P2's turn to move +P2's action: (0, 1) + + x | o | +---+---+--- + | | +---+---+--- + | | + +It is P1's turn to move +P1's action: (0, 2) + + x | o | x +---+---+--- + | | +---+---+--- + | | + +It is P2's turn to move +P2's action: (1, 1) + + x | o | x +---+---+--- + | o | +---+---+--- + | | + +It is P1's turn to move +P1's action: (1, 0) + + x | o | x +---+---+--- + x | o | +---+---+--- + | | + +It is P2's turn to move +P2's action: (2, 0) + + x | o | x +---+---+--- + x | o | +---+---+--- + o | | + +It is P1's turn to move +P1's action: (2, 1) + + x | o | x +---+---+--- + x | o | +---+---+--- + o | x | + +It is P2's turn to move +P2's action: (1, 2) + + x | o | x +---+---+--- + x | o | o +---+---+--- + o | x | + +It is P1's turn to move +P1's action: (2, 2) + + x | o | x +---+---+--- + x | o | o +---+---+--- + o | x | x + +The game is a draw +``` + +this is an optimal game, ending in a draw + +## (b) runtime improvement with alpha-beta pruning + +### results of our benchmarks + +- 8.7112 seconds without alpha-beta pruning + +- 0.0001 seconds with alpha-beta pruning + +the results are measured at the first step of the algorithms + +we can see a clear speed-up from using the alpha-beta pruning + +### benchmarking code + +``` +game = Game() +state = game.initial_state() +game.print(state) +record = False +while not game.is_terminal(state): + player = game.to_move(state) + start_time = time.time() + action = minimax_search(game, state) # The player whose turn it is is the MAX player + if not record: + print(f"Elapsed time (minimax): {time.time() - start_time:.4f} seconds") + record = True + print(f'P{player+1}\'s action: {action}') + assert action is not None + state = game.result(state, action) + game.print(state) + print() + +game = Game() +state = game.initial_state() +game.print(state) +record = False +while not game.is_terminal(state): + start_time = time.time() + player = game.to_move(state) + action = alpha_beta_search(game, state) # The player whose turn it is is the MAX player + if not record: + print(f"Elapsed time (alpha-beta-pruning): {time.time() - start_time:.9f} seconds") + record = False + print(f'P{player+1}\'s action: {action}') + assert action is not None + state = game.result(state, action) + game.print(state) + print() +``` + +## non-mandatory + +in the provided game state + +``` + x | o | o +---+---+--- + x | | +---+---+--- + | | +``` + +the minimax algorithm will choose the path that leads to victory, which could be +the middle point, or the lower left one. although, due to our implementation, it +will always pick the middle point because it sees it first (it is on a higher +row). this path also provides two paths to win. it is in such a sense even more +optimal than greedily choosing the lower left option, but both options lead to +a win and are as such considered equal. + +to make the algorithm choose the immediate path on the lower left, we can for +example have it greedily scan the board for such an option every recursive call +so as to discover the option of a quicke win. though this may not be more +performant due to the overhead of checking for this condition all the time, but +on this rare occasion we will at least avoid performing unecessary minimax +calls. + +another way to achieve this, which could also improve the performance, would be +to keep track of path lengths. this would also let it discover such an option. + +yet another way would be to implement the algorithm differently, as a BFS +algorithm that checks for the winning state before expanding it. diff --git a/assignment3/report.pdf b/assignment3/report.pdf new file mode 100644 index 0000000..ebcae09 Binary files /dev/null and b/assignment3/report.pdf differ diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..66d2e52 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1760878510, + "narHash": "sha256-K5Osef2qexezUfs0alLvZ7nQFTGS9DL2oTVsIXsqLgs=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "5e2a59a5b1a82f89f2c7e598302a9cacebb72a67", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..315ca39 --- /dev/null +++ b/flake.nix @@ -0,0 +1,24 @@ +{ + description = "introki"; + + inputs = { + nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; + }; + + outputs = + { self, nixpkgs, ... }: + let + system = "x86_64-linux"; + pkgs = import nixpkgs { inherit system; }; + in + { + devShells.${system}.default = pkgs.mkShell { + buildInputs = with pkgs; [ + zip + ]; + shellHook = '' + echo welcome! + ''; + }; + }; +}