Skip to content

Commit

Permalink
Rename lib to src
Browse files Browse the repository at this point in the history
  • Loading branch information
JBierenbroodspot committed Apr 19, 2023
1 parent 85c7de8 commit 2f49712
Show file tree
Hide file tree
Showing 7 changed files with 527 additions and 0 deletions.
Empty file added src/__init__.py
Empty file.
4 changes: 4 additions & 0 deletions src/algorithms/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# ----------------------------------------------------------------------------------------------------------------------
# SPDX-License-Identifier: BSD 3-Clause -
# Copyright (c) 2022 Jimmy Bierenbroodspot. -
# ----------------------------------------------------------------------------------------------------------------------
52 changes: 52 additions & 0 deletions src/algorithms/bogorithm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# ----------------------------------------------------------------------------------------------------------------------
# SPDX-License-Identifier: BSD 3-Clause -
# Copyright (c) 2022 Jimmy Bierenbroodspot. -
# ----------------------------------------------------------------------------------------------------------------------
"""A mastermind algorithm that tries to use the fundamentals of bogosort to solve a game of mastermind. It pops a random
combination from the pool of possible combinations and checks if it is the correct code, then repeats. This is obviously
horrifically inefficient.
This algorithm needs at best 1 guess and at most n^r, where n = the amount of objects and r = the sample size, guesses.
Because the probability of each amount of guesses to win the game (k) is equally distributed the average amount of
guesses needed by this algorithm is n^r/2. For a standard game of mastermind (n = 6 and r = 4) is this 6^4/2 = 1296/2 =
648 and since this number for most algorithms is around 3 or 4 makes bogorithm orders of magnitude worse than most
algorithms out there.
This could be improved on by reducing the incompatible codes from the pool of possible guesses. This makes it the
simple algorithm but with a random guess rather than the first pick from the pool.
"""
import typing
import random

import src.python.mastermind.mastermind as game
import initialization as init

# Overwrite game length to max amount of guesses.
init.GAME_LENGTH = 1296


def main() -> None:
game_simulation: typing.Generator[typing.Tuple[int, int, bool], game.Code, None]
answer: typing.Tuple[int, int, bool]
guess: game.Code
game_round: int = 0
possible_combinations: typing.List[game.Code] = init.get_combinations()

game_simulation = game.simulate_game(
init.COLOURS, init.GAME_LENGTH, init.GAME_WIDTH
)
for _ in game_simulation:
game_round += 1

guess = possible_combinations.pop(
random.randint(0, len(possible_combinations) - 1)
)
answer = game_simulation.send(guess)

if answer[2]:
print(f"Game won in {game_round} guesses!")
break
else:
print("Game lost.")


if __name__ == "__main__":
main()
56 changes: 56 additions & 0 deletions src/algorithms/initialization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# ----------------------------------------------------------------------------------------------------------------------
# SPDX-License-Identifier: BSD 3-Clause -
# Copyright (c) 2022 Jimmy Bierenbroodspot. -
# ----------------------------------------------------------------------------------------------------------------------
"""A module containing some constants, types and functions which are commonly shared between algorithms."""
import typing
import json

import scripts.generate_set as generate_set
import src.python.mastermind.mastermind as game

# These constants are declared here because they remain the same value in the entire module since I haven't implemented
# a way to customize them yet
GAME_WIDTH: int = 4
GAME_LENGTH: int = 8
COLOURS: typing.Tuple[int, ...] = tuple(num for num in range(6))

# Create generic Json type for ease of use, this has no actual functionality other than showing that something is a
# json serialized object.
Json: typing.Generic = typing.TypeVar("Json")


def get_combinations() -> Json:
"""Retrieves a list of all possible combinations from combinations.json.
:return: A Json serialized object with all possible combinations.
"""
json_io: typing.TextIO
json_string: str

# Generate dataset with possible combinations
generate_set.main()

with open("./combinations.json", "r") as json_io:
json_string = json_io.read()

return json.loads(json_string)


def reduce(
possible_combinations: Json, guess: game.Code, score: typing.Tuple[int, int]
) -> Json:
"""Compares the score of all combinations in possible_combinations against the given score.
:param possible_combinations: A list of possible combinations.
:param guess: A sequence containing integers.
:param score: A tuple containing 2 integers.
:return: A list
"""
# Using a list comprehension is absolutely useless here because game.compare_codes is a relatively expensive
# function. I just really like writing comprehensions.
return [
possible_combination
for possible_combination in possible_combinations
if score == game.compare_codes(guess, possible_combination)
]
116 changes: 116 additions & 0 deletions src/algorithms/koois.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# ----------------------------------------------------------------------------------------------------------------------
# SPDX-License-Identifier: BSD 3-Clause -
# Copyright (c) 2022 Jimmy Bierenbroodspot. -
# ----------------------------------------------------------------------------------------------------------------------
"""
Mastermind solving algorithm using B. Kooi's new strategy algorithm.
This code is developed using pseudocode found in the following article:
Kooi, B. (2005). Yet another mastermind strategy. ICGA Journal, 28(1), 13-20.
"""
import typing
import collections

import initialization as init
import src.python.mastermind.mastermind as game


def main() -> None:
game_simulation: typing.Generator[typing.Tuple[int, int, bool], game.Code, None]
guess: game.Code
answer: typing.Tuple[int, int, bool]
categories: typing.Set[game.Code]
partition_counts: typing.Dict[game.Code, int]
largest_category: game.Code
game_round: int = 0
combinations: init.Json = init.get_combinations()

game_simulation = game.simulate_game(
init.COLOURS, init.GAME_LENGTH, init.GAME_WIDTH
)
for _ in game_simulation:
game_round += 1

# Find all categories left in the combinations and count the partitions.
categories = get_all_categories(combinations)
partition_counts = {
category: get_partition_count(combinations, category)
for category in categories
}
# Find the largest category.
largest_category = max(partition_counts, key=partition_counts.get)
guess = next(
combination
for combination in combinations
if get_category(combination) == largest_category
)
answer = game_simulation.send(guess)
print(f"Guessed: {guess}; answer: {answer}")

# Check if the game is won
if answer[2]:
print(f"Game won in {game_round} guesses!")
break

combinations = init.reduce(combinations, guess, answer[:2])
else:
print("Game lost.")


def get_category(combination: game.Code) -> game.Code:
"""Decides the category of a combination. In most papers these categories are describes as: AAAA, AAAB, AABB, AABC
and ABCD.
:param combination: A Code combination of any width.
:return: A list that substitutes the letters in a category for integers in such that AABC == [0, 0, 1, 2].
"""
# Creates a list of values within the counter, we do not actually know to which number each value belongs.
combination_counts: typing.List[int] = sorted(
collections.Counter(combination).values(), reverse=True
)
category: game.Code = [] # This could've been a comprehension...

# Appends the amount each colour (number) appears in the combination.
for counter, combination_count in enumerate(combination_counts):
category.extend([counter] * combination_count)

return tuple(category)


def get_partition_count(
possible_combinations: init.Json, combination: game.Code
) -> int:
"""Finds the amount a given combination can be partitioned in by comparing the codes and calculating the possible
answer.
:param possible_combinations: A list with unique Code combinations.
:param combination: A Code combination.
:return: The amount of possible partitions.
"""
# Comprehensions are fun, aren't they?
return len(
collections.Counter(
[
game.compare_codes(combination, possible_combination)
for possible_combination in possible_combinations
]
)
)


def get_all_categories(
possible_combinations: init.Json,
) -> typing.Set[typing.Tuple[int]]:
"""Gets all categories in a list of combinations.
:param possible_combinations: A list of possible combinations.
:return: A set with one of every available category.
"""
return {
tuple(get_category(possible_combination))
for possible_combination in possible_combinations
}


if __name__ == "__main__":
main()
44 changes: 44 additions & 0 deletions src/algorithms/simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# ----------------------------------------------------------------------------------------------------------------------
# SPDX-License-Identifier: BSD 3-Clause -
# Copyright (c) 2022 Jimmy Bierenbroodspot. -
# ----------------------------------------------------------------------------------------------------------------------
"""
Mastermind solving algorithm using L. Sterling's and E. Shapiro's simple algorithm.
This code is developed using code found in the following book:
Sterling, L., & Shapiro, E. (1994). The art of Prolog: advanced programming techniques (2nd ed.). MIT Press.
"""
from __future__ import annotations
import typing

import src.python.mastermind.mastermind as game
import initialization as init


def main() -> None:
game_simulation: typing.Generator[typing.Tuple[int, int, bool], game.Code, None]
guess: game.Code
answer: typing.Tuple[int, int, bool]
game_round: int = 0
possible_combinations: init.Json = init.get_combinations()

game_simulation = game.simulate_game(init.COLOURS, init.GAME_LENGTH, init.GAME_WIDTH)
for _ in game_simulation:
game_round += 1

guess = possible_combinations[0]
answer = game_simulation.send(guess)
print(f"Guessed: {guess}; answer: {answer}")

# Check if the game is won
if answer[2]:
print(f'Game won in {game_round + 1} guesses!')
break

possible_combinations = init.reduce(possible_combinations, guess, answer[:2])
else:
print('Game lost.')


if __name__ == "__main__":
main()
Loading

0 comments on commit 2f49712

Please sign in to comment.