├── Nim ├── play.py ├── visual.py └── nim.py ├── Attention ├── analysis.md └── mask.py ├── README.md ├── Knights ├── puzzle.py └── logic.py ├── Crossword ├── crossword.py └── generate.py ├── Tic-Tac-Toe ├── runner.py └── tictactoe.py ├── Parser └── parser.py ├── Traffic └── traffic.py ├── Project 0a: Degrees ├── PageRank └── pagerank.py ├── Heredity └── heredity.py ├── Shopping └── shopping.py └── Minesweeper ├── runner.py └── minesweeper.py /Nim/play.py: -------------------------------------------------------------------------------- 1 | from nim import train, play 2 | 3 | ai = train(10000) 4 | play(ai) 5 | -------------------------------------------------------------------------------- /Attention/analysis.md: -------------------------------------------------------------------------------- 1 | # Analysis 2 | 3 | ## Layer TODO, Head TODO 4 | 5 | TODO 6 | 7 | Example Sentences: 8 | - TODO 9 | - TODO 10 | 11 | ## Layer TODO, Head TODO 12 | 13 | TODO 14 | 15 | Example Sentences: 16 | - TODO 17 | - TODO 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CS50-s-Introduction-to-Artificial-Intelligence-with-Python 2 | CS50's Introduction to Artificial Intelligence with Python 3 | ![image](https://github.com/Technophile-1/CS50-s-Introduction-to-Artificial-Intelligence-with-Python/assets/128832861/ae6513d0-3e79-4339-a6fd-e3215e02a728) 4 | -------------------------------------------------------------------------------- /Knights/puzzle.py: -------------------------------------------------------------------------------- 1 | from logic import * 2 | 3 | AKnight = Symbol("A is a Knight") 4 | AKnave = Symbol("A is a Knave") 5 | 6 | BKnight = Symbol("B is a Knight") 7 | BKnave = Symbol("B is a Knave") 8 | 9 | CKnight = Symbol("C is a Knight") 10 | CKnave = Symbol("C is a Knave") 11 | 12 | # Puzzle 0 13 | # A says "I am both a knight and a knave." 14 | sentence = And(AKnight, AKnave) 15 | knowledge0 = And( 16 | # game knowledge 17 | # each character is either a knight or a knave, 18 | Or(AKnight, AKnave), 19 | # but not both 20 | Biconditional(AKnight, Not(AKnave)), 21 | # if a knight states a sentence, then it is true 22 | Implication(AKnight, sentence), 23 | # if a knave states a sentence, then the sentence is false 24 | Implication(AKnave, Not(sentence)), 25 | ) 26 | 27 | # Puzzle 1 28 | # A says "We are both knaves." 29 | # B says nothing. 30 | sentence = And(AKnave, BKnave) 31 | 32 | knowledge1 = And( 33 | # game knowledge 34 | # each character is either a knight or a knave, 35 | Or(AKnight, AKnave), 36 | Or(BKnight, BKnave), 37 | # but not both 38 | Biconditional(AKnight, Not(AKnave)), 39 | Biconditional(BKnight, Not(BKnave)), 40 | # if a knight states a sentence, then it is true 41 | Implication(AKnight, sentence), 42 | # if a knave states a sentence, then the sentence is false 43 | Implication(AKnave, Not(sentence)), 44 | ) 45 | 46 | # Puzzle 2 47 | # A says "We are the same kind." 48 | # B says "We are of different kinds." 49 | sentenceA = Or(And(AKnight, BKnight), And(AKnave, BKnave)) 50 | sentenceB = Or(Biconditional(AKnight, BKnave), Biconditional(AKnave, BKnight)) 51 | 52 | knowledge2 = And( 53 | # game knowledge 54 | # each character is either a knight or a knave, 55 | # each character is either a knight or a knave, 56 | Or(AKnight, AKnave), 57 | Or(BKnight, BKnave), 58 | # but not both 59 | Biconditional(AKnight, Not(AKnave)), 60 | Biconditional(BKnight, Not(BKnave)), 61 | # if a knight states a sentence, then it is true 62 | Implication(AKnight, sentenceA), 63 | # if a knave states a sentence, then the sentence is false 64 | Implication(AKnave, Not(sentenceA)), 65 | Implication(BKnight, sentenceB), 66 | Implication(BKnave, Not(sentenceB)), 67 | ) 68 | 69 | # Puzzle 3 70 | # A says either "I am a knight." or "I am a knave.", but you don't know which. 71 | # B says "A said 'I am a knave'." 72 | # B says "C is a knave." 73 | # C says "A is a knight." 74 | 75 | sentenceA_1 = AKnight 76 | sentenceA_2 = AKnave 77 | 78 | sentenceB_2 = CKnave 79 | sentenceC = AKnight 80 | 81 | knowledge3 = And( 82 | # game knowledge 83 | # each character is either a knight or a knave, 84 | Or(AKnight, AKnave), 85 | Or(BKnight, BKnave), 86 | Or(CKnight, CKnave), 87 | # but not both 88 | Biconditional(AKnight, Not(AKnave)), 89 | Biconditional(BKnight, Not(BKnave)), 90 | Biconditional(CKnight, Not(CKnave)), 91 | 92 | # A says 93 | Or( 94 | And( 95 | Implication(AKnight, sentenceA_1), 96 | Implication(AKnave, Not(sentenceA_1)), 97 | ), 98 | And( 99 | Implication(AKnight, sentenceA_2), 100 | Implication(AKnave, Not(sentenceA_2)), 101 | ) 102 | ), 103 | 104 | # B says 105 | # B says "A said 'I am a knave'.". If B is telling the truth (BKnight), then 106 | # it must be the fact that if a is a knave, it must but a knight (a paradox) 107 | Implication(BKnight, Implication(AKnave, AKnight)), 108 | 109 | Implication(BKnight, sentenceB_2), 110 | Implication(BKnave, Not(sentenceB_2)), 111 | 112 | # C Says 113 | # if a knight states a sentence, then it is true 114 | Implication(CKnight, sentenceC), 115 | # if a knave states a sentence, then the sentence is false 116 | Implication(CKnave, Not(sentenceC)), 117 | 118 | 119 | ) 120 | 121 | 122 | def main(): 123 | symbols = [AKnight, AKnave, BKnight, BKnave, CKnight, CKnave] 124 | puzzles = [ 125 | ("Puzzle 0", knowledge0), 126 | ("Puzzle 1", knowledge1), 127 | ("Puzzle 2", knowledge2), 128 | ("Puzzle 3", knowledge3) 129 | ] 130 | for puzzle, knowledge in puzzles: 131 | print(puzzle) 132 | if len(knowledge.conjuncts) == 0: 133 | print(" Not yet implemented.") 134 | else: 135 | for symbol in symbols: 136 | if model_check(knowledge, symbol): 137 | print(f" {symbol}") 138 | 139 | 140 | if __name__ == "__main__": 141 | main() 142 | -------------------------------------------------------------------------------- /Attention/mask.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import tensorflow as tf 3 | 4 | from PIL import Image, ImageDraw, ImageFont 5 | from transformers import AutoTokenizer, TFBertForMaskedLM 6 | 7 | # Pre-trained masked language model 8 | MODEL = "bert-base-uncased" 9 | 10 | # Number of predictions to generate 11 | K = 3 12 | 13 | # Constants for generating attention diagrams 14 | FONT = ImageFont.truetype("assets/fonts/OpenSans-Regular.ttf", 28) 15 | GRID_SIZE = 40 16 | PIXELS_PER_WORD = 200 17 | 18 | 19 | def main(): 20 | text = input("Text: ") 21 | 22 | # Tokenize input 23 | tokenizer = AutoTokenizer.from_pretrained(MODEL) 24 | inputs = tokenizer(text, return_tensors="tf") 25 | mask_token_index = get_mask_token_index(tokenizer.mask_token_id, inputs) 26 | if mask_token_index is None: 27 | sys.exit(f"Input must include mask token {tokenizer.mask_token}.") 28 | 29 | # Use model to process input 30 | model = TFBertForMaskedLM.from_pretrained(MODEL) 31 | result = model(**inputs, output_attentions=True) 32 | 33 | # Generate predictions 34 | mask_token_logits = result.logits[0, mask_token_index] 35 | top_tokens = tf.math.top_k(mask_token_logits, K).indices.numpy() 36 | for token in top_tokens: 37 | print(text.replace(tokenizer.mask_token, tokenizer.decode([token]))) 38 | 39 | # Visualize attentions 40 | visualize_attentions(inputs.tokens(), result.attentions) 41 | 42 | 43 | def get_mask_token_index(mask_token_id, inputs): 44 | """ 45 | Return the index of the token with the specified `mask_token_id`, or 46 | `None` if not present in the `inputs`. 47 | """ 48 | for i, token in enumerate(inputs.input_ids[0]): 49 | if token == mask_token_id: 50 | return i 51 | 52 | return None 53 | 54 | 55 | def get_color_for_attention_score(attention_score): 56 | """ 57 | Return a tuple of three integers representing a shade of gray for the 58 | given `attention_score`. Each value should be in the range [0, 255]. 59 | """ 60 | return tuple([int(_ * attention_score) for _ in [255, 255, 255]]) 61 | 62 | 63 | def visualize_attentions(tokens, attentions): 64 | """ 65 | Produce a graphical representation of self-attention scores. 66 | 67 | For each attention layer, one diagram should be generated for each 68 | attention head in the layer. Each diagram should include the list of 69 | `tokens` in the sentence. The filename for each diagram should 70 | include both the layer number (starting count from 1) and head number 71 | (starting count from 1). 72 | """ 73 | for i, layer in enumerate(attentions): 74 | for k in range(len(layer[0])): 75 | layer_number, head_number = i + 1, k + 1 76 | generate_diagram(layer_number, head_number, tokens, attentions[i][0][k]) 77 | 78 | 79 | def generate_diagram(layer_number, head_number, tokens, attention_weights): 80 | """ 81 | Generate a diagram representing the self-attention scores for a single 82 | attention head. The diagram shows one row and column for each of the 83 | `tokens`, and cells are shaded based on `attention_weights`, with lighter 84 | cells corresponding to higher attention scores. 85 | 86 | The diagram is saved with a filename that includes both the `layer_number` 87 | and `head_number`. 88 | """ 89 | # Create new image 90 | image_size = GRID_SIZE * len(tokens) + PIXELS_PER_WORD 91 | img = Image.new("RGBA", (image_size, image_size), "black") 92 | draw = ImageDraw.Draw(img) 93 | 94 | # Draw each token onto the image 95 | for i, token in enumerate(tokens): 96 | # Draw token columns 97 | token_image = Image.new("RGBA", (image_size, image_size), (0, 0, 0, 0)) 98 | token_draw = ImageDraw.Draw(token_image) 99 | token_draw.text( 100 | (image_size - PIXELS_PER_WORD, PIXELS_PER_WORD + i * GRID_SIZE), 101 | token, 102 | fill="white", 103 | font=FONT, 104 | ) 105 | token_image = token_image.rotate(90) 106 | img.paste(token_image, mask=token_image) 107 | 108 | # Draw token rows 109 | _, _, width, _ = draw.textbbox((0, 0), token, font=FONT) 110 | draw.text( 111 | (PIXELS_PER_WORD - width, PIXELS_PER_WORD + i * GRID_SIZE), 112 | token, 113 | fill="white", 114 | font=FONT, 115 | ) 116 | 117 | # Draw each word 118 | for i in range(len(tokens)): 119 | y = PIXELS_PER_WORD + i * GRID_SIZE 120 | for j in range(len(tokens)): 121 | x = PIXELS_PER_WORD + j * GRID_SIZE 122 | color = get_color_for_attention_score(attention_weights[i][j]) 123 | draw.rectangle((x, y, x + GRID_SIZE, y + GRID_SIZE), fill=color) 124 | 125 | # Save image 126 | img.save(f"Attention_Layer{layer_number}_Head{head_number}.png") 127 | 128 | 129 | if __name__ == "__main__": 130 | main() 131 | -------------------------------------------------------------------------------- /Crossword/crossword.py: -------------------------------------------------------------------------------- 1 | class Variable(): 2 | 3 | ACROSS = "across" 4 | DOWN = "down" 5 | 6 | def __init__(self, i, j, direction, length): 7 | """Create a new variable with starting point, direction, and length.""" 8 | self.i = i 9 | self.j = j 10 | self.direction = direction 11 | self.length = length 12 | self.cells = [] 13 | for k in range(self.length): 14 | self.cells.append( 15 | (self.i + (k if self.direction == Variable.DOWN else 0), 16 | self.j + (k if self.direction == Variable.ACROSS else 0)) 17 | ) 18 | 19 | def __hash__(self): 20 | return hash((self.i, self.j, self.direction, self.length)) 21 | 22 | def __eq__(self, other): 23 | return ( 24 | (self.i == other.i) and 25 | (self.j == other.j) and 26 | (self.direction == other.direction) and 27 | (self.length == other.length) 28 | ) 29 | 30 | def __str__(self): 31 | return f"({self.i}, {self.j}) {self.direction} : {self.length}" 32 | 33 | def __repr__(self): 34 | direction = repr(self.direction) 35 | return f"Variable({self.i}, {self.j}, {direction}, {self.length})" 36 | 37 | 38 | class Crossword(): 39 | 40 | def __init__(self, structure_file, words_file): 41 | 42 | # Determine structure of crossword 43 | with open(structure_file) as f: 44 | contents = f.read().splitlines() 45 | self.height = len(contents) 46 | self.width = max(len(line) for line in contents) 47 | 48 | self.structure = [] 49 | for i in range(self.height): 50 | row = [] 51 | for j in range(self.width): 52 | if j >= len(contents[i]): 53 | row.append(False) 54 | elif contents[i][j] == "_": 55 | row.append(True) 56 | else: 57 | row.append(False) 58 | self.structure.append(row) 59 | 60 | # Save vocabulary list 61 | with open(words_file) as f: 62 | self.words = set(f.read().upper().splitlines()) 63 | 64 | # Determine variable set 65 | self.variables = set() 66 | for i in range(self.height): 67 | for j in range(self.width): 68 | 69 | # Vertical words 70 | starts_word = ( 71 | self.structure[i][j] 72 | and (i == 0 or not self.structure[i - 1][j]) 73 | ) 74 | if starts_word: 75 | length = 1 76 | for k in range(i + 1, self.height): 77 | if self.structure[k][j]: 78 | length += 1 79 | else: 80 | break 81 | if length > 1: 82 | self.variables.add(Variable( 83 | i=i, j=j, 84 | direction=Variable.DOWN, 85 | length=length 86 | )) 87 | 88 | # Horizontal words 89 | starts_word = ( 90 | self.structure[i][j] 91 | and (j == 0 or not self.structure[i][j - 1]) 92 | ) 93 | if starts_word: 94 | length = 1 95 | for k in range(j + 1, self.width): 96 | if self.structure[i][k]: 97 | length += 1 98 | else: 99 | break 100 | if length > 1: 101 | self.variables.add(Variable( 102 | i=i, j=j, 103 | direction=Variable.ACROSS, 104 | length=length 105 | )) 106 | 107 | # Compute overlaps for each word 108 | # For any pair of variables v1, v2, their overlap is either: 109 | # None, if the two variables do not overlap; or 110 | # (i, j), where v1's ith character overlaps v2's jth character 111 | self.overlaps = dict() 112 | for v1 in self.variables: 113 | for v2 in self.variables: 114 | if v1 == v2: 115 | continue 116 | cells1 = v1.cells 117 | cells2 = v2.cells 118 | intersection = set(cells1).intersection(cells2) 119 | if not intersection: 120 | self.overlaps[v1, v2] = None 121 | else: 122 | intersection = intersection.pop() 123 | self.overlaps[v1, v2] = ( 124 | cells1.index(intersection), 125 | cells2.index(intersection) 126 | ) 127 | 128 | def neighbors(self, var): 129 | """Given a variable, return set of overlapping variables.""" 130 | return set( 131 | v for v in self.variables 132 | if v != var and self.overlaps[v, var] 133 | ) 134 | -------------------------------------------------------------------------------- /Tic-Tac-Toe/runner.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import sys 3 | import time 4 | 5 | import tictactoe as ttt 6 | 7 | pygame.init() 8 | size = width, height = 600, 400 9 | 10 | # Colors 11 | black = (0, 0, 0) 12 | white = (255, 255, 255) 13 | 14 | screen = pygame.display.set_mode(size) 15 | 16 | mediumFont = pygame.font.Font("OpenSans-Regular.ttf", 28) 17 | largeFont = pygame.font.Font("OpenSans-Regular.ttf", 40) 18 | moveFont = pygame.font.Font("OpenSans-Regular.ttf", 60) 19 | 20 | user = None 21 | board = ttt.initial_state() 22 | ai_turn = False 23 | 24 | while True: 25 | 26 | for event in pygame.event.get(): 27 | if event.type == pygame.QUIT: 28 | sys.exit() 29 | 30 | screen.fill(black) 31 | 32 | # Let user choose a player. 33 | if user is None: 34 | 35 | # Draw title 36 | title = largeFont.render("Play Tic-Tac-Toe", True, white) 37 | titleRect = title.get_rect() 38 | titleRect.center = ((width / 2), 50) 39 | screen.blit(title, titleRect) 40 | 41 | # Draw buttons 42 | playXButton = pygame.Rect((width / 8), (height / 2), width / 4, 50) 43 | playX = mediumFont.render("Play as X", True, black) 44 | playXRect = playX.get_rect() 45 | playXRect.center = playXButton.center 46 | pygame.draw.rect(screen, white, playXButton) 47 | screen.blit(playX, playXRect) 48 | 49 | playOButton = pygame.Rect(5 * (width / 8), (height / 2), width / 4, 50) 50 | playO = mediumFont.render("Play as O", True, black) 51 | playORect = playO.get_rect() 52 | playORect.center = playOButton.center 53 | pygame.draw.rect(screen, white, playOButton) 54 | screen.blit(playO, playORect) 55 | 56 | # Check if button is clicked 57 | click, _, _ = pygame.mouse.get_pressed() 58 | if click == 1: 59 | mouse = pygame.mouse.get_pos() 60 | if playXButton.collidepoint(mouse): 61 | time.sleep(0.2) 62 | user = ttt.X 63 | elif playOButton.collidepoint(mouse): 64 | time.sleep(0.2) 65 | user = ttt.O 66 | 67 | else: 68 | 69 | # Draw game board 70 | tile_size = 80 71 | tile_origin = (width / 2 - (1.5 * tile_size), 72 | height / 2 - (1.5 * tile_size)) 73 | tiles = [] 74 | for i in range(3): 75 | row = [] 76 | for j in range(3): 77 | rect = pygame.Rect( 78 | tile_origin[0] + j * tile_size, 79 | tile_origin[1] + i * tile_size, 80 | tile_size, tile_size 81 | ) 82 | pygame.draw.rect(screen, white, rect, 3) 83 | 84 | if board[i][j] != ttt.EMPTY: 85 | move = moveFont.render(board[i][j], True, white) 86 | moveRect = move.get_rect() 87 | moveRect.center = rect.center 88 | screen.blit(move, moveRect) 89 | row.append(rect) 90 | tiles.append(row) 91 | 92 | game_over = ttt.terminal(board) 93 | player = ttt.player(board) 94 | 95 | # Show title 96 | if game_over: 97 | winner = ttt.winner(board) 98 | if winner is None: 99 | title = f"Game Over: Tie." 100 | else: 101 | title = f"Game Over: {winner} wins." 102 | elif user == player: 103 | title = f"Play as {user}" 104 | else: 105 | title = f"Computer thinking..." 106 | title = largeFont.render(title, True, white) 107 | titleRect = title.get_rect() 108 | titleRect.center = ((width / 2), 30) 109 | screen.blit(title, titleRect) 110 | 111 | # Check for AI move 112 | if user != player and not game_over: 113 | if ai_turn: 114 | time.sleep(0.5) 115 | move = ttt.minimax(board) 116 | board = ttt.result(board, move) 117 | ai_turn = False 118 | else: 119 | ai_turn = True 120 | 121 | # Check for a user move 122 | click, _, _ = pygame.mouse.get_pressed() 123 | if click == 1 and user == player and not game_over: 124 | mouse = pygame.mouse.get_pos() 125 | for i in range(3): 126 | for j in range(3): 127 | if (board[i][j] == ttt.EMPTY and tiles[i][j].collidepoint(mouse)): 128 | board = ttt.result(board, (i, j)) 129 | 130 | if game_over: 131 | againButton = pygame.Rect(width / 3, height - 65, width / 3, 50) 132 | again = mediumFont.render("Play Again", True, black) 133 | againRect = again.get_rect() 134 | againRect.center = againButton.center 135 | pygame.draw.rect(screen, white, againButton) 136 | screen.blit(again, againRect) 137 | click, _, _ = pygame.mouse.get_pressed() 138 | if click == 1: 139 | mouse = pygame.mouse.get_pos() 140 | if againButton.collidepoint(mouse): 141 | time.sleep(0.2) 142 | user = None 143 | board = ttt.initial_state() 144 | ai_turn = False 145 | 146 | pygame.display.flip() 147 | -------------------------------------------------------------------------------- /Parser/parser.py: -------------------------------------------------------------------------------- 1 | from functools import cache 2 | from typing import List 3 | 4 | import nltk 5 | import sys 6 | 7 | TERMINALS = """ 8 | Adj -> "country" | "dreadful" | "enigmatical" | "little" | "moist" | "red" 9 | Adv -> "down" | "here" | "never" 10 | Conj -> "and" | "until" 11 | Det -> "a" | "an" | "his" | "my" | "the" 12 | N -> "armchair" | "companion" | "day" | "door" | "hand" | "he" | "himself" 13 | N -> "holmes" | "home" | "i" | "mess" | "paint" | "palm" | "pipe" | "she" 14 | N -> "smile" | "thursday" | "walk" | "we" | "word" 15 | P -> "at" | "before" | "in" | "of" | "on" | "to" 16 | V -> "arrived" | "came" | "chuckled" | "had" | "lit" | "said" | "sat" 17 | V -> "smiled" | "tell" | "were" 18 | """ 19 | 20 | NONTERMINALS = """ 21 | S -> NP VP | NP VP Conj VP | NP VP Conj S | NP Adv VP 22 | NP -> N | Det NP | N PP | Det NP PP | Adj NP 23 | VP -> V | V Adv | V PP | V NP | V NP Adv | V NP PP 24 | PP -> P NP | Conj S 25 | """ 26 | 27 | grammar = nltk.CFG.fromstring(NONTERMINALS + TERMINALS) 28 | parser = nltk.ChartParser(grammar) 29 | 30 | 31 | def main(): 32 | # If filename specified, read sentence from file 33 | if len(sys.argv) == 2: 34 | with open(sys.argv[1]) as f: 35 | s = f.read() 36 | 37 | # Otherwise, get sentence as input 38 | else: 39 | s = input("Sentence: ") 40 | 41 | # Convert input into list of words 42 | s = preprocess(s) 43 | 44 | # Attempt to parse sentence 45 | try: 46 | trees = list(parser.parse(s)) 47 | except ValueError as e: 48 | print(e) 49 | return 50 | if not trees: 51 | print("Could not parse sentence.") 52 | return 53 | 54 | # Print each tree with noun phrase chunks 55 | for tree in trees: 56 | tree.pretty_print() 57 | 58 | print("Noun Phrase Chunks") 59 | for np in np_chunk(tree): 60 | print(" ".join(np.flatten())) 61 | 62 | 63 | def preprocess(sentence): 64 | """ 65 | Convert `sentence` to a list of its words. 66 | Pre-process sentence by converting all characters to lowercase 67 | and removing any word that does not contain at least one alphabetic 68 | character. 69 | """ 70 | sentence = nltk.tokenize.word_tokenize(sentence) 71 | ans = [] 72 | for word in sentence: 73 | word = word.lower() 74 | contains_alphabet = any(ch.isalpha() for ch in word) 75 | if contains_alphabet: 76 | ans.append(word) 77 | return ans 78 | 79 | 80 | def check(subtree): 81 | """ 82 | Return True if any child of 'subtree' has the label "NP" 83 | ref: https://github.com/Rajil1213/cs50AI/blob/master/Week6/parser/parser.py 84 | """ 85 | 86 | # if the subtree itself is an NP, then return True 87 | if subtree.label() == "NP": 88 | return True 89 | 90 | # if the subtree has only one child, then its probably a terminal node 91 | # in which case return False 92 | # but the label must not be 'S' since 93 | # S can contain only one child when S -> VP is followed by the parse tree 94 | if len(subtree) == 1 and subtree.label() != 'S': 95 | return False 96 | 97 | # otherwise go further into the subtree 98 | # and evaluate each subsubtree there 99 | for subsubtree in subtree: 100 | 101 | if check(subsubtree): 102 | return True 103 | 104 | return False 105 | 106 | 107 | def np_chunk(tree: nltk.tree.Tree): 108 | """ 109 | Return a list of all noun phrase chunks in the sentence tree. 110 | A noun phrase chunk is defined as any subtree of the sentence 111 | whose label is "NP" that does not itself contain any other 112 | noun phrases as subtrees. 113 | """ 114 | 115 | # @cache 116 | # def findNP(np_tree: nltk.tree.Tree) -> List[nltk.tree.Tree]: 117 | # """ 118 | # Find the minimum subtrees with 'NP' tag of one tree whose tag is 'NP' 119 | # """ 120 | # np_lst = [] 121 | # sub_np = False 122 | # for i in range(len(np_tree)): 123 | # sub_tree = np_tree[i] 124 | # if sub_tree and sub_tree.label() == 'NP': 125 | # sub_np = True 126 | # np_lst.extend(findNP(sub_tree)) 127 | # if not sub_np: 128 | # np_lst.append(np_tree) 129 | # return np_lst 130 | # ans = [] 131 | # for i in range(len(tree)): 132 | # sub_tree = tree[i] 133 | # if sub_tree and sub_tree.label() == 'NP': 134 | # ans.extend(findNP(sub_tree)) 135 | # return ans 136 | 137 | chunks = [] 138 | contains = False 139 | for subtree in tree: 140 | 141 | # get the label of the subtree defined by the grammar 142 | node = subtree.label() 143 | 144 | # check if this tree contains a subtree with 'NP' 145 | # if not check another subtree 146 | contains = check(subtree) 147 | if not contains: 148 | continue 149 | 150 | # if the node is a NP or VP or S, then 151 | # go further into the tree to check for noun phrase chunks 152 | # at each point take the list of trees returned and 153 | # append each to the actual chunks' list in the parent 154 | if node == "NP" or node == "VP" or node == "S": 155 | subsubtree = np_chunk(subtree) 156 | for np in subsubtree: 157 | chunks.append(np) 158 | 159 | # if the current tree has no subtree with a 'NP' label 160 | # and is itself an 'NP' labeled node then, append the tree to chunks 161 | if tree.label() == "NP" and not contains: 162 | chunks.append(tree) 163 | 164 | return chunks 165 | 166 | 167 | if __name__ == "__main__": 168 | main() 169 | -------------------------------------------------------------------------------- /Traffic/traffic.py: -------------------------------------------------------------------------------- 1 | import cv2 2 | import numpy as np 3 | import os 4 | import sys 5 | import warnings 6 | warnings.filterwarnings(action='ignore') 7 | 8 | # from tensorflow.python.keras import layers 9 | import tensorflow as tf 10 | from tqdm import tqdm 11 | 12 | from sklearn.model_selection import train_test_split 13 | 14 | EPOCHS = 10 15 | IMG_WIDTH = 30 16 | IMG_HEIGHT = 30 17 | NUM_CATEGORIES = 43 18 | # NUM_CATEGORIES = 3 19 | TEST_SIZE = 0.4 20 | 21 | 22 | def main(): 23 | 24 | # Check command-line arguments 25 | if len(sys.argv) not in [2, 3]: 26 | sys.exit("Usage: python traffic.py data_directory [model.h5]") 27 | 28 | # Get image arrays and labels for all image files 29 | images, labels = load_data(sys.argv[1]) 30 | 31 | # Split data into training and testing sets 32 | labels = tf.keras.utils.to_categorical(labels) 33 | # tf.cast(labels, '') 34 | 35 | x_train, x_test, y_train, y_test = train_test_split( 36 | np.array(images), np.array(labels), test_size=TEST_SIZE, random_state=42, 37 | ) 38 | 39 | # Get a compiled neural network 40 | model = get_model() 41 | 42 | # Fit model on training data 43 | model.fit(x_train, y_train, epochs=EPOCHS) 44 | 45 | # Evaluate neural network performance 46 | model.evaluate(x_test, y_test, verbose=2) 47 | 48 | # Save model to file 49 | if len(sys.argv) == 3: 50 | filename = sys.argv[2] 51 | model.save(filename) 52 | print(f"Model saved to {filename}.") 53 | 54 | 55 | def load_data(data_dir): 56 | """ 57 | Load image data from directory `data_dir`. 58 | 59 | Assume `data_dir` has one directory named after each category, numbered 60 | 0 through NUM_CATEGORIES - 1. Inside each category directory will be some 61 | number of image files. 62 | 63 | Return tuple `(images, labels)`. `images` should be a list of all 64 | of the images in the data directory, where each image is formatted as a 65 | numpy ndarray with dimensions IMG_WIDTH x IMG_HEIGHT x 3. `labels` should 66 | be a list of integer labels, representing the categories for each of the 67 | corresponding `images`. 68 | """ 69 | labels = [] 70 | images = [] 71 | categories = list(filter(lambda x: not x.startswith('.'), os.listdir(data_dir))) 72 | 73 | for each_category in tqdm(categories, desc='Reading folders'): 74 | if not each_category in map(lambda x : str(x), range(NUM_CATEGORIES)): 75 | # skip if not within the standard naming convention 76 | continue 77 | category_dir = os.path.join(data_dir, each_category) 78 | files = os.listdir(category_dir) 79 | # ppm refers to a Portable image format 80 | for file in files: 81 | 82 | path = os.path.join(category_dir, file) 83 | img = cv2.imread(path) 84 | 85 | # resize img 86 | # According to cv2 docs: 87 | # To shrink an image, it will generally look best with #INTER_AREA interpolation 88 | img_dimension = (IMG_WIDTH, IMG_HEIGHT) 89 | resized = cv2.resize(img, img_dimension, interpolation = cv2.INTER_AREA) 90 | if resized.shape != (30, 30, 3): 91 | raise Exception('Error while resizing image.') 92 | 93 | images.append(resized/255) 94 | labels.append(int(each_category)) 95 | 96 | print(resized.shape) 97 | print(np.unique(labels)) 98 | return (images, labels) 99 | 100 | 101 | def get_model(): 102 | """ 103 | Returns a compiled convolutional neural network model. Assume that the 104 | `input_shape` of the first layer is `(IMG_WIDTH, IMG_HEIGHT, 3)`. 105 | The output layer should have `NUM_CATEGORIES` units, one for each category. 106 | """ 107 | neural_network = tf.keras.models.Sequential(layers=[ 108 | # Standardization (probably not so necessary as value is within 0-1) 109 | 110 | # centering data at mean = 0 111 | tf.keras.layers.BatchNormalization(input_shape=(IMG_WIDTH, IMG_HEIGHT, 3)), 112 | 113 | 114 | # Pooling Layers 115 | 116 | # Extract primary set of informations (edges, contours) 117 | tf.keras.layers.Conv2D(filters=32, kernel_size=(3,3), activation='relu'), 118 | # Max Pooling to sample our data by the maximum value within block (2x2 pooling) 119 | tf.keras.layers.MaxPooling2D(pool_size=(2,2)), 120 | 121 | # Extract secondary set of informations (like objects) 122 | tf.keras.layers.Conv2D(filters=64, kernel_size=(3,3), activation='relu'), 123 | tf.keras.layers.MaxPooling2D(pool_size=(2,2)), 124 | 125 | # Extract tertiary set of informations (i don't know) 126 | tf.keras.layers.Conv2D(filters=128, kernel_size=(3,3), activation='relu'), 127 | tf.keras.layers.MaxPooling2D(pool_size=(2,2)), 128 | 129 | # flatten data so as it becomes the initial units of a deep neural network 130 | tf.keras.layers.Flatten(), 131 | 132 | # add a hidden layer here 133 | tf.keras.layers.Dense(units=128, activation='relu'), 134 | 135 | # add a dropout so as to avoid overfitting (avoid over-reliance on specific units) 136 | tf.keras.layers.Dropout(rate=0.5), 137 | 138 | # Output Layer, using softmax to convert to probability distributions 139 | tf.keras.layers.Dense(NUM_CATEGORIES, activation='softmax'), 140 | ]) 141 | 142 | neural_network.compile( 143 | optimizer="adam", 144 | loss="categorical_crossentropy", 145 | metrics=["accuracy"] 146 | ) 147 | 148 | return neural_network 149 | 150 | 151 | if __name__ == "__main__": 152 | main() 153 | -------------------------------------------------------------------------------- /Project 0a: Degrees: -------------------------------------------------------------------------------- 1 | import csv 2 | import sys 3 | 4 | from util import Node, StackFrontier, QueueFrontier 5 | 6 | # Maps names to a set of corresponding person_ids 7 | names = {} 8 | 9 | # Maps person_ids to a dictionary of: name, birth, movies (a set of movie_ids) 10 | people = {} 11 | 12 | # Maps movie_ids to a dictionary of: title, year, stars (a set of person_ids) 13 | movies = {} 14 | 15 | 16 | def load_data(directory): 17 | """ 18 | Load data from CSV files into memory. 19 | """ 20 | # Load people 21 | with open(f"{directory}/people.csv", encoding="utf-8") as f: 22 | reader = csv.DictReader(f) 23 | for row in reader: 24 | people[row["id"]] = { 25 | "name": row["name"], 26 | "birth": row["birth"], 27 | "movies": set() 28 | } 29 | if row["name"].lower() not in names: 30 | names[row["name"].lower()] = {row["id"]} 31 | else: 32 | names[row["name"].lower()].add(row["id"]) 33 | 34 | 35 | # Load movies 36 | with open(f"{directory}/movies.csv", encoding="utf-8") as f: 37 | reader = csv.DictReader(f) 38 | for row in reader: 39 | movies[row["id"]] = { 40 | "title": row["title"], 41 | "year": row["year"], 42 | "stars": set() 43 | } 44 | 45 | # Load stars 46 | with open(f"{directory}/stars.csv", encoding="utf-8") as f: 47 | reader = csv.DictReader(f) 48 | for row in reader: 49 | try: 50 | people[row["person_id"]]["movies"].add(row["movie_id"]) 51 | movies[row["movie_id"]]["stars"].add(row["person_id"]) 52 | except KeyError: 53 | pass 54 | 55 | 56 | def main(): 57 | if len(sys.argv) > 2: 58 | sys.exit("Usage: python degrees.py [directory]") 59 | directory = sys.argv[1] if len(sys.argv) == 2 else "large" 60 | 61 | # Load data from files into memory 62 | print("Loading data...") 63 | load_data(directory) 64 | print("Data loaded.") 65 | 66 | source = person_id_for_name(input("Name: ")) 67 | if source is None: 68 | sys.exit("Person not found.") 69 | target = person_id_for_name(input("Name: ")) 70 | if target is None: 71 | sys.exit("Person not found.") 72 | 73 | path = shortest_path(source, target) 74 | 75 | if path is None: 76 | print("Not connected.") 77 | else: 78 | degrees = len(path) 79 | print(f"{degrees} degrees of separation.") 80 | path = [(None, source)] + path 81 | for i in range(degrees): 82 | person1 = people[path[i][1]]["name"] 83 | person2 = people[path[i + 1][1]]["name"] 84 | movie = movies[path[i + 1][0]]["title"] 85 | print(f"{i + 1}: {person1} and {person2} starred in {movie}") 86 | 87 | 88 | def shortest_path(source, target): 89 | """ 90 | Returns the shortest list of (movie_id, person_id) pairs 91 | that connect the source to the target. 92 | 93 | If no possible path, returns None. 94 | """ 95 | 96 | start = Node(state=source, parent=None, action=None) 97 | frontier = QueueFrontier() 98 | frontier.add(start) 99 | 100 | explored = set() 101 | 102 | while True: 103 | if frontier.empty(): 104 | return None 105 | 106 | node = frontier.remove() 107 | 108 | if node.state == target: 109 | rt = [] 110 | 111 | while node.parent is not None: 112 | rt.append((node.action, node.state)) 113 | node = node.parent 114 | 115 | rt.reverse() 116 | return rt 117 | 118 | explored.add(node.state) 119 | 120 | 121 | # find neighbours 122 | neighbours = neighbors_for_person(node.state) 123 | 124 | # add neighbours to frontier 125 | for action, state in neighbours: 126 | if not frontier.contains_state(state) and state not in explored: 127 | child = Node(state=state, parent=node, action=action) 128 | if child.state == target: 129 | rt = [] 130 | 131 | while child.parent is not None: 132 | rt.append((child.action, child.state)) 133 | child = child.parent 134 | 135 | rt.reverse() 136 | return rt 137 | frontier.add(child) 138 | 139 | 140 | def person_id_for_name(name): 141 | """ 142 | Returns the IMDB id for a person's name, 143 | resolving ambiguities as needed. 144 | """ 145 | person_ids = list(names.get(name.lower(), set())) 146 | if len(person_ids) == 0: 147 | return None 148 | elif len(person_ids) > 1: 149 | print(f"Which '{name}'?") 150 | for person_id in person_ids: 151 | person = people[person_id] 152 | name = person["name"] 153 | birth = person["birth"] 154 | print(f"ID: {person_id}, Name: {name}, Birth: {birth}") 155 | try: 156 | person_id = input("Intended Person ID: ") 157 | if person_id in person_ids: 158 | return person_id 159 | except ValueError: 160 | pass 161 | return None 162 | else: 163 | return person_ids[0] 164 | 165 | 166 | def neighbors_for_person(person_id): 167 | """ 168 | Returns (movie_id, person_id) pairs for people 169 | who starred with a given person. 170 | """ 171 | movie_ids = people[person_id]["movies"] 172 | neighbors = set() 173 | for movie_id in movie_ids: 174 | for person_id in movies[movie_id]["stars"]: 175 | neighbors.add((movie_id, person_id)) 176 | return neighbors 177 | 178 | 179 | if __name__ == "__main__": 180 | main() 181 | -------------------------------------------------------------------------------- /PageRank/pagerank.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | import re 4 | import sys 5 | 6 | DAMPING = 0.85 7 | SAMPLES = 10000 8 | 9 | 10 | def main(): 11 | if len(sys.argv) != 2: 12 | sys.exit("Usage: python pagerank.py corpus") 13 | corpus = crawl(sys.argv[1]) 14 | ranks = sample_pagerank(corpus, DAMPING, SAMPLES) 15 | print(f"PageRank Results from Sampling (n = {SAMPLES})") 16 | for page in sorted(ranks): 17 | print(f" {page}: {ranks[page]:.4f}") 18 | ranks = iterate_pagerank(corpus, DAMPING) 19 | print(f"PageRank Results from Iteration") 20 | for page in sorted(ranks): 21 | print(f" {page}: {ranks[page]:.4f}") 22 | 23 | 24 | def crawl(directory): 25 | """ 26 | Parse a directory of HTML pages and check for links to other pages. 27 | Return a dictionary where each key is a page, and values are 28 | a list of all other pages in the corpus that are linked to by the page. 29 | """ 30 | pages = dict() 31 | 32 | # Extract all links from HTML files 33 | for filename in os.listdir(directory): 34 | if not filename.endswith(".html"): 35 | continue 36 | with open(os.path.join(directory, filename)) as f: 37 | contents = f.read() 38 | links = re.findall(r"]*?)href=\"([^\"]*)\"", contents) 39 | pages[filename] = set(links) - {filename} 40 | 41 | # Only include links to other pages in the corpus 42 | for filename in pages: 43 | pages[filename] = set( 44 | link for link in pages[filename] 45 | if link in pages 46 | ) 47 | 48 | return pages 49 | 50 | 51 | def transition_model(corpus, page, damping_factor): 52 | """ 53 | Return a probability distribution over which page to visit next, 54 | given a current page. 55 | 56 | With probability `damping_factor`, choose a link at random 57 | linked to by `page`. With probability `1 - damping_factor`, choose 58 | a link at random chosen from all pages in the corpus. 59 | """ 60 | 61 | prop_dist = {} 62 | 63 | # check if page has outgoing links 64 | dict_len = len(corpus.keys()) 65 | pages_len = len(corpus[page]) 66 | 67 | if len(corpus[page]) < 1: 68 | # no outgoing pages, choosing randomly from all possible pages 69 | for key in corpus.keys(): 70 | prop_dist[key] = 1 / dict_len 71 | 72 | else: 73 | # there are outgoing pages, calculating distribution 74 | random_factor = (1 - damping_factor) / dict_len 75 | even_factor = damping_factor / pages_len 76 | 77 | for key in corpus.keys(): 78 | if key not in corpus[page]: 79 | prop_dist[key] = random_factor 80 | else: 81 | prop_dist[key] = even_factor + random_factor 82 | 83 | return prop_dist 84 | 85 | 86 | def sample_pagerank(corpus, damping_factor, n): 87 | """ 88 | Return PageRank values for each page by sampling `n` pages 89 | according to transition model, starting with a page at random. 90 | 91 | Return a dictionary where keys are page names, and values are 92 | their estimated PageRank value (a value between 0 and 1). All 93 | PageRank values should sum to 1. 94 | """ 95 | 96 | # prepare dictionary with number of samples == 0 97 | samples_dict = corpus.copy() 98 | for i in samples_dict: 99 | samples_dict[i] = 0 100 | sample = None 101 | 102 | # itearting n times 103 | for _ in range(n): 104 | if sample: 105 | # previous sample is available, choosing using transition model 106 | dist = transition_model(corpus, sample, damping_factor) 107 | dist_lst = list(dist.keys()) 108 | dist_weights = [dist[i] for i in dist] 109 | sample = random.choices(dist_lst, dist_weights, k=1)[0] 110 | else: 111 | # no previous sample, choosing randomly 112 | sample = random.choice(list(corpus.keys())) 113 | 114 | # count each sample 115 | samples_dict[sample] += 1 116 | 117 | # turn sample count to percentage 118 | for item in samples_dict: 119 | samples_dict[item] /= n 120 | 121 | return samples_dict 122 | 123 | 124 | def iterate_pagerank(corpus, damping_factor): 125 | """ 126 | Return PageRank values for each page by iteratively updating 127 | PageRank values until convergence. 128 | 129 | Return a dictionary where keys are page names, and values are 130 | their estimated PageRank value (a value between 0 and 1). All 131 | PageRank values should sum to 1. 132 | """ 133 | pages_number = len(corpus) 134 | old_dict = {} 135 | new_dict = {} 136 | 137 | # assigning each page a rank of 1/n, where n is total number of pages in the corpus 138 | for page in corpus: 139 | old_dict[page] = 1 / pages_number 140 | 141 | # repeatedly calculating new rank values basing on all of the current rank values 142 | while True: 143 | for page in corpus: 144 | temp = 0 145 | for linking_page in corpus: 146 | # check if page links to our page 147 | if page in corpus[linking_page]: 148 | temp += (old_dict[linking_page] / len(corpus[linking_page])) 149 | # if page has no links, interpret it as having one link for every other page 150 | if len(corpus[linking_page]) == 0: 151 | temp += (old_dict[linking_page]) / len(corpus) 152 | temp *= damping_factor 153 | temp += (1 - damping_factor) / pages_number 154 | 155 | new_dict[page] = temp 156 | 157 | difference = max([abs(new_dict[x] - old_dict[x]) for x in old_dict]) 158 | if difference < 0.001: 159 | break 160 | else: 161 | old_dict = new_dict.copy() 162 | 163 | return old_dict 164 | 165 | if __name__ == "__main__": 166 | main() 167 | -------------------------------------------------------------------------------- /Heredity/heredity.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import itertools 3 | import sys 4 | 5 | PROBS = { 6 | 7 | # Unconditional probabilities for having gene 8 | "gene": { 9 | 2: 0.01, 10 | 1: 0.03, 11 | 0: 0.96 12 | }, 13 | 14 | "trait": { 15 | 16 | # Probability of trait given two copies of gene 17 | 2: { 18 | True: 0.65, 19 | False: 0.35 20 | }, 21 | 22 | # Probability of trait given one copy of gene 23 | 1: { 24 | True: 0.56, 25 | False: 0.44 26 | }, 27 | 28 | # Probability of trait given no gene 29 | 0: { 30 | True: 0.01, 31 | False: 0.99 32 | } 33 | }, 34 | 35 | # Mutation probability 36 | "mutation": 0.01 37 | } 38 | 39 | 40 | def main(): 41 | 42 | # Check for proper usage 43 | if len(sys.argv) != 2: 44 | sys.exit("Usage: python heredity.py data.csv") 45 | people = load_data(sys.argv[1]) 46 | 47 | # Keep track of gene and trait probabilities for each person 48 | probabilities = { 49 | person: { 50 | "gene": { 51 | 2: 0, 52 | 1: 0, 53 | 0: 0 54 | }, 55 | "trait": { 56 | True: 0, 57 | False: 0 58 | } 59 | } 60 | for person in people 61 | } 62 | 63 | # Loop over all sets of people who might have the trait 64 | names = set(people) 65 | for have_trait in powerset(names): 66 | 67 | # Check if current set of people violates known information 68 | fails_evidence = any( 69 | (people[person]["trait"] is not None and 70 | people[person]["trait"] != (person in have_trait)) 71 | for person in names 72 | ) 73 | if fails_evidence: 74 | continue 75 | 76 | # Loop over all sets of people who might have the gene 77 | for one_gene in powerset(names): 78 | for two_genes in powerset(names - one_gene): 79 | 80 | # Update probabilities with new joint probability 81 | p = joint_probability(people, one_gene, two_genes, have_trait) 82 | update(probabilities, one_gene, two_genes, have_trait, p) 83 | 84 | # Ensure probabilities sum to 1 85 | normalize(probabilities) 86 | 87 | # Print results 88 | for person in people: 89 | print(f"{person}:") 90 | for field in probabilities[person]: 91 | print(f" {field.capitalize()}:") 92 | for value in probabilities[person][field]: 93 | p = probabilities[person][field][value] 94 | print(f" {value}: {p:.4f}") 95 | 96 | 97 | def load_data(filename): 98 | """ 99 | Load gene and trait data from a file into a dictionary. 100 | File assumed to be a CSV containing fields name, mother, father, trait. 101 | mother, father must both be blank, or both be valid names in the CSV. 102 | trait should be 0 or 1 if trait is known, blank otherwise. 103 | """ 104 | data = dict() 105 | with open(filename) as f: 106 | reader = csv.DictReader(f) 107 | for row in reader: 108 | name = row["name"] 109 | data[name] = { 110 | "name": name, 111 | "mother": row["mother"] or None, 112 | "father": row["father"] or None, 113 | "trait": (True if row["trait"] == "1" else 114 | False if row["trait"] == "0" else None) 115 | } 116 | return data 117 | 118 | 119 | def powerset(s): 120 | """ 121 | Return a list of all possible subsets of set s. 122 | """ 123 | s = list(s) 124 | return [ 125 | set(s) for s in itertools.chain.from_iterable( 126 | itertools.combinations(s, r) for r in range(len(s) + 1) 127 | ) 128 | ] 129 | 130 | 131 | def joint_probability(people, one_gene, two_genes, have_trait): 132 | """ 133 | Compute and return a joint probability. 134 | 135 | The probability returned should be the probability that 136 | * everyone in set `one_gene` has one copy of the gene, and 137 | * everyone in set `two_genes` has two copies of the gene, and 138 | * everyone not in `one_gene` or `two_gene` does not have the gene, and 139 | * everyone in set `have_trait` has the trait, and 140 | * everyone not in set` have_trait` does not have the trait. 141 | """ 142 | probability = 1 143 | 144 | for person in people: 145 | gene_number = 1 if person in one_gene else 2 if person in two_genes else 0 146 | trait = True if person in have_trait else False 147 | 148 | gene_numb_prop = PROBS['gene'][gene_number] 149 | trait_prop = PROBS['trait'][gene_number][trait] 150 | 151 | if people[person]['mother'] is None: 152 | # no parents, use probability distribution 153 | probability *= gene_numb_prop * trait_prop 154 | else: 155 | # info about parents is available 156 | mother = people[person]['mother'] 157 | father = people[person]['father'] 158 | percentages = {} 159 | 160 | for ppl in [mother, father]: 161 | number = 1 if ppl in one_gene else 2 if ppl in two_genes else 0 162 | perc = 0 + PROBS['mutation'] if number == 0 else 0.5 if number == 1 else 1 - PROBS['mutation'] 163 | percentages[ppl] = perc 164 | 165 | if gene_number == 0: 166 | # 0, none of parents gave gene 167 | probability *= (1 - percentages[mother]) * (1 - percentages[father]) 168 | elif gene_number == 1: 169 | # 1, one of parents gave gene 170 | probability *= (1 - percentages[mother]) * percentages[father] + percentages[mother] * (1 - percentages[father]) 171 | else: 172 | # 2, both of parents gave gene 173 | probability *= percentages[mother] * percentages[father] 174 | 175 | probability *= trait_prop 176 | 177 | return probability 178 | 179 | 180 | def update(probabilities, one_gene, two_genes, have_trait, p): 181 | """ 182 | Add to `probabilities` a new joint probability `p`. 183 | Each person should have their "gene" and "trait" distributions updated. 184 | Which value for each distribution is updated depends on whether 185 | the person is in `have_gene` and `have_trait`, respectively. 186 | """ 187 | for person in probabilities: 188 | gene_number = 1 if person in one_gene else 2 if person in two_genes else 0 189 | probabilities[person]["gene"][gene_number] += p 190 | probabilities[person]["trait"][person in have_trait] += p 191 | 192 | 193 | def normalize(probabilities): 194 | """ 195 | Update `probabilities` such that each probability distribution 196 | is normalized (i.e., sums to 1, with relative proportions the same). 197 | """ 198 | normalized = probabilities.copy() 199 | for person in probabilities: 200 | for typ in ['gene', 'trait']: 201 | summed = sum(probabilities[person][typ].values()) 202 | for category in probabilities[person][typ]: 203 | val = probabilities[person][typ][category] 204 | normalized_val = val / summed 205 | normalized[person][typ][category] = normalized_val 206 | return normalized 207 | 208 | 209 | if __name__ == "__main__": 210 | main() 211 | -------------------------------------------------------------------------------- /Tic-Tac-Toe/tictactoe.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tic Tac Toe Player 3 | """ 4 | 5 | import copy 6 | 7 | X = "X" 8 | O = "O" 9 | EMPTY = None 10 | INFINITY = 1e3 11 | 12 | 13 | class InvalidAction(Exception): 14 | "Raised when the action is not available" 15 | pass 16 | 17 | 18 | def initial_state(): 19 | """ 20 | Returns starting state of the board. 21 | """ 22 | return [[EMPTY, EMPTY, EMPTY], 23 | [EMPTY, EMPTY, EMPTY], 24 | [EMPTY, EMPTY, EMPTY]] 25 | 26 | 27 | def player(board): 28 | """ 29 | Returns player who has the next turn on a board. 30 | 31 | X starts the game. 32 | """ 33 | # initialize counters 34 | x_counter, o_counter = 0, 0 35 | 36 | for each_row in board: 37 | # row-wise count of each X and O's 38 | x_counter += each_row.count(X) 39 | o_counter += each_row.count(O) 40 | 41 | # O plays if and only if it has less turns on board 42 | if o_counter < x_counter: 43 | return O 44 | 45 | return X 46 | 47 | 48 | def actions(board): 49 | """ 50 | Returns set of all possible actions (i, j) available on the board. 51 | 52 | An available in this game refers to a position on board where no X nor O is 53 | chosen yet. 54 | """ 55 | action_set = set() 56 | for row_index, row in enumerate(board): 57 | for col_index, value in enumerate(row): 58 | if value not in (X, O): 59 | action_set.add((row_index, col_index)) 60 | 61 | return action_set 62 | 63 | 64 | def result(board, action): 65 | """ 66 | Returns the board that results from making move (i, j) on the board. 67 | 68 | Cannot overwrite values. 69 | """ 70 | # check if action is a valid action 71 | if action not in actions(board): 72 | raise InvalidAction("You have performed an invalid action.") 73 | 74 | # whose turn it is now? result will be either X or O 75 | current_player = player(board) 76 | 77 | # create a deep copy so as not to substitute values 78 | new_board = copy.deepcopy(board) 79 | 80 | row_index, col_index = action 81 | new_board[row_index][col_index] = current_player 82 | 83 | return new_board 84 | 85 | 86 | def winner(board): 87 | """ 88 | Returns the winner of the game, if there is one. 89 | """ 90 | # check for consecutive positioning 91 | # assume square board 92 | BOARD_SIZE = len(board) 93 | 94 | # row-wise 95 | for each_row in board: 96 | if each_row.count(X) == BOARD_SIZE: 97 | return X 98 | if each_row.count(O) == BOARD_SIZE: 99 | return O 100 | 101 | # column-wise 102 | for col_index in range(BOARD_SIZE): 103 | column = [row[col_index] for row in board] 104 | if column.count(X) == BOARD_SIZE: 105 | return X 106 | if column.count(O) == BOARD_SIZE: 107 | return O 108 | 109 | # diagonal-wise 110 | # main diagonal i = j 111 | main_diagonal = [board[i][i] for i in range(BOARD_SIZE)] 112 | # anti diagonal i = len(matrix) -1 -j 113 | anti_diagonal = [board[BOARD_SIZE-1-i][i] for i in range(BOARD_SIZE)] 114 | if main_diagonal.count(X) == BOARD_SIZE \ 115 | or anti_diagonal.count(X) == BOARD_SIZE: 116 | return X 117 | if main_diagonal.count(O) == BOARD_SIZE \ 118 | or anti_diagonal.count(O) == BOARD_SIZE: 119 | return O 120 | 121 | return None 122 | 123 | 124 | def terminal(board): 125 | """ 126 | Returns True if game is over, False otherwise. 127 | """ 128 | # the game is over when X or O is the winner 129 | if winner(board) in (X, O): 130 | return True 131 | 132 | # or if the board is full (i.e., there are no EMPTY anymore) 133 | if not any(map(lambda x : EMPTY in x, board)): 134 | return True 135 | 136 | return False 137 | 138 | 139 | def utility(board): 140 | """ 141 | Returns 1 if X has won the game, -1 if O has won, 0 otherwise. 142 | """ 143 | VICTORY = 1 144 | DEFEAT = -1 145 | DRAW = 0 146 | 147 | if winner(board) == X: 148 | return VICTORY 149 | elif winner(board) == O: 150 | return DEFEAT 151 | 152 | return DRAW 153 | 154 | 155 | def minimax(board): 156 | """ 157 | Returns the optimal action for the current player on the board. 158 | 159 | If I'm the maximizing player, I want to choose from my actions, the one that will 160 | lead to the MAXIMUM possible outcome, from the outcomes my opponent has offered me. 161 | And, of course, my opponent has tried to minimize the outcome to offer me. 162 | 163 | On the contrary, if it's the the minimizing player's turn, now I'm in the shoes of my opponent. 164 | In this case, as per my point of view, he/she is trying to minimize my outcome. Then I need to 165 | choose from his/her actions, the one that will lead to the MINIMUM possible outcome 166 | which is what he/she would do. 167 | 168 | Of course, this supposes that your opponent is playing optimally. 169 | """ 170 | if terminal(board): 171 | return None 172 | 173 | current_player = player(board) 174 | 175 | if current_player == X: 176 | # i'm the maximizing player, my opponent is the minimizing player 177 | candidate_outcome = -INFINITY 178 | for action in actions(board): 179 | possible_board = result(board, action) 180 | # opponent is trying to minimize this action of mine 181 | opponents_utility = min_value(possible_board) 182 | # store info (action and utility) from the maximum 183 | # value my opponent has given to me 184 | if opponents_utility > candidate_outcome: 185 | candidate_outcome = opponents_utility 186 | candidate_action = action 187 | elif current_player == O: 188 | # i'm the minimizing player, my opponent is the maximizing player 189 | candidate_outcome = INFINITY 190 | for action in actions(board): 191 | possible_board = result(board, action) 192 | # I am trying to maximize this action of my opponent 193 | opponents_utility = max_value(possible_board) 194 | # store info (action and utility) from the minimum 195 | # value my opponent has given to me 196 | if opponents_utility < candidate_outcome: 197 | # store this result 198 | candidate_outcome = opponents_utility 199 | candidate_action = action 200 | 201 | return candidate_action 202 | 203 | 204 | def max_value(board): 205 | """ 206 | The maximizing player picks action a in set of actions that 207 | produces the highest value of min_value(result(state, action)). 208 | """ 209 | v = -INFINITY 210 | if terminal(board): 211 | return utility(board) 212 | 213 | for action in actions(board): 214 | # get the resulting board for a given action 215 | resulting_board = result(board, action) 216 | # check the best option for the next player 217 | v = max(v, min_value(resulting_board)) 218 | 219 | return v 220 | 221 | 222 | def min_value(board): 223 | """ 224 | The minimizing player picks action a in set of actions that 225 | produces the lowest value of max_value(result(state, actions)). 226 | """ 227 | v = INFINITY 228 | if terminal(board): 229 | return utility(board) 230 | 231 | for action in actions(board): 232 | # get the resulting board for a given action 233 | resulting_board = result(board, action) 234 | # check the best option for the next player 235 | v = min(v, max_value(resulting_board)) 236 | 237 | return v 238 | -------------------------------------------------------------------------------- /Shopping/shopping.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import sys 3 | 4 | import pandas as pd 5 | from datetime import datetime 6 | 7 | from sklearn.model_selection import train_test_split 8 | from sklearn.neighbors import KNeighborsClassifier 9 | 10 | TEST_SIZE = 0.4 11 | 12 | 13 | def main(): 14 | 15 | # Check command-line arguments 16 | if len(sys.argv) != 2: 17 | sys.exit("Usage: python shopping.py data") 18 | 19 | # Load data from spreadsheet and split into train and test sets 20 | evidence, labels = load_data(sys.argv[1]) 21 | 22 | # including random_state just to keep results steady 23 | X_train, X_test, y_train, y_test = train_test_split( 24 | evidence, labels, test_size=TEST_SIZE, random_state=11, 25 | ) 26 | 27 | # Train model and make predictions 28 | model = train_model(X_train, y_train) 29 | predictions = model.predict(X_test) 30 | sensitivity, specificity = evaluate(y_test, predictions) 31 | 32 | # Print results 33 | print(f"Correct: {(y_test == predictions).sum()}") 34 | print(f"Incorrect: {(y_test != predictions).sum()}") 35 | print(f"True Positive Rate: {100 * sensitivity:.2f}%") 36 | print(f"True Negative Rate: {100 * specificity:.2f}%") 37 | 38 | 39 | def load_data(filename: str): 40 | """ 41 | Load shopping data from a CSV file `filename` and convert into a list of 42 | evidence lists and a list of labels. Return a tuple (evidence, labels). 43 | 44 | evidence should be a list of lists, where each list contains the 45 | following values, in order: 46 | - Administrative, an integer 47 | - Administrative_Duration, a floating point number 48 | - Informational, an integer 49 | - Informational_Duration, a floating point number 50 | - ProductRelated, an integer 51 | - ProductRelated_Duration, a floating point number 52 | - BounceRates, a floating point number 53 | - ExitRates, a floating point number 54 | - PageValues, a floating point number 55 | - SpecialDay, a floating point number 56 | - Month, an index from 0 (January) to 11 (December) 57 | - OperatingSystems, an integer 58 | - Browser, an integer 59 | - Region, an integer 60 | - TrafficType, an integer 61 | - VisitorType, an integer 0 (not returning) or 1 (returning) 62 | - Weekend, an integer 0 (if false) or 1 (if true) 63 | 64 | labels should be the corresponding list of labels, where each label 65 | is 1 if Revenue is true, and 0 otherwise. 66 | 67 | Added notes: 68 | This process is likely to not work in real life production model. 69 | And it may be even potentially dangerous. 70 | 71 | Production rationale: As this is a simple case, it is fine, but 72 | if in real life something changes, we are not considering it. For example, 73 | the treatment we had to perfom on the variable 'June' to translate it to 'Jun', 74 | if any new incoming data does not follow that rule, or if sometime 'Jul' comes as 75 | 'July', we would end up with a crash. The best solution would be to include 76 | these transformations in a pipeline that would better handle possible errors. 77 | Best idea would be to fit these transforms in the training set and apply them 78 | on the testing set/real world incoming data. 79 | 80 | Dangerous rationale: We should have created our variables only after 81 | splitting our dataset. In this case it is acceptable, but if we were 82 | to input missing values using an average, for instance, we would have 83 | leaked data from our training set to our test set. We would end up with 84 | a probable over-estimated test score as compared to real life production 85 | model. 86 | """ 87 | data = pd.read_csv(filename) 88 | 89 | # Month, VisitorType should be converted 90 | # remove last 'e' from June to make it %b-able 91 | data.loc[:, ['Month']] = data.loc[:, 'Month'].apply( 92 | lambda x : datetime.strptime(x.replace('June','Jun'), r"%b").month - 1 93 | ) 94 | data.loc[:, ['VisitorType']] = data.loc[:, 'VisitorType'] == 'Returning_Visitor' 95 | 96 | int_cols = [ 97 | 'Administrative','Informational','ProductRelated', 98 | 'Month','OperatingSystems','Browser', 99 | 'Region','TrafficType','VisitorType', 100 | 'Weekend','Revenue'] 101 | 102 | float_cols = [ 103 | 'Administrative_Duration','Informational_Duration', 104 | 'ProductRelated_Duration','BounceRates', 105 | 'ExitRates','PageValues','SpecialDay' 106 | ] 107 | 108 | data.loc[:, int_cols] = data.loc[:, int_cols].astype(int) 109 | data.loc[:, float_cols] = data.loc[:, float_cols].astype(float) 110 | 111 | evidence = data.drop(columns=['Revenue']) 112 | label = data.loc[:, 'Revenue'] 113 | 114 | # convert to list 115 | evidences = [list(row) for row in evidence.to_records(index=False).tolist()] 116 | labels = label.values.tolist() 117 | 118 | return (evidences, labels) 119 | 120 | 121 | def train_model(evidence, labels): 122 | """ 123 | Given a list of evidence lists and a list of labels, return a 124 | fitted k-nearest neighbor model (k=1) trained on the data. 125 | 126 | Added Notes: 127 | For the KNN model, as K decreases, complexity increases. 128 | Probably, they wanted the model as overfited as possible so as 129 | they could pass through a grade-checker without much trouble. 130 | Maybe a random_state within the train_test_split function would help, 131 | or a pickle model saved at the end. 132 | """ 133 | # K-NN with K=1 as specified 134 | model = KNeighborsClassifier(n_neighbors=1) 135 | model.fit(evidence, labels) 136 | 137 | return model 138 | 139 | 140 | def evaluate(labels, predictions): 141 | """ 142 | Given a list of actual labels and a list of predicted labels, 143 | return a tuple (sensitivity, specificty). 144 | 145 | Assume each label is either a 1 (positive) or 0 (negative). 146 | 147 | `sensitivity` should be a floating-point value from 0 to 1 148 | representing the "true positive rate": the proportion of 149 | actual positive labels that were accurately identified. 150 | 151 | `specificity` should be a floating-point value from 0 to 1 152 | representing the "true negative rate": the proportion of 153 | actual negative labels that were accurately identified. 154 | """ 155 | from sklearn import metrics 156 | # sensitivity: true positive rate 157 | sensitivity = 0 158 | # specificity: true negative rate 159 | specificity = 0 160 | 161 | for predicted, label in zip(predictions, labels): 162 | if label == 1: 163 | sensitivity += predicted == label 164 | if label == 0: 165 | specificity += predicted == label 166 | 167 | # number of correct predictions that were labeled 1 168 | # divided by the total number of labeled 1 instances 169 | sensitivity /= sum(labels) 170 | 171 | # number of correct predictions that were labeled 0 172 | # divided by the total nubmer of labeled 0 instances 173 | specificity /= (len(labels) - sum(labels)) 174 | 175 | # using a confusion matrix: 176 | tn, fp, fn, tp = metrics.confusion_matrix(labels, predictions).ravel() 177 | # true positive divided by all positives (true positive + false negatives) 178 | sensitivity_confusion = tp / (tp+fn) 179 | # true negative divided by all negatives (true negative + false positive) 180 | specificity = tn / (tn+fp) 181 | 182 | 183 | # sensitivity is also called recall 184 | sensitivity_recall = metrics.recall_score(labels, predictions) 185 | # print(sensitivity, sensitivity_confusion, sensitivity_recall) 186 | return sensitivity, specificity 187 | 188 | 189 | if __name__ == "__main__": 190 | main() 191 | -------------------------------------------------------------------------------- /Minesweeper/runner.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import sys 3 | import time 4 | 5 | from minesweeper import Minesweeper, MinesweeperAI 6 | 7 | HEIGHT = 8 8 | WIDTH = 8 9 | MINES = 8 10 | 11 | # Colors 12 | BLACK = (0, 0, 0) 13 | GRAY = (180, 180, 180) 14 | WHITE = (255, 255, 255) 15 | 16 | # Create game 17 | pygame.init() 18 | size = width, height = 600, 400 19 | screen = pygame.display.set_mode(size) 20 | 21 | # Fonts 22 | OPEN_SANS = "assets/fonts/OpenSans-Regular.ttf" 23 | smallFont = pygame.font.Font(OPEN_SANS, 20) 24 | mediumFont = pygame.font.Font(OPEN_SANS, 28) 25 | largeFont = pygame.font.Font(OPEN_SANS, 40) 26 | 27 | # Compute board size 28 | BOARD_PADDING = 20 29 | board_width = ((2 / 3) * width) - (BOARD_PADDING * 2) 30 | board_height = height - (BOARD_PADDING * 2) 31 | cell_size = int(min(board_width / WIDTH, board_height / HEIGHT)) 32 | board_origin = (BOARD_PADDING, BOARD_PADDING) 33 | 34 | # Add images 35 | flag = pygame.image.load("assets/images/flag.png") 36 | flag = pygame.transform.scale(flag, (cell_size, cell_size)) 37 | mine = pygame.image.load("assets/images/mine.png") 38 | mine = pygame.transform.scale(mine, (cell_size, cell_size)) 39 | 40 | # Create game and AI agent 41 | game = Minesweeper(height=HEIGHT, width=WIDTH, mines=MINES) 42 | ai = MinesweeperAI(height=HEIGHT, width=WIDTH) 43 | 44 | # Keep track of revealed cells, flagged cells, and if a mine was hit 45 | revealed = set() 46 | flags = set() 47 | lost = False 48 | 49 | # Show instructions initially 50 | instructions = True 51 | 52 | while True: 53 | 54 | # Check if game quit 55 | for event in pygame.event.get(): 56 | if event.type == pygame.QUIT: 57 | sys.exit() 58 | 59 | screen.fill(BLACK) 60 | 61 | # Show game instructions 62 | if instructions: 63 | 64 | # Title 65 | title = largeFont.render("Play Minesweeper", True, WHITE) 66 | titleRect = title.get_rect() 67 | titleRect.center = ((width / 2), 50) 68 | screen.blit(title, titleRect) 69 | 70 | # Rules 71 | rules = [ 72 | "Click a cell to reveal it.", 73 | "Right-click a cell to mark it as a mine.", 74 | "Mark all mines successfully to win!" 75 | ] 76 | for i, rule in enumerate(rules): 77 | line = smallFont.render(rule, True, WHITE) 78 | lineRect = line.get_rect() 79 | lineRect.center = ((width / 2), 150 + 30 * i) 80 | screen.blit(line, lineRect) 81 | 82 | # Play game button 83 | buttonRect = pygame.Rect((width / 4), (3 / 4) * height, width / 2, 50) 84 | buttonText = mediumFont.render("Play Game", True, BLACK) 85 | buttonTextRect = buttonText.get_rect() 86 | buttonTextRect.center = buttonRect.center 87 | pygame.draw.rect(screen, WHITE, buttonRect) 88 | screen.blit(buttonText, buttonTextRect) 89 | 90 | # Check if play button clicked 91 | click, _, _ = pygame.mouse.get_pressed() 92 | if click == 1: 93 | mouse = pygame.mouse.get_pos() 94 | if buttonRect.collidepoint(mouse): 95 | instructions = False 96 | time.sleep(0.3) 97 | 98 | pygame.display.flip() 99 | continue 100 | 101 | # Draw board 102 | cells = [] 103 | for i in range(HEIGHT): 104 | row = [] 105 | for j in range(WIDTH): 106 | 107 | # Draw rectangle for cell 108 | rect = pygame.Rect( 109 | board_origin[0] + j * cell_size, 110 | board_origin[1] + i * cell_size, 111 | cell_size, cell_size 112 | ) 113 | pygame.draw.rect(screen, GRAY, rect) 114 | pygame.draw.rect(screen, WHITE, rect, 3) 115 | 116 | # Add a mine, flag, or number if needed 117 | if game.is_mine((i, j)) and lost: 118 | screen.blit(mine, rect) 119 | elif (i, j) in flags: 120 | screen.blit(flag, rect) 121 | elif (i, j) in revealed: 122 | neighbors = smallFont.render( 123 | str(game.nearby_mines((i, j))), 124 | True, BLACK 125 | ) 126 | neighborsTextRect = neighbors.get_rect() 127 | neighborsTextRect.center = rect.center 128 | screen.blit(neighbors, neighborsTextRect) 129 | 130 | row.append(rect) 131 | cells.append(row) 132 | 133 | # AI Move button 134 | aiButton = pygame.Rect( 135 | (2 / 3) * width + BOARD_PADDING, (1 / 3) * height - 50, 136 | (width / 3) - BOARD_PADDING * 2, 50 137 | ) 138 | buttonText = mediumFont.render("AI Move", True, BLACK) 139 | buttonRect = buttonText.get_rect() 140 | buttonRect.center = aiButton.center 141 | pygame.draw.rect(screen, WHITE, aiButton) 142 | screen.blit(buttonText, buttonRect) 143 | 144 | # Reset button 145 | resetButton = pygame.Rect( 146 | (2 / 3) * width + BOARD_PADDING, (1 / 3) * height + 20, 147 | (width / 3) - BOARD_PADDING * 2, 50 148 | ) 149 | buttonText = mediumFont.render("Reset", True, BLACK) 150 | buttonRect = buttonText.get_rect() 151 | buttonRect.center = resetButton.center 152 | pygame.draw.rect(screen, WHITE, resetButton) 153 | screen.blit(buttonText, buttonRect) 154 | 155 | # Display text 156 | text = "Lost" if lost else "Won" if game.mines == flags else "" 157 | text = mediumFont.render(text, True, WHITE) 158 | textRect = text.get_rect() 159 | textRect.center = ((5 / 6) * width, (2 / 3) * height) 160 | screen.blit(text, textRect) 161 | 162 | move = None 163 | 164 | left, _, right = pygame.mouse.get_pressed() 165 | 166 | # Check for a right-click to toggle flagging 167 | if right == 1 and not lost: 168 | mouse = pygame.mouse.get_pos() 169 | for i in range(HEIGHT): 170 | for j in range(WIDTH): 171 | if cells[i][j].collidepoint(mouse) and (i, j) not in revealed: 172 | if (i, j) in flags: 173 | flags.remove((i, j)) 174 | else: 175 | flags.add((i, j)) 176 | time.sleep(0.2) 177 | 178 | elif left == 1: 179 | mouse = pygame.mouse.get_pos() 180 | 181 | # If AI button clicked, make an AI move 182 | if aiButton.collidepoint(mouse) and not lost: 183 | move = ai.make_safe_move() 184 | if move is None: 185 | move = ai.make_random_move() 186 | if move is None: 187 | flags = ai.mines.copy() 188 | print("No moves left to make.") 189 | else: 190 | print("No known safe moves, AI making random move.") 191 | else: 192 | print("AI making safe move.") 193 | time.sleep(0.2) 194 | 195 | # Reset game state 196 | elif resetButton.collidepoint(mouse): 197 | game = Minesweeper(height=HEIGHT, width=WIDTH, mines=MINES) 198 | ai = MinesweeperAI(height=HEIGHT, width=WIDTH) 199 | revealed = set() 200 | flags = set() 201 | lost = False 202 | continue 203 | 204 | # User-made move 205 | elif not lost: 206 | for i in range(HEIGHT): 207 | for j in range(WIDTH): 208 | if (cells[i][j].collidepoint(mouse) 209 | and (i, j) not in flags 210 | and (i, j) not in revealed): 211 | move = (i, j) 212 | 213 | # Make move and update AI knowledge 214 | if move: 215 | if game.is_mine(move): 216 | lost = True 217 | else: 218 | nearby = game.nearby_mines(move) 219 | revealed.add(move) 220 | ai.add_knowledge(move, nearby) 221 | 222 | pygame.display.flip() 223 | -------------------------------------------------------------------------------- /Knights/logic.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | 3 | 4 | class Sentence(): 5 | 6 | def evaluate(self, model): 7 | """Evaluates the logical sentence.""" 8 | raise Exception("nothing to evaluate") 9 | 10 | def formula(self): 11 | """Returns string formula representing logical sentence.""" 12 | return "" 13 | 14 | def symbols(self): 15 | """Returns a set of all symbols in the logical sentence.""" 16 | return set() 17 | 18 | @classmethod 19 | def validate(cls, sentence): 20 | if not isinstance(sentence, Sentence): 21 | raise TypeError("must be a logical sentence") 22 | 23 | @classmethod 24 | def parenthesize(cls, s): 25 | """Parenthesizes an expression if not already parenthesized.""" 26 | def balanced(s): 27 | """Checks if a string has balanced parentheses.""" 28 | count = 0 29 | for c in s: 30 | if c == "(": 31 | count += 1 32 | elif c == ")": 33 | if count <= 0: 34 | return False 35 | count -= 1 36 | return count == 0 37 | if not len(s) or s.isalpha() or ( 38 | s[0] == "(" and s[-1] == ")" and balanced(s[1:-1]) 39 | ): 40 | return s 41 | else: 42 | return f"({s})" 43 | 44 | 45 | class Symbol(Sentence): 46 | 47 | def __init__(self, name): 48 | self.name = name 49 | 50 | def __eq__(self, other): 51 | return isinstance(other, Symbol) and self.name == other.name 52 | 53 | def __hash__(self): 54 | return hash(("symbol", self.name)) 55 | 56 | def __repr__(self): 57 | return self.name 58 | 59 | def evaluate(self, model): 60 | try: 61 | return bool(model[self.name]) 62 | except KeyError: 63 | raise Exception(f"variable {self.name} not in model") 64 | 65 | def formula(self): 66 | return self.name 67 | 68 | def symbols(self): 69 | return {self.name} 70 | 71 | 72 | class Not(Sentence): 73 | def __init__(self, operand): 74 | Sentence.validate(operand) 75 | self.operand = operand 76 | 77 | def __eq__(self, other): 78 | return isinstance(other, Not) and self.operand == other.operand 79 | 80 | def __hash__(self): 81 | return hash(("not", hash(self.operand))) 82 | 83 | def __repr__(self): 84 | return f"Not({self.operand})" 85 | 86 | def evaluate(self, model): 87 | return not self.operand.evaluate(model) 88 | 89 | def formula(self): 90 | return "¬" + Sentence.parenthesize(self.operand.formula()) 91 | 92 | def symbols(self): 93 | return self.operand.symbols() 94 | 95 | 96 | class And(Sentence): 97 | def __init__(self, *conjuncts): 98 | for conjunct in conjuncts: 99 | Sentence.validate(conjunct) 100 | self.conjuncts = list(conjuncts) 101 | 102 | def __eq__(self, other): 103 | return isinstance(other, And) and self.conjuncts == other.conjuncts 104 | 105 | def __hash__(self): 106 | return hash( 107 | ("and", tuple(hash(conjunct) for conjunct in self.conjuncts)) 108 | ) 109 | 110 | def __repr__(self): 111 | conjunctions = ", ".join( 112 | [str(conjunct) for conjunct in self.conjuncts] 113 | ) 114 | return f"And({conjunctions})" 115 | 116 | def add(self, conjunct): 117 | Sentence.validate(conjunct) 118 | self.conjuncts.append(conjunct) 119 | 120 | def evaluate(self, model): 121 | return all(conjunct.evaluate(model) for conjunct in self.conjuncts) 122 | 123 | def formula(self): 124 | if len(self.conjuncts) == 1: 125 | return self.conjuncts[0].formula() 126 | return " ∧ ".join([Sentence.parenthesize(conjunct.formula()) 127 | for conjunct in self.conjuncts]) 128 | 129 | def symbols(self): 130 | return set.union(*[conjunct.symbols() for conjunct in self.conjuncts]) 131 | 132 | 133 | class Or(Sentence): 134 | def __init__(self, *disjuncts): 135 | for disjunct in disjuncts: 136 | Sentence.validate(disjunct) 137 | self.disjuncts = list(disjuncts) 138 | 139 | def __eq__(self, other): 140 | return isinstance(other, Or) and self.disjuncts == other.disjuncts 141 | 142 | def __hash__(self): 143 | return hash( 144 | ("or", tuple(hash(disjunct) for disjunct in self.disjuncts)) 145 | ) 146 | 147 | def __repr__(self): 148 | disjuncts = ", ".join([str(disjunct) for disjunct in self.disjuncts]) 149 | return f"Or({disjuncts})" 150 | 151 | def evaluate(self, model): 152 | return any(disjunct.evaluate(model) for disjunct in self.disjuncts) 153 | 154 | def formula(self): 155 | if len(self.disjuncts) == 1: 156 | return self.disjuncts[0].formula() 157 | return " ∨ ".join([Sentence.parenthesize(disjunct.formula()) 158 | for disjunct in self.disjuncts]) 159 | 160 | def symbols(self): 161 | return set.union(*[disjunct.symbols() for disjunct in self.disjuncts]) 162 | 163 | 164 | class Implication(Sentence): 165 | def __init__(self, antecedent, consequent): 166 | Sentence.validate(antecedent) 167 | Sentence.validate(consequent) 168 | self.antecedent = antecedent 169 | self.consequent = consequent 170 | 171 | def __eq__(self, other): 172 | return (isinstance(other, Implication) 173 | and self.antecedent == other.antecedent 174 | and self.consequent == other.consequent) 175 | 176 | def __hash__(self): 177 | return hash(("implies", hash(self.antecedent), hash(self.consequent))) 178 | 179 | def __repr__(self): 180 | return f"Implication({self.antecedent}, {self.consequent})" 181 | 182 | def evaluate(self, model): 183 | return ((not self.antecedent.evaluate(model)) 184 | or self.consequent.evaluate(model)) 185 | 186 | def formula(self): 187 | antecedent = Sentence.parenthesize(self.antecedent.formula()) 188 | consequent = Sentence.parenthesize(self.consequent.formula()) 189 | return f"{antecedent} => {consequent}" 190 | 191 | def symbols(self): 192 | return set.union(self.antecedent.symbols(), self.consequent.symbols()) 193 | 194 | 195 | class Biconditional(Sentence): 196 | def __init__(self, left, right): 197 | Sentence.validate(left) 198 | Sentence.validate(right) 199 | self.left = left 200 | self.right = right 201 | 202 | def __eq__(self, other): 203 | return (isinstance(other, Biconditional) 204 | and self.left == other.left 205 | and self.right == other.right) 206 | 207 | def __hash__(self): 208 | return hash(("biconditional", hash(self.left), hash(self.right))) 209 | 210 | def __repr__(self): 211 | return f"Biconditional({self.left}, {self.right})" 212 | 213 | def evaluate(self, model): 214 | return ((self.left.evaluate(model) 215 | and self.right.evaluate(model)) 216 | or (not self.left.evaluate(model) 217 | and not self.right.evaluate(model))) 218 | 219 | def formula(self): 220 | left = Sentence.parenthesize(str(self.left)) 221 | right = Sentence.parenthesize(str(self.right)) 222 | return f"{left} <=> {right}" 223 | 224 | def symbols(self): 225 | return set.union(self.left.symbols(), self.right.symbols()) 226 | 227 | 228 | def model_check(knowledge, query): 229 | """Checks if knowledge base entails query.""" 230 | 231 | def check_all(knowledge, query, symbols, model): 232 | """Checks if knowledge base entails query, given a particular model.""" 233 | 234 | # If model has an assignment for each symbol 235 | if not symbols: 236 | 237 | # If knowledge base is true in model, then query must also be true 238 | if knowledge.evaluate(model): 239 | return query.evaluate(model) 240 | return True 241 | else: 242 | 243 | # Choose one of the remaining unused symbols 244 | remaining = symbols.copy() 245 | p = remaining.pop() 246 | 247 | # Create a model where the symbol is true 248 | model_true = model.copy() 249 | model_true[p] = True 250 | 251 | # Create a model where the symbol is false 252 | model_false = model.copy() 253 | model_false[p] = False 254 | 255 | # Ensure entailment holds in both models 256 | return (check_all(knowledge, query, remaining, model_true) and 257 | check_all(knowledge, query, remaining, model_false)) 258 | 259 | # Get all symbols in both knowledge and query 260 | symbols = set.union(knowledge.symbols(), query.symbols()) 261 | 262 | # Check that knowledge entails query 263 | return check_all(knowledge, query, symbols, dict()) 264 | -------------------------------------------------------------------------------- /Nim/visual.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import sys 3 | import time 4 | 5 | # import tictactoe as ttt 6 | from nim import Nim, NimAI 7 | 8 | CIRCLE = 'O' 9 | EMPTY = None 10 | N_GAMES = 10001 11 | VISUALIZE_EACH = 50 12 | SLOW_VISUALIZE_EACH = 1000 13 | 14 | pygame.init() 15 | size = width, height = 600, 400 16 | 17 | # Colors 18 | black = (0, 0, 0) 19 | white = (255, 255, 255) 20 | red = (255, 0, 0) 21 | blue = (0, 0, 255) 22 | 23 | screen = pygame.display.set_mode(size) 24 | 25 | mediumFont = pygame.font.Font("OpenSans-Regular.ttf", 28) 26 | largeFont = pygame.font.Font("OpenSans-Regular.ttf", 40) 27 | moveFont = pygame.font.Font("OpenSans-Regular.ttf", 60) 28 | 29 | user = None 30 | # board = ttt.initial_state() 31 | game = Nim() 32 | INITIAL_PILE = game.piles.copy() 33 | # Keep track of last move made by either player 34 | last = { 35 | 0: {"state": None, "action": None}, 36 | 1: {"state": None, "action": None} 37 | } 38 | 39 | piles = game.piles 40 | player = NimAI() 41 | 42 | def clean_board(): 43 | # initialize board 44 | board = [] 45 | for j in range(len(INITIAL_PILE)): 46 | board.append([EMPTY] * max(INITIAL_PILE)) 47 | 48 | return board 49 | 50 | def populate_board(piles): 51 | board = clean_board() 52 | 53 | for i, pile in enumerate(piles): 54 | for j in range(pile): 55 | board[i][j] = CIRCLE 56 | 57 | return board 58 | 59 | def draw_game_state(screen, board, title=''): 60 | # Draw game board 61 | title = largeFont.render(title, True, white) 62 | titleRect = title.get_rect() 63 | titleRect.center = ((width / 2), 50) 64 | screen.blit(title, titleRect) 65 | 66 | tile_size = 60 67 | tile_origin = (width / 2 - (3 * tile_size), 68 | height / 2 - (1.5 * tile_size)) 69 | 70 | for i in range(len(board)): 71 | row = [] 72 | for j in range(len(board[0])): 73 | rect = pygame.Rect( 74 | tile_origin[0] + j * tile_size, 75 | tile_origin[1] + i * tile_size, 76 | tile_size, tile_size 77 | ) 78 | pygame.draw.rect(screen, white, rect, 3) 79 | 80 | if board[i][j] != None:# ttt.EMPTY: 81 | move = moveFont.render(board[i][j], True, white) 82 | moveRect = move.get_rect() 83 | moveRect.center = rect.center 84 | screen.blit(move, moveRect) 85 | 86 | pygame.display.flip() 87 | 88 | return screen, board 89 | 90 | def highlight_move(screen, board, action, player, title=''): 91 | # Draw game board 92 | title = largeFont.render(title, True, white) 93 | titleRect = title.get_rect() 94 | titleRect.center = ((width / 2), 50) 95 | screen.blit(title, titleRect) 96 | 97 | 98 | tile_size = 60 99 | tile_origin = (width / 2 - (3 * tile_size), 100 | height / 2 - (1.5 * tile_size)) 101 | 102 | pile_number = action[0] 103 | count = action[1] 104 | 105 | for i in range(len(board)): 106 | row = [] 107 | for j in reversed(range(len(board[0]))): 108 | rect = pygame.Rect( 109 | tile_origin[0] + j * tile_size, 110 | tile_origin[1] + i * tile_size, 111 | tile_size, tile_size 112 | ) 113 | pygame.draw.rect(screen, white, rect, 3) 114 | 115 | 116 | if board[i][j] != None and i == pile_number: 117 | if count > 0: 118 | if player: 119 | move = moveFont.render('X', True, red) 120 | else: 121 | move = moveFont.render('X', True, blue) 122 | moveRect = move.get_rect() 123 | moveRect.center = rect.center 124 | screen.blit(move, moveRect) 125 | count -= 1 126 | 127 | pygame.display.flip() 128 | 129 | return screen, board 130 | 131 | 132 | 133 | board = clean_board() 134 | ai_turn = False 135 | 136 | # General Game Loop 137 | while True: 138 | 139 | for event in pygame.event.get(): 140 | if event.type == pygame.QUIT: 141 | sys.exit() 142 | 143 | screen.fill(black) 144 | 145 | # Let user choose a player. 146 | if user is None: 147 | 148 | # Draw title 149 | title = largeFont.render("Play Nim", True, white) 150 | titleRect = title.get_rect() 151 | titleRect.center = ((width / 2), 50) 152 | screen.blit(title, titleRect) 153 | 154 | # Draw buttons 155 | playXButton = pygame.Rect((width / 8), (height / 2), width / 4, 50) 156 | playX = mediumFont.render("Train AI", True, black) 157 | playXRect = playX.get_rect() 158 | playXRect.center = playXButton.center 159 | pygame.draw.rect(screen, white, playXButton) 160 | screen.blit(playX, playXRect) 161 | 162 | playOButton = pygame.Rect(5 * (width / 8), (height / 2), width / 4, 50) 163 | playO = mediumFont.render("Play", True, black) 164 | playORect = playO.get_rect() 165 | playORect.center = playOButton.center 166 | pygame.draw.rect(screen, white, playOButton) 167 | screen.blit(playO, playORect) 168 | 169 | # Check if button is clicked 170 | click, _, _ = pygame.mouse.get_pressed() 171 | if click == 1: 172 | mouse = pygame.mouse.get_pos() 173 | if playXButton.collidepoint(mouse): 174 | time.sleep(0.2) 175 | user = NimAI() 176 | 177 | elif playOButton.collidepoint(mouse): 178 | time.sleep(0.2) 179 | pass 180 | 181 | else: 182 | if playXButton: 183 | print('Training...') 184 | # play 10 games 185 | for game_number in range(N_GAMES): 186 | # specific game loop 187 | game = Nim() 188 | piles = game.piles 189 | title = f'Game #{game_number}' 190 | 191 | while True: 192 | screen.fill(black) 193 | if game.piles == INITIAL_PILE: 194 | fl_first_move = True 195 | 196 | board = populate_board(piles) 197 | # Print content of piles 198 | 199 | if game_number % VISUALIZE_EACH == 0: 200 | screen, board = draw_game_state(screen, board, title) 201 | #time.sleep(0.1) 202 | 203 | # Keep track of current state and action 204 | state = game.piles.copy() 205 | action = player.choose_action(game.piles, epsilon=True) 206 | 207 | # Keep track of last state and action 208 | last[game.player]["state"] = state 209 | last[game.player]["action"] = action 210 | 211 | # Make move (game.move switches player) 212 | game.move(action) 213 | new_state = game.piles.copy() 214 | 215 | if game_number % SLOW_VISUALIZE_EACH == 0 and game_number > 0: 216 | # highlight best possible actions 217 | if fl_first_move: 218 | print(('Red' if game.player else 'Blue') + ' player begins') 219 | fl_first_move = False 220 | if game.winner is not None: 221 | print(('Red' if not game.winner else 'Blue') + ' player wins') 222 | # a move that leads to a winning move makes the player a loser 223 | 224 | time.sleep(0.2) 225 | screen, board = highlight_move(screen, board, action, game.player, title) 226 | time.sleep(1) 227 | 228 | # When game is over, update Q values with rewards 229 | if game.winner is not None: 230 | # if that move led to a game.winner 231 | # then it means that it was a LOSING move. 232 | # so we need to give a reward of -1 233 | player.update(state, action, new_state, -1) 234 | 235 | # if that move was a LOSING move, the move before that 236 | # has led to a WINNING, which means we need to give 237 | # a reward of +1 238 | player.update( 239 | last[game.player]["state"], 240 | last[game.player]["action"], 241 | new_state, 242 | 1 243 | ) 244 | # it's interesting because the AI won and lost at the same time, 245 | # both leading to a new knowledge/reward. 246 | break 247 | 248 | 249 | # If game is continuing, no rewards yet 250 | elif last[game.player]["state"] is not None: 251 | player.update( 252 | last[game.player]["state"], 253 | last[game.player]["action"], 254 | new_state, 255 | 0 256 | ) 257 | 258 | # exit game 259 | print(dict(sorted(player.q.items(), key=lambda x : -x[1])[:10])) 260 | break 261 | 262 | pygame.display.flip() 263 | 264 | 265 | -------------------------------------------------------------------------------- /Nim/nim.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import time 4 | 5 | 6 | class Nim(): 7 | 8 | def __init__(self, initial=[1, 3, 5, 7]): 9 | """ 10 | Initialize game board. 11 | Each game board has 12 | - `piles`: a list of how many elements remain in each pile 13 | - `player`: 0 or 1 to indicate which player's turn 14 | - `winner`: None, 0, or 1 to indicate who the winner is 15 | """ 16 | self.piles = initial.copy() 17 | self.player = 0 18 | self.winner = None 19 | 20 | @classmethod 21 | def available_actions(cls, piles): 22 | """ 23 | Nim.available_actions(piles) takes a `piles` list as input 24 | and returns all of the available actions `(i, j)` in that state. 25 | 26 | Action `(i, j)` represents the action of removing `j` items 27 | from pile `i` (where piles are 0-indexed). 28 | """ 29 | actions = set() 30 | for i, pile in enumerate(piles): 31 | for j in range(1, pile + 1): 32 | actions.add((i, j)) 33 | return actions 34 | 35 | @classmethod 36 | def other_player(cls, player): 37 | """ 38 | Nim.other_player(player) returns the player that is not 39 | `player`. Assumes `player` is either 0 or 1. 40 | """ 41 | return 0 if player == 1 else 1 42 | 43 | def switch_player(self): 44 | """ 45 | Switch the current player to the other player. 46 | """ 47 | self.player = Nim.other_player(self.player) 48 | 49 | def move(self, action): 50 | """ 51 | Make the move `action` for the current player. 52 | `action` must be a tuple `(i, j)`. 53 | """ 54 | pile, count = action 55 | 56 | # Check for errors 57 | if self.winner is not None: 58 | raise Exception("Game already won") 59 | elif pile < 0 or pile >= len(self.piles): 60 | raise Exception("Invalid pile") 61 | elif count < 1 or count > self.piles[pile]: 62 | raise Exception("Invalid number of objects") 63 | 64 | # Update pile 65 | self.piles[pile] -= count 66 | self.switch_player() 67 | 68 | # Check for a winner 69 | if all(pile == 0 for pile in self.piles): 70 | self.winner = self.player 71 | 72 | @classmethod 73 | def state(cls, piles): 74 | """ 75 | Nim.state(piles) takes a `piles` list as input 76 | """ 77 | pass 78 | 79 | 80 | class NimAI(): 81 | 82 | def __init__(self, alpha=0.5, epsilon=0.1): 83 | """ 84 | Initialize AI with an empty Q-learning dictionary, 85 | an alpha (learning) rate, and an epsilon rate. 86 | 87 | The Q-learning dictionary maps `(state, action)` 88 | pairs to a Q-value (a number). 89 | - `state` is a tuple of remaining piles, e.g. (1, 1, 4, 4) 90 | - `action` is a tuple `(i, j)` for an action 91 | """ 92 | self.q = dict() 93 | self.alpha = alpha 94 | self.epsilon = epsilon 95 | 96 | def update(self, old_state, action, new_state, reward): 97 | """ 98 | Update Q-learning model, given an old state, an action taken 99 | in that state, a new resulting state, and the reward received 100 | from taking that action. 101 | """ 102 | old = self.get_q_value(old_state, action) 103 | best_future = self.best_future_reward(new_state) 104 | self.update_q_value(old_state, action, old, reward, best_future) 105 | 106 | def get_q_value(self, state, action): 107 | """ 108 | Return the Q-value for the state `state` and the action `action`. 109 | If no Q-value exists yet in `self.q`, return 0. 110 | """ 111 | key = (tuple(state), action) 112 | return self.q.get(key, 0) 113 | 114 | 115 | def update_q_value(self, state, action, old_q, reward, future_rewards): 116 | """ 117 | Update the Q-value for the state `state` and the action `action` 118 | given the previous Q-value `old_q`, a current reward `reward`, 119 | and an estiamte of future rewards `future_rewards`. 120 | 121 | Use the formula: 122 | 123 | Q(s, a) <- old value estimate 124 | + alpha * (new value estimate - old value estimate) 125 | 126 | where `old value estimate` is the previous Q-value, 127 | `alpha` is the learning rate, and `new value estimate` 128 | is the sum of the current reward and estimated future rewards. 129 | """ 130 | new_value_estimate = reward + future_rewards 131 | new_q = old_q + self.alpha * (new_value_estimate - old_q) 132 | 133 | key = (tuple(state), action) 134 | self.q[key] = new_q 135 | 136 | return 137 | 138 | def best_future_reward(self, state): 139 | """ 140 | Given a state `state`, consider all possible `(state, action)` 141 | pairs available in that state and return the maximum of all 142 | of their Q-values. 143 | 144 | Use 0 as the Q-value if a `(state, action)` pair has no 145 | Q-value in `self.q`. If there are no available actions in 146 | `state`, return 0. 147 | """ 148 | rewards = [] 149 | for possible_action in Nim.available_actions(state): 150 | key = (tuple(state), possible_action) 151 | rewards.append(self.q.get(key, 0)) 152 | 153 | if not len(rewards): 154 | return 0 155 | 156 | return max(rewards) 157 | 158 | def choose_action(self, state, epsilon=True): 159 | """ 160 | Given a state `state`, return an action `(i, j)` to take. 161 | 162 | If `epsilon` is `False`, then return the best action 163 | available in the state (the one with the highest Q-value, 164 | using 0 for pairs that have no Q-values). 165 | 166 | If `epsilon` is `True`, then with probability 167 | `self.epsilon` choose a random available action, 168 | otherwise choose the best action available. 169 | 170 | If multiple actions have the same Q-value, any of those 171 | options is an acceptable return value. 172 | """ 173 | possible_actions = Nim.available_actions(state) 174 | 175 | # assume you are not going to perform a random choice 176 | fl_random_choice = False 177 | if epsilon: 178 | # it means we're training 179 | if random.random() < self.epsilon: 180 | fl_random_choice = True 181 | 182 | if fl_random_choice: 183 | return possible_actions.pop() 184 | 185 | # TODO: implement best action 186 | current_reward = -1e6 187 | candidate_action = None 188 | 189 | for action in possible_actions: 190 | key = (tuple(state), action) 191 | candidate_reward = self.q.get(key, 0) 192 | if candidate_reward > current_reward: 193 | current_reward = candidate_reward 194 | candidate_action = action 195 | 196 | if candidate_action: 197 | return candidate_action 198 | 199 | return possible_actions.pop() 200 | 201 | 202 | 203 | def train(n): 204 | """ 205 | Train an AI by playing `n` games against itself. 206 | """ 207 | 208 | player = NimAI() 209 | 210 | # Play n games 211 | for i in range(n): 212 | print(f"Playing training game {i + 1}") 213 | game = Nim() 214 | 215 | # Keep track of last move made by either player 216 | last = { 217 | 0: {"state": None, "action": None}, 218 | 1: {"state": None, "action": None} 219 | } 220 | 221 | # Game loop 222 | while True: 223 | 224 | # Keep track of current state and action 225 | state = game.piles.copy() 226 | action = player.choose_action(game.piles, epsilon=True) 227 | 228 | # Keep track of last state and action 229 | last[game.player]["state"] = state 230 | last[game.player]["action"] = action 231 | 232 | # Make move (game.move switches player) 233 | game.move(action) 234 | new_state = game.piles.copy() 235 | 236 | # When game is over, update Q values with rewards 237 | if game.winner is not None: 238 | # if that move led to a game.winner 239 | # then it means that it was a LOSING move. 240 | # so we need to give a reward of -1 241 | player.update(state, action, new_state, -1) 242 | 243 | # if that move was a LOSING move, the move before that 244 | # has led to a WINNING, which means we need to give 245 | # a reward of +1 246 | player.update( 247 | last[game.player]["state"], 248 | last[game.player]["action"], 249 | new_state, 250 | 1 251 | ) 252 | # it's interesting because the AI won and lost at the same time, 253 | # both leading to a new knowledge/reward. 254 | break 255 | 256 | # If game is continuing, no rewards yet 257 | elif last[game.player]["state"] is not None: 258 | player.update( 259 | last[game.player]["state"], 260 | last[game.player]["action"], 261 | new_state, 262 | 0 263 | ) 264 | # if (i+1) == n: 265 | # print('---------') 266 | # print(n) 267 | # for k, v in sorted(player.q.items(), key=lambda x : -x[1]): 268 | # print(k, v) 269 | 270 | print("Done training") 271 | 272 | # Return the trained AI 273 | return player 274 | 275 | 276 | def play(ai, human_player=None): 277 | """ 278 | Play human game against the AI. 279 | `human_player` can be set to 0 or 1 to specify whether 280 | human player moves first or second. 281 | """ 282 | 283 | # If no player order set, choose human's order randomly 284 | if human_player is None: 285 | human_player = random.randint(0, 1) 286 | 287 | # Create new game 288 | game = Nim() 289 | 290 | # Game loop 291 | while True: 292 | 293 | # Print contents of piles 294 | print() 295 | print("Piles:") 296 | for i, pile in enumerate(game.piles): 297 | print(f"Pile {i}: {pile}") 298 | print() 299 | 300 | # Compute available actions 301 | available_actions = Nim.available_actions(game.piles) 302 | time.sleep(1) 303 | 304 | # Let human make a move 305 | if game.player == human_player: 306 | print("Your Turn") 307 | while True: 308 | pile = int(input("Choose Pile: ")) 309 | count = int(input("Choose Count: ")) 310 | if (pile, count) in available_actions: 311 | break 312 | print("Invalid move, try again.") 313 | 314 | # Have AI make a move 315 | else: 316 | print("AI's Turn") 317 | pile, count = ai.choose_action(game.piles, epsilon=False) 318 | print(f"AI chose to take {count} from pile {pile}.") 319 | 320 | # Make move 321 | game.move((pile, count)) 322 | 323 | # Check for winner 324 | if game.winner is not None: 325 | print() 326 | print("GAME OVER") 327 | winner = "Human" if game.winner == human_player else "AI" 328 | print(f"Winner is {winner}") 329 | return 330 | -------------------------------------------------------------------------------- /Crossword/generate.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from crossword import * 4 | 5 | 6 | class CrosswordCreator(): 7 | 8 | def __init__(self, crossword): 9 | """ 10 | Create new CSP crossword generate. 11 | """ 12 | self.crossword = crossword 13 | self.domains = { 14 | var: self.crossword.words.copy() 15 | for var in self.crossword.variables 16 | } 17 | 18 | def letter_grid(self, assignment): 19 | """ 20 | Return 2D array representing a given assignment. 21 | """ 22 | letters = [ 23 | [None for _ in range(self.crossword.width)] 24 | for _ in range(self.crossword.height) 25 | ] 26 | for variable, word in assignment.items(): 27 | direction = variable.direction 28 | for k in range(len(word)): 29 | i = variable.i + (k if direction == Variable.DOWN else 0) 30 | j = variable.j + (k if direction == Variable.ACROSS else 0) 31 | letters[i][j] = word[k] 32 | return letters 33 | 34 | def print(self, assignment): 35 | """ 36 | Print crossword assignment to the terminal. 37 | """ 38 | letters = self.letter_grid(assignment) 39 | for i in range(self.crossword.height): 40 | for j in range(self.crossword.width): 41 | if self.crossword.structure[i][j]: 42 | print(letters[i][j] or " ", end="") 43 | else: 44 | print("█", end="") 45 | print() 46 | 47 | def save(self, assignment, filename): 48 | """ 49 | Save crossword assignment to an image file. 50 | """ 51 | from PIL import Image, ImageDraw, ImageFont 52 | cell_size = 100 53 | cell_border = 2 54 | interior_size = cell_size - 2 * cell_border 55 | letters = self.letter_grid(assignment) 56 | 57 | # Create a blank canvas 58 | img = Image.new( 59 | "RGBA", 60 | (self.crossword.width * cell_size, 61 | self.crossword.height * cell_size), 62 | "black" 63 | ) 64 | font = ImageFont.truetype("assets/fonts/OpenSans-Regular.ttf", 80) 65 | draw = ImageDraw.Draw(img) 66 | 67 | for i in range(self.crossword.height): 68 | for j in range(self.crossword.width): 69 | 70 | rect = [ 71 | (j * cell_size + cell_border, 72 | i * cell_size + cell_border), 73 | ((j + 1) * cell_size - cell_border, 74 | (i + 1) * cell_size - cell_border) 75 | ] 76 | if self.crossword.structure[i][j]: 77 | draw.rectangle(rect, fill="white") 78 | if letters[i][j]: 79 | w, h = draw.textsize(letters[i][j], font=font) 80 | draw.text( 81 | (rect[0][0] + ((interior_size - w) / 2), 82 | rect[0][1] + ((interior_size - h) / 2) - 10), 83 | letters[i][j], fill="black", font=font 84 | ) 85 | 86 | img.save(filename) 87 | 88 | def solve(self): 89 | """ 90 | Enforce node and arc consistency, and then solve the CSP. 91 | """ 92 | self.enforce_node_consistency() 93 | self.ac3() 94 | return self.backtrack(dict()) 95 | 96 | def enforce_node_consistency(self): 97 | """ 98 | Update `self.domains` such that each variable is node-consistent, 99 | meaning that every value in a variable's domain satisfy the unary 100 | constraints. 101 | 102 | Remove any values that are inconsistent with a variable's unary 103 | constraints; in this case, the length of the word. 104 | """ 105 | # Apply unary constraint 106 | variables = self.domains.keys() 107 | for var in variables: 108 | domain = self.domains[var].copy() 109 | for word in self.domains[var]: 110 | if len(word) != var.length: 111 | domain.discard(word) 112 | # overwrite domain 113 | self.domains[var] = domain 114 | 115 | def revise(self, x, y): 116 | """ 117 | Make variable `x` arc consistent with variable `y`. 118 | To do so, remove values from `self.domains[x]` for which there is no 119 | possible corresponding value for `y` in `self.domains[y]`. 120 | 121 | Return True if a revision was made to the domain of `x`; return 122 | False if no revision was made. 123 | """ 124 | # if there are no overlaps between variables, 125 | # then there are no restrictions 126 | if not self.crossword.overlaps[x,y]: 127 | return False 128 | 129 | n1, n2 = self.crossword.overlaps[x,y] 130 | 131 | domain = self.domains[x].copy() 132 | 133 | for d1 in self.domains[x]: 134 | # initialize acceptable flag to latter remove unacceptable domains from x 135 | fl_acceptable = False 136 | 137 | for d2 in self.domains[y]: 138 | # equal words are not acceptable 139 | if d1 == d2: 140 | continue 141 | 142 | if d1[n1] == d2[n2]: 143 | fl_acceptable = True 144 | break 145 | 146 | # if all of them are unnacceptable, remove from x's domain: 147 | if not fl_acceptable: 148 | domain.discard(d1) 149 | 150 | if self.domains[x] == domain: 151 | return False 152 | else: 153 | self.domains[x] = domain 154 | 155 | return True 156 | 157 | def ac3(self, arcs=None): 158 | """ 159 | Update `self.domains` such that each variable is arc consistent. 160 | If `arcs` is None, begin with initial list of all arcs in the problem. 161 | Otherwise, use `arcs` as the initial list of arcs to make consistent. 162 | 163 | Return True if arc consistency is enforced and no domains are empty; 164 | return False if one or more domains end up empty. 165 | """ 166 | if not arcs: 167 | # consider all arcs in the problem 168 | variables = self.domains.keys() 169 | arcs = [] 170 | for v1 in variables: 171 | for v2 in variables: 172 | if v1 == v2: 173 | continue 174 | arc = (v1, v2) 175 | arcs.append(arc) 176 | else: 177 | pass 178 | 179 | while len(arcs) > 0: 180 | v1, v2 = arcs.pop() 181 | 182 | if self.revise(v1, v2): 183 | if not self.domains[v1]: 184 | # there's no way to solve this problem 185 | return False 186 | for vn in (self.crossword.neighbors(v1) - {v2}): 187 | # if we've modified v1's domain, there might 188 | # be some other (vn, v1) that was arc-consistent, but 189 | # now is not anymore. 190 | arcs.append((vn, v1)) 191 | 192 | 193 | return True 194 | 195 | def assignment_complete(self, assignment): 196 | """ 197 | Return True if `assignment` is complete (i.e., assigns a value to each 198 | crossword variable); return False otherwise. 199 | """ 200 | self.consistent(assignment) 201 | variables = set(self.domains.keys()) 202 | for var in variables: 203 | if var not in assignment.keys(): 204 | return False 205 | 206 | if not assignment.get(var): 207 | return False 208 | 209 | return True 210 | 211 | def consistent(self, assignment): 212 | """ 213 | Return True if `assignment` is consistent (i.e., words fit in crossword 214 | puzzle without conflicting characters); return False otherwise. 215 | """ 216 | words = [] 217 | variables = set(assignment.keys()) 218 | 219 | for v1 in variables: 220 | 221 | assigned_word = assignment.get(v1, '') 222 | # if the size differs from expected, not consistent 223 | if not len(assigned_word) == v1.length: 224 | return False 225 | 226 | # append to check whether they have same values afterwards 227 | words.append(assigned_word) 228 | 229 | # if conflicting characters, not consistent 230 | for v2 in variables: 231 | if v1 != v2: 232 | 233 | overlaps = self.crossword.overlaps[v1,v2] 234 | if not overlaps: 235 | continue 236 | n1, n2 = overlaps 237 | if assignment[v1][n1] != assignment[v2][n2]: 238 | return False 239 | 240 | # if there are repeated ones, not consistent 241 | if len(words) != len(set(words)): 242 | return False 243 | 244 | return True 245 | 246 | def order_domain_values(self, var, assignment): 247 | """ 248 | Return a list of values in the domain of `var`, in order by 249 | the number of values they rule out for neighboring variables. 250 | The first value in the list, for example, should be the one 251 | that rules out the fewest values among the neighbors of `var`. 252 | """ 253 | return self.domains[var] 254 | 255 | def select_unassigned_variable(self, assignment): 256 | """ 257 | Return an unassigned variable not already part of `assignment`. 258 | Choose the variable with the minimum number of remaining values 259 | in its domain. If there is a tie, choose the variable with the highest 260 | degree. If there is a tie, any of the tied variables are acceptable 261 | return values. 262 | """ 263 | assigned_variables = set(assignment.keys()) 264 | all_variables = set(self.domains.keys()) 265 | unassigned_variables = all_variables - assigned_variables 266 | chosen = unassigned_variables.pop() 267 | 268 | return chosen 269 | 270 | def backtrack(self, assignment): 271 | """ 272 | Using Backtracking Search, take as input a partial assignment for the 273 | crossword and return a complete assignment if possible to do so. 274 | 275 | `assignment` is a mapping from variables (keys) to words (values). 276 | 277 | If no assignment is possible, return None. 278 | """ 279 | if self.assignment_complete(assignment): 280 | return assignment 281 | 282 | var = self.select_unassigned_variable(assignment) 283 | for value in self.order_domain_values(var, assignment): 284 | assignment[var] = value 285 | if self.consistent(assignment): 286 | result = self.backtrack(assignment) 287 | if result: 288 | return result 289 | else: 290 | assignment.pop(var) 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | def main(): 299 | 300 | # Check usage 301 | if len(sys.argv) not in [3, 4]: 302 | sys.exit("Usage: python generate.py structure words [output]") 303 | 304 | # Parse command-line arguments 305 | structure = sys.argv[1] 306 | words = sys.argv[2] 307 | output = sys.argv[3] if len(sys.argv) == 4 else None 308 | 309 | # Generate crossword 310 | crossword = Crossword(structure, words) 311 | creator = CrosswordCreator(crossword) 312 | assignment = creator.solve() 313 | 314 | # Print result 315 | if assignment is None: 316 | print("No solution.") 317 | else: 318 | creator.print(assignment) 319 | if output: 320 | creator.save(assignment, output) 321 | 322 | 323 | if __name__ == "__main__": 324 | main() 325 | -------------------------------------------------------------------------------- /Minesweeper/minesweeper.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import random 3 | import copy 4 | 5 | 6 | class Minesweeper(): 7 | """ 8 | Minesweeper game representation 9 | """ 10 | 11 | def __init__(self, height=8, width=8, mines=8): 12 | 13 | # Set initial width, height, and number of mines 14 | self.height = height 15 | self.width = width 16 | self.mines = set() 17 | 18 | # Initialize an empty field with no mines 19 | self.board = [] 20 | for i in range(self.height): 21 | row = [] 22 | for j in range(self.width): 23 | row.append(False) 24 | self.board.append(row) 25 | 26 | # Add mines randomly 27 | while len(self.mines) != mines: 28 | i = random.randrange(height) 29 | j = random.randrange(width) 30 | if not self.board[i][j]: 31 | self.mines.add((i, j)) 32 | self.board[i][j] = True 33 | 34 | # At first, player has found no mines 35 | self.mines_found = set() 36 | 37 | def print(self): 38 | """ 39 | Prints a text-based representation 40 | of where mines are located. 41 | """ 42 | for i in range(self.height): 43 | print("--" * self.width + "-") 44 | for j in range(self.width): 45 | if self.board[i][j]: 46 | print("|X", end="") 47 | else: 48 | print("| ", end="") 49 | print("|") 50 | print("--" * self.width + "-") 51 | 52 | def is_mine(self, cell): 53 | i, j = cell 54 | return self.board[i][j] 55 | 56 | def nearby_mines(self, cell): 57 | """ 58 | Returns the number of mines that are 59 | within one row and column of a given cell, 60 | not including the cell itself. 61 | """ 62 | 63 | # Keep count of nearby mines 64 | count = 0 65 | 66 | # Loop over all cells within one row and column 67 | for i in range(cell[0] - 1, cell[0] + 2): 68 | for j in range(cell[1] - 1, cell[1] + 2): 69 | 70 | # Ignore the cell itself 71 | if (i, j) == cell: 72 | continue 73 | 74 | # Update count if cell in bounds and is mine 75 | if 0 <= i < self.height and 0 <= j < self.width: 76 | if self.board[i][j]: 77 | count += 1 78 | 79 | return count 80 | 81 | def won(self): 82 | """ 83 | Checks if all mines have been flagged. 84 | """ 85 | return self.mines_found == self.mines 86 | 87 | 88 | class Sentence(): 89 | """ 90 | Logical statement about a Minesweeper game 91 | A sentence consists of a set of board cells, 92 | and a count of the number of those cells which are mines. 93 | """ 94 | 95 | def __init__(self, cells, count): 96 | self.cells = set(cells) 97 | self.count = count 98 | 99 | def __eq__(self, other): 100 | return self.cells == other.cells and self.count == other.count 101 | 102 | def __str__(self): 103 | return f"{self.cells} = {self.count}" 104 | 105 | def known_mines(self): 106 | """ 107 | Returns the set of all cells in self.cells known to be mines. 108 | """ 109 | if self.count == len(self.cells): 110 | return self.cells 111 | 112 | def known_safes(self): 113 | """ 114 | Returns the set of all cells in self.cells known to be safe. 115 | """ 116 | if self.count == 0: 117 | return self.cells 118 | 119 | def mark_mine(self, cell): 120 | """ 121 | Updates internal knowledge representation given the fact that 122 | a cell is known to be a mine. 123 | """ 124 | if cell in self.cells: 125 | self.cells.remove(cell) 126 | self.count -= 1 127 | else: 128 | pass 129 | 130 | def mark_safe(self, cell): 131 | """ 132 | Updates internal knowledge representation given the fact that 133 | a cell is known to be safe. 134 | """ 135 | if cell in self.cells: 136 | self.cells.remove(cell) 137 | else: 138 | pass 139 | 140 | 141 | class MinesweeperAI(): 142 | """ 143 | Minesweeper game player 144 | """ 145 | 146 | def __init__(self, height=8, width=8): 147 | 148 | # Set initial height and width 149 | self.height = height 150 | self.width = width 151 | 152 | # Keep track of which cells have been clicked on 153 | self.moves_made = set() 154 | 155 | # Keep track of cells known to be safe or mines 156 | self.mines = set() 157 | self.safes = set() 158 | 159 | # List of sentences about the game known to be true 160 | self.knowledge = [] 161 | 162 | def mark_mine(self, cell): 163 | """ 164 | Marks a cell as a mine, and updates all knowledge 165 | to mark that cell as a mine as well. 166 | """ 167 | self.mines.add(cell) 168 | for sentence in self.knowledge: 169 | sentence.mark_mine(cell) 170 | 171 | def mark_safe(self, cell): 172 | """ 173 | Marks a cell as safe, and updates all knowledge 174 | to mark that cell as safe as well. 175 | """ 176 | self.safes.add(cell) 177 | for sentence in self.knowledge: 178 | sentence.mark_safe(cell) 179 | 180 | def add_knowledge(self, cell, count): 181 | """ 182 | Called when the Minesweeper board tells us, for a given 183 | safe cell, how many neighboring cells have mines in them. 184 | 185 | This function should: 186 | 1) mark the cell as a move that has been made 187 | 2) mark the cell as safe 188 | 3) add a new sentence to the AI's knowledge base 189 | based on the value of `cell` and `count` 190 | 4) mark any additional cells as safe or as mines 191 | if it can be concluded based on the AI's knowledge base 192 | 5) add any new sentences to the AI's knowledge base 193 | if they can be inferred from existing knowledge 194 | """ 195 | 196 | # mark the cell as one of the moves made in the game 197 | self.moves_made.add(cell) 198 | 199 | # mark the cell as a safe cell, updating any sequences that contain the cell as well 200 | self.mark_safe(cell) 201 | 202 | # add new sentence to AI knowledge base based on value of cell and count 203 | cells = set() 204 | count_cpy = copy.deepcopy(count) 205 | close_cells = self.return_close_cells(cell) # returns neighbour cells 206 | for cl in close_cells: 207 | if cl in self.mines: 208 | count_cpy -= 1 209 | if cl not in self.mines | self.safes: 210 | cells.add(cl) # only add cells that are of unknown state 211 | 212 | new_sentence = Sentence(cells, count_cpy) # prepare new sentence 213 | 214 | if len(new_sentence.cells) > 0: # add that sentence to knowledge only if it is not empty 215 | self.knowledge.append(new_sentence) 216 | # print(f"Adding new sentence: {new_sentence}") 217 | 218 | # # print("Printing knowledge:") 219 | # for sent in self.knowledge: 220 | # # print(sent) 221 | 222 | # check sentences for new cells that could be marked as safe or as mine 223 | self.check_knowledge() 224 | # print(f"Safe cells: {self.safes - self.moves_made}") 225 | # print(f"Mine cells: {self.mines}") 226 | # print("------------") 227 | 228 | self.extra_inference() 229 | 230 | def return_close_cells(self, cell): 231 | """ 232 | returns cell that are 1 cell away from cell passed in arg 233 | """ 234 | # returns cells close to arg cell by 1 cell 235 | close_cells = set() 236 | for rows in range(self.height): 237 | for columns in range(self.width): 238 | if abs(cell[0] - rows) <= 1 and abs(cell[1] - columns) <= 1 and (rows, columns) != cell: 239 | close_cells.add((rows, columns)) 240 | return close_cells 241 | 242 | def check_knowledge(self): 243 | """ 244 | check knowledge for new safes and mines, updates knowledge if possible 245 | """ 246 | # copies the knowledge to operate on copy 247 | knowledge_copy = copy.deepcopy(self.knowledge) 248 | # iterates through sentences 249 | 250 | for sentence in knowledge_copy: 251 | if len(sentence.cells) == 0: 252 | try: 253 | self.knowledge.remove(sentence) 254 | except ValueError: 255 | pass 256 | # check for possible mines and safes 257 | mines = sentence.known_mines() 258 | safes = sentence.known_safes() 259 | 260 | # update knowledge if mine or safe was found 261 | if mines: 262 | for mine in mines: 263 | # print(f"Marking {mine} as mine") 264 | self.mark_mine(mine) 265 | self.check_knowledge() 266 | if safes: 267 | for safe in safes: 268 | # print(f"Marking {safe} as safe") 269 | self.mark_safe(safe) 270 | self.check_knowledge() 271 | 272 | def extra_inference(self): 273 | """ 274 | update knowledge based on inference 275 | """ 276 | # iterate through pairs of sentences 277 | for sentence1 in self.knowledge: 278 | for sentence2 in self.knowledge: 279 | # check if sentence 1 is subset of sentence 2 280 | if sentence1.cells.issubset(sentence2.cells): 281 | new_cells = sentence2.cells - sentence1.cells 282 | new_count = sentence2.count - sentence1.count 283 | new_sentence = Sentence(new_cells, new_count) 284 | mines = new_sentence.known_mines() 285 | safes = new_sentence.known_safes() 286 | if mines: 287 | for mine in mines: 288 | # print(f"Used inference to mark mine: {mine}") 289 | # print(f"FinalSen: {new_sentence}") 290 | # print(f"Sent1: {sent1copy}") 291 | # print(f"Sent2: {sent2copy}") 292 | self.mark_mine(mine) 293 | 294 | if safes: 295 | for safe in safes: 296 | # print(f"Used inference to mark safe: {safe}") 297 | # print(f"FinalSen: {new_sentence}") 298 | # print(f"Sent1: {sent1copy}") 299 | # print(f"Sent2: {sent2copy}") 300 | self.mark_safe(safe) 301 | 302 | def make_safe_move(self): 303 | """ 304 | Returns a safe cell to choose on the Minesweeper board. 305 | The move must be known to be safe, and not already a move 306 | that has been made. 307 | 308 | This function may use the knowledge in self.mines, self.safes 309 | and self.moves_made, but should not modify any of those values. 310 | """ 311 | for i in self.safes - self.moves_made: 312 | # choose first safe cell that wasn't picked before 313 | # print(f"Making {i} move") 314 | return i 315 | 316 | return None 317 | 318 | def make_random_move(self): 319 | """ 320 | Returns a move to make on the Minesweeper board. 321 | Should choose randomly among cells that: 322 | 1) have not already been chosen, and 323 | 2) are not known to be mines 324 | """ 325 | 326 | maxmoves = self.width * self.height 327 | 328 | while maxmoves > 0: 329 | maxmoves -= 1 330 | 331 | row = random.randrange(self.height) 332 | column = random.randrange(self.width) 333 | 334 | if (row, column) not in self.moves_made | self.mines: 335 | return (row, column) 336 | 337 | return None 338 | --------------------------------------------------------------------------------