@ -0,0 +1,74 @@ | |||||
# Created by .ignore support plugin (hsz.mobi) | |||||
### JetBrains template | |||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider | |||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 | |||||
# User-specific stuff | |||||
.idea/**/workspace.xml | |||||
.idea/**/tasks.xml | |||||
.idea/**/usage.statistics.xml | |||||
.idea/**/dictionaries | |||||
.idea/**/shelf | |||||
# Generated files | |||||
.idea/**/contentModel.xml | |||||
# Sensitive or high-churn files | |||||
.idea/**/dataSources/ | |||||
.idea/**/dataSources.ids | |||||
.idea/**/dataSources.local.xml | |||||
.idea/**/sqlDataSources.xml | |||||
.idea/**/dynamic.xml | |||||
.idea/**/uiDesigner.xml | |||||
.idea/**/dbnavigator.xml | |||||
# Gradle | |||||
.idea/**/gradle.xml | |||||
.idea/**/libraries | |||||
# Gradle and Maven with auto-import | |||||
# When using Gradle or Maven with auto-import, you should exclude module files, | |||||
# since they will be recreated, and may cause churn. Uncomment if using | |||||
# auto-import. | |||||
# .idea/artifacts | |||||
# .idea/compiler.xml | |||||
# .idea/jarRepositories.xml | |||||
# .idea/modules.xml | |||||
# .idea/*.iml | |||||
# .idea/modules | |||||
# *.iml | |||||
# *.ipr | |||||
# CMake | |||||
cmake-build-*/ | |||||
# Mongo Explorer plugin | |||||
.idea/**/mongoSettings.xml | |||||
# File-based project format | |||||
*.iws | |||||
# IntelliJ | |||||
out/ | |||||
# mpeltonen/sbt-idea plugin | |||||
.idea_modules/ | |||||
# JIRA plugin | |||||
atlassian-ide-plugin.xml | |||||
# Cursive Clojure plugin | |||||
.idea/replstate.xml | |||||
# Crashlytics plugin (for Android Studio and IntelliJ) | |||||
com_crashlytics_export_strings.xml | |||||
crashlytics.properties | |||||
crashlytics-build.properties | |||||
fabric.properties | |||||
# Editor-based Rest Client | |||||
.idea/httpRequests | |||||
# Android studio 3.1+ serialized cache file | |||||
.idea/caches/build_file_checksums.ser | |||||
@ -0,0 +1,8 @@ | |||||
# Default ignored files | |||||
/shelf/ | |||||
/workspace.xml | |||||
# Datasource local storage ignored files | |||||
/dataSources/ | |||||
/dataSources.local.xml | |||||
# Editor-based HTTP Client requests | |||||
/httpRequests/ |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<module type="JAVA_MODULE" version="4"> | |||||
<component name="NewModuleRootManager" inherit-compiler-output="true"> | |||||
<exclude-output /> | |||||
<content url="file://$MODULE_DIR$" /> | |||||
<orderEntry type="inheritedJdk" /> | |||||
<orderEntry type="sourceFolder" forTests="false" /> | |||||
</component> | |||||
</module> |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="JavaScriptSettings"> | |||||
<option name="languageLevel" value="ES6" /> | |||||
</component> | |||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="Python 3.8" project-jdk-type="Python SDK"> | |||||
<output url="file://$PROJECT_DIR$/out" /> | |||||
</component> | |||||
</project> |
@ -0,0 +1,8 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ProjectModuleManager"> | |||||
<modules> | |||||
<module fileurl="file://$PROJECT_DIR$/.idea/crossword.iml" filepath="$PROJECT_DIR$/.idea/crossword.iml" /> | |||||
</modules> | |||||
</component> | |||||
</project> |
@ -0,0 +1,6 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="VcsDirectoryMappings"> | |||||
<mapping directory="$PROJECT_DIR$" vcs="Git" /> | |||||
</component> | |||||
</project> |
@ -0,0 +1,133 @@ | |||||
class Variable(): | |||||
ACROSS = "across" | |||||
DOWN = "down" | |||||
def __init__(self, i, j, direction, length): | |||||
"""Create a new variable with starting point, direction, and length.""" | |||||
self.i = i | |||||
self.j = j | |||||
self.direction = direction | |||||
self.length = length | |||||
self.cells = [] | |||||
for k in range(self.length): | |||||
self.cells.append( | |||||
(self.i + (k if self.direction == Variable.DOWN else 0), | |||||
self.j + (k if self.direction == Variable.ACROSS else 0)) | |||||
) | |||||
def __hash__(self): | |||||
return hash((self.i, self.j, self.direction, self.length)) | |||||
def __eq__(self, other): | |||||
return ( | |||||
(self.i == other.i) and | |||||
(self.j == other.j) and | |||||
(self.direction == other.direction) and | |||||
(self.length == other.length) | |||||
) | |||||
def __str__(self): | |||||
return f"({self.i}, {self.j}) {self.direction} : {self.length}" | |||||
def __repr__(self): | |||||
direction = repr(self.direction) | |||||
return f"Variable({self.i}, {self.j}, {direction}, {self.length})" | |||||
class Crossword(): | |||||
def __init__(self, structure_file, words_file): | |||||
# Determine structure of crossword | |||||
with open(structure_file) as f: | |||||
contents = f.read().splitlines() | |||||
self.height = len(contents) | |||||
self.width = max(len(line) for line in contents) | |||||
self.structure = [] | |||||
for i in range(self.height): | |||||
row = [] | |||||
for j in range(self.width): | |||||
if j >= len(contents[i]): | |||||
row.append(False) | |||||
elif contents[i][j] == "_": | |||||
row.append(True) | |||||
else: | |||||
row.append(False) | |||||
self.structure.append(row) | |||||
# Save vocabulary list | |||||
with open(words_file) as f: | |||||
self.words = set(f.read().upper().splitlines()) | |||||
# Determine variable set | |||||
self.variables = set() | |||||
for i in range(self.height): | |||||
for j in range(self.width): | |||||
# Vertical words | |||||
starts_word = ( | |||||
self.structure[i][j] | |||||
and (i == 0 or not self.structure[i - 1][j]) | |||||
) | |||||
if starts_word: | |||||
length = 1 | |||||
for k in range(i + 1, self.height): | |||||
if self.structure[k][j]: | |||||
length += 1 | |||||
else: | |||||
break | |||||
if length > 1: | |||||
self.variables.add(Variable( | |||||
i=i, j=j, | |||||
direction=Variable.DOWN, | |||||
length=length | |||||
)) | |||||
# Horizontal words | |||||
starts_word = ( | |||||
self.structure[i][j] | |||||
and (j == 0 or not self.structure[i][j - 1]) | |||||
) | |||||
if starts_word: | |||||
length = 1 | |||||
for k in range(j + 1, self.width): | |||||
if self.structure[i][k]: | |||||
length += 1 | |||||
else: | |||||
break | |||||
if length > 1: | |||||
self.variables.add(Variable( | |||||
i=i, j=j, | |||||
direction=Variable.ACROSS, | |||||
length=length | |||||
)) | |||||
# Compute overlaps for each word | |||||
# For any pair of variables v1, v2, their overlap is either: | |||||
# None, if the two variables do not overlap; or | |||||
# (i, j), where v1's ith character overlaps v2's jth character | |||||
self.overlaps = dict() | |||||
for v1 in self.variables: | |||||
for v2 in self.variables: | |||||
if v1 == v2: | |||||
continue | |||||
cells1 = v1.cells | |||||
cells2 = v2.cells | |||||
intersection = set(cells1).intersection(cells2) | |||||
if not intersection: | |||||
self.overlaps[v1, v2] = None | |||||
else: | |||||
intersection = intersection.pop() | |||||
self.overlaps[v1, v2] = ( | |||||
cells1.index(intersection), | |||||
cells2.index(intersection) | |||||
) | |||||
def neighbors(self, var): | |||||
"""Given a variable, return set of overlapping variables.""" | |||||
return set( | |||||
v for v in self.variables | |||||
if v != var and self.overlaps[v, var] | |||||
) |
@ -0,0 +1,5 @@ | |||||
#___# | |||||
#_##_ | |||||
#_##_ | |||||
#_##_ | |||||
#____ |
@ -0,0 +1,9 @@ | |||||
############## | |||||
#######_####_# | |||||
#____________# | |||||
#_#####_####_# | |||||
#_##_____###_# | |||||
#_#####_####_# | |||||
#_###______#_# | |||||
#######_####_# | |||||
############## |
@ -0,0 +1,6 @@ | |||||
######_ | |||||
____##_ | |||||
_##____ | |||||
_##_##_ | |||||
_##_##_ | |||||
#___##_ |
@ -0,0 +1,10 @@ | |||||
one | |||||
two | |||||
three | |||||
four | |||||
five | |||||
six | |||||
seven | |||||
eight | |||||
nine | |||||
ten |
@ -0,0 +1,51 @@ | |||||
adversarial | |||||
alpha | |||||
arc | |||||
artificial | |||||
bayes | |||||
beta | |||||
bit | |||||
breadth | |||||
byte | |||||
classification | |||||
classify | |||||
condition | |||||
constraint | |||||
create | |||||
depth | |||||
distribution | |||||
end | |||||
false | |||||
graph | |||||
heuristic | |||||
infer | |||||
inference | |||||
initial | |||||
intelligence | |||||
knowledge | |||||
language | |||||
learning | |||||
line | |||||
logic | |||||
loss | |||||
markov | |||||
minimax | |||||
network | |||||
neural | |||||
node | |||||
optimization | |||||
probability | |||||
proposition | |||||
prune | |||||
reason | |||||
recurrent | |||||
regression | |||||
resolution | |||||
resolve | |||||
satisfaction | |||||
search | |||||
sine | |||||
start | |||||
true | |||||
truth | |||||
uncertainty |
@ -0,0 +1,22 @@ | |||||
import os | |||||
import sys | |||||
import time | |||||
import subprocess | |||||
combinations = [(0, 0), (0, 1),(1, 1),(2, 2),(1, 2),(0, 2)] | |||||
start = time.time() | |||||
for count in range(int(sys.argv[1])): | |||||
print("RUN " + str(count + 1)) | |||||
for i in combinations: | |||||
os.system("python generate.py data/structure{0}.txt data/words{1}.txt > debug/{0}_{1}.{2}.out".format(i[0], i[1], count)) | |||||
print("Program took {} seconds to execute on average".format((time.time()-start) / (count + 1))) | |||||
proc = subprocess.Popen(["grep 'No solution' debug/*"], stdout=subprocess.PIPE, shell=True) | |||||
(out, err) = proc.communicate() | |||||
out = out.decode() | |||||
print("{} tests did not find any solutions!\n".format(len(out.split("\n")) - 1)) | |||||
for i in out.split("\n"): | |||||
print(i.split(":")[0]) | |||||
@ -0,0 +1,299 @@ | |||||
import sys | |||||
from copy import deepcopy | |||||
from crossword import * | |||||
# I did this project during vacation with a bunch of kids screaming around me, so it is | |||||
# very possible that some parts of the code is sub-optimal and not particularly pretty | |||||
# I do not accept any responsibility if you suffer brain damage or have an | |||||
# aneurysm while reading the code below | |||||
class CrosswordCreator(): | |||||
def __init__(self, crossword): | |||||
""" | |||||
Create new CSP crossword generate. | |||||
""" | |||||
self.crossword = crossword | |||||
self.domains = { | |||||
var: self.crossword.words.copy() | |||||
for var in self.crossword.variables | |||||
} | |||||
def letter_grid(self, assignment): | |||||
""" | |||||
Return 2D array representing a given assignment. | |||||
""" | |||||
letters = [ | |||||
[None for _ in range(self.crossword.width)] | |||||
for _ in range(self.crossword.height) | |||||
] | |||||
for variable, word in assignment.items(): | |||||
direction = variable.direction | |||||
for k in range(len(word)): | |||||
i = variable.i + (k if direction == Variable.DOWN else 0) | |||||
j = variable.j + (k if direction == Variable.ACROSS else 0) | |||||
letters[i][j] = word[k] | |||||
return letters | |||||
def print(self, assignment): | |||||
""" | |||||
Print crossword assignment to the terminal. | |||||
""" | |||||
letters = self.letter_grid(assignment) | |||||
print() | |||||
print("_"*self.crossword.width*2) | |||||
for i in range(self.crossword.height): | |||||
for j in range(self.crossword.width): | |||||
if self.crossword.structure[i][j]: | |||||
print(letters[i][j] or " ", end="|") | |||||
else: | |||||
print("■", end="|") | |||||
print() | |||||
print("-"*self.crossword.width*2) | |||||
def save(self, assignment, filename): | |||||
""" | |||||
Save crossword assignment to an image file. | |||||
""" | |||||
from PIL import Image, ImageDraw, ImageFont | |||||
cell_size = 100 | |||||
cell_border = 2 | |||||
interior_size = cell_size - 2 * cell_border | |||||
letters = self.letter_grid(assignment) | |||||
# Create a blank canvas | |||||
img = Image.new( | |||||
"RGBA", | |||||
(self.crossword.width * cell_size, | |||||
self.crossword.height * cell_size), | |||||
"black" | |||||
) | |||||
font = ImageFont.truetype("assets/fonts/OpenSans-Regular.ttf", 80) | |||||
draw = ImageDraw.Draw(img) | |||||
for i in range(self.crossword.height): | |||||
for j in range(self.crossword.width): | |||||
rect = [ | |||||
(j * cell_size + cell_border, | |||||
i * cell_size + cell_border), | |||||
((j + 1) * cell_size - cell_border, | |||||
(i + 1) * cell_size - cell_border) | |||||
] | |||||
if self.crossword.structure[i][j]: | |||||
draw.rectangle(rect, fill="white") | |||||
if letters[i][j]: | |||||
w, h = draw.textsize(letters[i][j], font=font) | |||||
draw.text( | |||||
(rect[0][0] + ((interior_size - w) / 2), | |||||
rect[0][1] + ((interior_size - h) / 2) - 10), | |||||
letters[i][j], fill="black", font=font | |||||
) | |||||
img.save(filename) | |||||
def solve(self): | |||||
""" | |||||
Enforce node and arc consistency, and then solve the CSP. | |||||
""" | |||||
self.enforce_node_consistency() | |||||
self.ac3() | |||||
return self.backtrack(dict()) | |||||
def enforce_node_consistency(self): | |||||
""" | |||||
Update `self.domains` such that each variable is node-consistent. | |||||
(Remove any values that are inconsistent with a variable's unary | |||||
constraints; in this case, the length of the word.) | |||||
""" | |||||
for var in self.domains: | |||||
new_domain = self.domains[var].copy() | |||||
for word in self.domains[var]: | |||||
if var.length != len(word): | |||||
new_domain -= {word} | |||||
self.domains[var] = new_domain | |||||
def revise(self, x, y): | |||||
revised = False | |||||
def search_in_domain(domain, index, char, disallow): | |||||
for word in domain: | |||||
if word[index] == char and word != disallow: | |||||
return True | |||||
return False | |||||
if (x, y) in self.crossword.overlaps: | |||||
new_domain = self.domains[x].copy() | |||||
overlap = self.crossword.overlaps[(x, y)] | |||||
for i in self.domains[x]: | |||||
if not search_in_domain(self.domains[y], overlap[1], i[overlap[0]], i): | |||||
new_domain -= {i} | |||||
revised = True | |||||
self.domains[x] = new_domain | |||||
return revised | |||||
def ac3(self, arcs=None): | |||||
if arcs: | |||||
queue = arcs | |||||
else: | |||||
queue = [] | |||||
for i in self.crossword.overlaps: | |||||
if self.crossword.overlaps[i]: | |||||
queue.append(i) | |||||
while queue: | |||||
(x, y) = queue[-1] | |||||
queue = queue[:-1] | |||||
if self.revise(x, y): | |||||
if len(self.domains[x]) == 0: | |||||
return False | |||||
for i in self.crossword.neighbors(x) - {y}: | |||||
queue.append((i, x)) | |||||
return True | |||||
def assignment_complete(self, assignment): | |||||
""" | |||||
Return True if `assignment` is complete (i.e., assigns a value to each | |||||
crossword variable); return False otherwise. | |||||
""" | |||||
return len(assignment) == len(self.domains) | |||||
def consistent(self, assignment): | |||||
""" | |||||
Return True if `assignment` is consistent (i.e., words fit in crossword | |||||
puzzle without conflicting characters); return False otherwise. | |||||
""" | |||||
if not assignment: | |||||
return False | |||||
for i in assignment: | |||||
if i.length != len(assignment[i]): | |||||
return False | |||||
for j in assignment: | |||||
if j == i: | |||||
continue | |||||
overlap = self.crossword.overlaps[(i, j)] | |||||
if overlap: | |||||
if assignment[i][overlap[0]] != assignment[j][overlap[1]]: | |||||
return False | |||||
return True | |||||
def order_domain_values(self, var, assignment): | |||||
""" | |||||
Return a list of values in the domain of `var`, in order by | |||||
the number of values they rule out for neighboring variables. | |||||
The first value in the list, for example, should be the one | |||||
that rules out the fewest values among the neighbors of `var`. | |||||
""" | |||||
domain = [] | |||||
for i in self.domains[var]: | |||||
discarded = 0 | |||||
for j in self.crossword.neighbors(var): | |||||
if j not in assignment: | |||||
overlap = self.crossword.overlaps[(var, j)] | |||||
for k in self.domains[j]: | |||||
if k[overlap[1]] != i[overlap[0]]: | |||||
discarded += 1 | |||||
domain.append((discarded, i)) | |||||
domain.sort(key = lambda x: x[0]) | |||||
final = [] | |||||
for i in domain: | |||||
final.append(i[1]) | |||||
return final | |||||
def select_unassigned_variable(self, assignment): | |||||
""" | |||||
Return an unassigned variable not already part of `assignment`. | |||||
Choose the variable with the minimum number of remaining values | |||||
in its domain. If there is a tie, choose the variable with the highest | |||||
degree. If there is a tie, any of the tied variables are acceptable | |||||
return values. | |||||
""" | |||||
best = [len(self.crossword.words) + 1, 0, []] | |||||
for i in self.domains: | |||||
if i not in assignment: | |||||
length = self.domains[i].__len__() | |||||
if best[0] > length: | |||||
neighbors = self.crossword.neighbors(i) | |||||
best[0] = length | |||||
best[2].append(i) | |||||
best[1] = len(neighbors) | |||||
elif best[0] == length: | |||||
neighbors = self.crossword.neighbors(i) | |||||
if len(neighbors) == best[1]: | |||||
best[2].append(self.domains[i]) | |||||
elif len(neighbors) < best[1]: | |||||
best[2] = [i] | |||||
best[1] = len(neighbors) | |||||
return best[2][0] | |||||
def backtrack(self, assignment): | |||||
""" | |||||
Using Backtracking Search, take as input a partial assignment for the | |||||
crossword and return a complete assignment if possible to do so. | |||||
`assignment` is a mapping from variables (keys) to words (values). | |||||
If no assignment is possible, return None. | |||||
""" | |||||
if self.assignment_complete(assignment): | |||||
return assignment | |||||
var = self.select_unassigned_variable(assignment) | |||||
domain_values = self.order_domain_values(var, assignment) | |||||
inferences = [] | |||||
domains_backup = deepcopy(self.domains) | |||||
for i in domain_values: | |||||
assignment[var] = i | |||||
arcs = [] | |||||
self.domains[var] = {i} | |||||
for j in self.crossword.neighbors(var): | |||||
arcs.append(self.crossword.overlaps[(j, var)]) | |||||
if not self.ac3(arcs=arcs): | |||||
continue | |||||
inferences = self.infer(assignment) | |||||
if self.consistent(assignment): | |||||
result = self.backtrack(assignment) | |||||
if result: | |||||
return result | |||||
for j in inferences: | |||||
assignment.pop(j) | |||||
assignment.pop(var) | |||||
self.domains = domains_backup | |||||
return None | |||||
def infer(self, assignment): | |||||
inferred = [] | |||||
for i in self.domains: | |||||
if i in assignment: | |||||
continue | |||||
val = self.domains[i] | |||||
if len(val) == 1: | |||||
inferred.append(i) | |||||
assignment[i] = next(iter(val)) | |||||
return inferred | |||||
def main(): | |||||
# Check usage | |||||
if len(sys.argv) not in [3, 4]: | |||||
sys.exit("Usage: python generate.py structure words [output]") | |||||
# Parse command-line arguments | |||||
structure = sys.argv[1] | |||||
words = sys.argv[2] | |||||
output = sys.argv[3] if len(sys.argv) == 4 else None | |||||
# Generate crossword | |||||
crossword = Crossword(structure, words) | |||||
creator = CrosswordCreator(crossword) | |||||
assignment = creator.solve() | |||||
# Print result | |||||
if assignment is None: | |||||
print("No solution.") | |||||
else: | |||||
creator.print(assignment) | |||||
if output: | |||||
creator.save(assignment, output) | |||||
if __name__ == "__main__": | |||||
main() |
@ -0,0 +1,9 @@ | |||||
import os | |||||
import time | |||||
combinations = [(0,0),(0,1),(1,1),(2,2),(0,2)] | |||||
for i in combinations: | |||||
print("Running structure {} with words {}".format(i[0], i[1])) | |||||
os.system("python generate.py data/structure{0}.txt data/words{1}.txt images/{0}_{1}.png".format(i[0], i[1])) | |||||
time.sleep(0.5) |
@ -0,0 +1,74 @@ | |||||
# Created by .ignore support plugin (hsz.mobi) | |||||
### JetBrains template | |||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider | |||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 | |||||
# User-specific stuff | |||||
.idea/**/workspace.xml | |||||
.idea/**/tasks.xml | |||||
.idea/**/usage.statistics.xml | |||||
.idea/**/dictionaries | |||||
.idea/**/shelf | |||||
# Generated files | |||||
.idea/**/contentModel.xml | |||||
# Sensitive or high-churn files | |||||
.idea/**/dataSources/ | |||||
.idea/**/dataSources.ids | |||||
.idea/**/dataSources.local.xml | |||||
.idea/**/sqlDataSources.xml | |||||
.idea/**/dynamic.xml | |||||
.idea/**/uiDesigner.xml | |||||
.idea/**/dbnavigator.xml | |||||
# Gradle | |||||
.idea/**/gradle.xml | |||||
.idea/**/libraries | |||||
# Gradle and Maven with auto-import | |||||
# When using Gradle or Maven with auto-import, you should exclude module files, | |||||
# since they will be recreated, and may cause churn. Uncomment if using | |||||
# auto-import. | |||||
# .idea/artifacts | |||||
# .idea/compiler.xml | |||||
# .idea/jarRepositories.xml | |||||
# .idea/modules.xml | |||||
# .idea/*.iml | |||||
# .idea/modules | |||||
# *.iml | |||||
# *.ipr | |||||
# CMake | |||||
cmake-build-*/ | |||||
# Mongo Explorer plugin | |||||
.idea/**/mongoSettings.xml | |||||
# File-based project format | |||||
*.iws | |||||
# IntelliJ | |||||
out/ | |||||
# mpeltonen/sbt-idea plugin | |||||
.idea_modules/ | |||||
# JIRA plugin | |||||
atlassian-ide-plugin.xml | |||||
# Cursive Clojure plugin | |||||
.idea/replstate.xml | |||||
# Crashlytics plugin (for Android Studio and IntelliJ) | |||||
com_crashlytics_export_strings.xml | |||||
crashlytics.properties | |||||
crashlytics-build.properties | |||||
fabric.properties | |||||
# Editor-based Rest Client | |||||
.idea/httpRequests | |||||
# Android studio 3.1+ serialized cache file | |||||
.idea/caches/build_file_checksums.ser | |||||
@ -0,0 +1,8 @@ | |||||
# Default ignored files | |||||
/shelf/ | |||||
/workspace.xml | |||||
# Datasource local storage ignored files | |||||
/dataSources/ | |||||
/dataSources.local.xml | |||||
# Editor-based HTTP Client requests | |||||
/httpRequests/ |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<module type="JAVA_MODULE" version="4"> | |||||
<component name="NewModuleRootManager" inherit-compiler-output="true"> | |||||
<exclude-output /> | |||||
<content url="file://$MODULE_DIR$" /> | |||||
<orderEntry type="inheritedJdk" /> | |||||
<orderEntry type="sourceFolder" forTests="false" /> | |||||
</component> | |||||
</module> |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="JavaScriptSettings"> | |||||
<option name="languageLevel" value="ES6" /> | |||||
</component> | |||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="Python 3.8" project-jdk-type="Python SDK"> | |||||
<output url="file://$PROJECT_DIR$/out" /> | |||||
</component> | |||||
</project> |
@ -0,0 +1,8 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ProjectModuleManager"> | |||||
<modules> | |||||
<module fileurl="file://$PROJECT_DIR$/.idea/degrees.iml" filepath="$PROJECT_DIR$/.idea/degrees.iml" /> | |||||
</modules> | |||||
</component> | |||||
</project> |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<module type="JAVA_MODULE" version="4"> | |||||
<component name="NewModuleRootManager" inherit-compiler-output="true"> | |||||
<exclude-output /> | |||||
<content url="file://$MODULE_DIR$" /> | |||||
<orderEntry type="inheritedJdk" /> | |||||
<orderEntry type="sourceFolder" forTests="false" /> | |||||
</component> | |||||
</module> |
@ -0,0 +1,6 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="VcsDirectoryMappings"> | |||||
<mapping directory="$PROJECT_DIR$" vcs="Git" /> | |||||
</component> | |||||
</project> |
@ -0,0 +1 @@ | |||||
# yigitcolakoglu |
@ -0,0 +1,146 @@ | |||||
import csv | |||||
import sys | |||||
from util import Node, StackFrontier, QueueFrontier | |||||
# Maps names to a set of corresponding person_ids | |||||
names = {} | |||||
# Maps person_ids to a dictionary of: name, birth, movies (a set of movie_ids) | |||||
people = {} | |||||
# Maps movie_ids to a dictionary of: title, year, stars (a set of person_ids) | |||||
movies = {} | |||||
def load_data(directory): | |||||
""" | |||||
Load data from CSV files into memory. | |||||
""" | |||||
# Load people | |||||
with open(f"{directory}/people.csv", encoding="utf-8") as f: | |||||
reader = csv.DictReader(f) | |||||
for row in reader: | |||||
people[row["id"]] = { | |||||
"name": row["name"], | |||||
"birth": row["birth"], | |||||
"movies": set() | |||||
} | |||||
if row["name"].lower() not in names: | |||||
names[row["name"].lower()] = {row["id"]} | |||||
else: | |||||
names[row["name"].lower()].add(row["id"]) | |||||
# Load movies | |||||
with open(f"{directory}/movies.csv", encoding="utf-8") as f: | |||||
reader = csv.DictReader(f) | |||||
for row in reader: | |||||
movies[row["id"]] = { | |||||
"title": row["title"], | |||||
"year": row["year"], | |||||
"stars": set() | |||||
} | |||||
# Load stars | |||||
with open(f"{directory}/stars.csv", encoding="utf-8") as f: | |||||
reader = csv.DictReader(f) | |||||
for row in reader: | |||||
try: | |||||
people[row["person_id"]]["movies"].add(row["movie_id"]) | |||||
movies[row["movie_id"]]["stars"].add(row["person_id"]) | |||||
except KeyError: | |||||
pass | |||||
def main(): | |||||
if len(sys.argv) > 2: | |||||
sys.exit("Usage: python degrees.py [directory]") | |||||
directory = sys.argv[1] if len(sys.argv) == 2 else "large" | |||||
# Load data from files into memory | |||||
print("Loading data...") | |||||
load_data(directory) | |||||
print("Data loaded.") | |||||
source = person_id_for_name(input("Name: ")) | |||||
if source is None: | |||||
sys.exit("Person not found.") | |||||
target = person_id_for_name(input("Name: ")) | |||||
if target is None: | |||||
sys.exit("Person not found.") | |||||
path = shortest_path(source, target) | |||||
if path is None: | |||||
print("Not connected.") | |||||
else: | |||||
degrees = len(path) | |||||
print(f"{degrees} degrees of separation.") | |||||
path = [(None, source)] + path | |||||
for i in range(degrees): | |||||
person1 = people[path[i][1]]["name"] | |||||
person2 = people[path[i + 1][1]]["name"] | |||||
movie = movies[path[i + 1][0]]["title"] | |||||
print(f"{i + 1}: {person1} and {person2} starred in {movie}") | |||||
def shortest_path(source, target): | |||||
explored_nodes = set() | |||||
frontier = QueueFrontier() | |||||
frontier.add(Node(source, None, None)) | |||||
while True: | |||||
if frontier.empty(): | |||||
return None | |||||
current_node = frontier.remove() | |||||
explored_nodes.add(current_node.state) | |||||
for i in neighbors_for_person(current_node.state): | |||||
actor_node = Node(i[1], current_node, i[0]) | |||||
if actor_node.state in explored_nodes: | |||||
continue | |||||
if i[1] == target: | |||||
return actor_node.draw_path() | |||||
frontier.add(actor_node) | |||||
def person_id_for_name(name): | |||||
""" | |||||
Returns the IMDB id for a person's name, | |||||
resolving ambiguities as needed. | |||||
""" | |||||
person_ids = list(names.get(name.lower(), set())) | |||||
if len(person_ids) == 0: | |||||
return None | |||||
elif len(person_ids) > 1: | |||||
print(f"Which '{name}'?") | |||||
for person_id in person_ids: | |||||
person = people[person_id] | |||||
name = person["name"] | |||||
birth = person["birth"] | |||||
print(f"ID: {person_id}, Name: {name}, Birth: {birth}") | |||||
try: | |||||
person_id = input("Intended Person ID: ") | |||||
if person_id in person_ids: | |||||
return person_id | |||||
except ValueError: | |||||
pass | |||||
return None | |||||
else: | |||||
return person_ids[0] | |||||
def neighbors_for_person(person_id): | |||||
""" | |||||
Returns (movie_id, person_id) pairs for people | |||||
who starred with a given person. | |||||
""" | |||||
movie_ids = people[person_id]["movies"] | |||||
neighbors = set() | |||||
for movie_id in movie_ids: | |||||
for person_id in movies[movie_id]["stars"]: | |||||
neighbors.add((movie_id, person_id)) | |||||
return neighbors | |||||
if __name__ == "__main__": | |||||
main() |
@ -0,0 +1,6 @@ | |||||
id,title,year | |||||
112384,"Apollo 13",1995 | |||||
104257,"A Few Good Men",1992 | |||||
109830,"Forrest Gump",1994 | |||||
93779,"The Princess Bride",1987 | |||||
95953,"Rain Man",1988 |
@ -0,0 +1,17 @@ | |||||
id,name,birth | |||||
102,"Kevin Bacon",1958 | |||||
129,"Tom Cruise",1962 | |||||
144,"Cary Elwes",1962 | |||||
158,"Tom Hanks",1956 | |||||
1597,"Mandy Patinkin",1952 | |||||
163,"Dustin Hoffman",1937 | |||||
1697,"Chris Sarandon",1942 | |||||
193,"Demi Moore",1962 | |||||
197,"Jack Nicholson",1937 | |||||
200,"Bill Paxton",1955 | |||||
398,"Sally Field",1946 | |||||
420,"Valeria Golino",1965 | |||||
596520,"Gerald R. Molen",1935 | |||||
641,"Gary Sinise",1955 | |||||
705,"Robin Wright",1966 | |||||
914612,"Emma Watson",1990 |
@ -0,0 +1,21 @@ | |||||
person_id,movie_id | |||||
102,104257 | |||||
102,112384 | |||||
129,104257 | |||||
129,95953 | |||||
144,93779 | |||||
158,109830 | |||||
158,112384 | |||||
1597,93779 | |||||
163,95953 | |||||
1697,93779 | |||||
193,104257 | |||||
197,104257 | |||||
200,112384 | |||||
398,109830 | |||||
420,95953 | |||||
596520,95953 | |||||
641,109830 | |||||
641,112384 | |||||
705,109830 | |||||
705,93779 |
@ -0,0 +1,47 @@ | |||||
class Node(): | |||||
def __init__(self, state, parent, action): | |||||
self.state = state | |||||
self.parent = parent | |||||
self.action = action | |||||
def draw_path(self): | |||||
path = [] | |||||
node = self | |||||
while node.parent: | |||||
path.append((node.action, node.state)) | |||||
node = node.parent | |||||
path.reverse() | |||||
return path | |||||
class StackFrontier(): | |||||
def __init__(self): | |||||
self.frontier = [] | |||||
def add(self, node): | |||||
self.frontier.append(node) | |||||
def contains_state(self, state): | |||||
return any(node.state == state for node in self.frontier) | |||||
def empty(self): | |||||
return len(self.frontier) == 0 | |||||
def remove(self): | |||||
if self.empty(): | |||||
raise Exception("empty frontier") | |||||
else: | |||||
node = self.frontier[-1] | |||||
self.frontier = self.frontier[:-1] | |||||
return node | |||||
class QueueFrontier(StackFrontier): | |||||
def remove(self): | |||||
if self.empty(): | |||||
raise Exception("empty frontier") | |||||
else: | |||||
node = self.frontier[0] | |||||
self.frontier = self.frontier[1:] | |||||
return node |
@ -0,0 +1,8 @@ | |||||
# Default ignored files | |||||
/shelf/ | |||||
/workspace.xml | |||||
# Datasource local storage ignored files | |||||
/dataSources/ | |||||
/dataSources.local.xml | |||||
# Editor-based HTTP Client requests | |||||
/httpRequests/ |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<module type="JAVA_MODULE" version="4"> | |||||
<component name="NewModuleRootManager" inherit-compiler-output="true"> | |||||
<exclude-output /> | |||||
<content url="file://$MODULE_DIR$" /> | |||||
<orderEntry type="inheritedJdk" /> | |||||
<orderEntry type="sourceFolder" forTests="false" /> | |||||
</component> | |||||
</module> |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="JavaScriptSettings"> | |||||
<option name="languageLevel" value="ES6" /> | |||||
</component> | |||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="Python 3.8" project-jdk-type="Python SDK"> | |||||
<output url="file://$PROJECT_DIR$/out" /> | |||||
</component> | |||||
</project> |
@ -0,0 +1,8 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ProjectModuleManager"> | |||||
<modules> | |||||
<module fileurl="file://$PROJECT_DIR$/.idea/heredity.iml" filepath="$PROJECT_DIR$/.idea/heredity.iml" /> | |||||
</modules> | |||||
</component> | |||||
</project> |
@ -0,0 +1,4 @@ | |||||
name,mother,father,trait | |||||
Harry,Lily,James, | |||||
James,,,1 | |||||
Lily,,,0 |
@ -0,0 +1,7 @@ | |||||
name,mother,father,trait | |||||
Arthur,,,0 | |||||
Charlie,Molly,Arthur,0 | |||||
Fred,Molly,Arthur,1 | |||||
Ginny,Molly,Arthur, | |||||
Molly,,,0 | |||||
Ron,Molly,Arthur, |
@ -0,0 +1,6 @@ | |||||
name,mother,father,trait | |||||
Arthur,,,0 | |||||
Hermione,,,0 | |||||
Molly,,, | |||||
Ron,Molly,Arthur,0 | |||||
Rose,Ron,Hermione,1 |
@ -0,0 +1,212 @@ | |||||
import csv | |||||
import itertools | |||||
import sys | |||||
PROBS = { | |||||
# Unconditional probabilities for having gene | |||||
"gene": { | |||||
2: 0.01, | |||||
1: 0.03, | |||||
0: 0.96 | |||||
}, | |||||
"trait": { | |||||
# Probability of trait given two copies of gene | |||||
2: { | |||||
True: 0.65, | |||||
False: 0.35 | |||||
}, | |||||
# Probability of trait given one copy of gene | |||||
1: { | |||||
True: 0.56, | |||||
False: 0.44 | |||||
}, | |||||
# Probability of trait given no gene | |||||
0: { | |||||
True: 0.01, | |||||
False: 0.99 | |||||
} | |||||
}, | |||||
# Mutation probability | |||||
"mutation": 0.01 | |||||
} | |||||
def main(): | |||||
# Check for proper usage | |||||
if len(sys.argv) != 2: | |||||
sys.exit("Usage: python heredity.py data.csv") | |||||
people = load_data(sys.argv[1]) | |||||
# Keep track of gene and trait probabilities for each person | |||||
probabilities = { | |||||
person: { | |||||
"gene": { | |||||
2: 0, | |||||
1: 0, | |||||
0: 0 | |||||
}, | |||||
"trait": { | |||||
True: 0, | |||||
False: 0 | |||||
} | |||||
} | |||||
for person in people | |||||
} | |||||
# Loop over all sets of people who might have the trait | |||||
names = set(people) | |||||
for have_trait in powerset(names): | |||||
# Check if current set of people violates known information | |||||
fails_evidence = any( | |||||
(people[person]["trait"] is not None and | |||||
people[person]["trait"] != (person in have_trait)) | |||||
for person in names | |||||
) | |||||
if fails_evidence: | |||||
continue | |||||
# Loop over all sets of people who might have the gene | |||||
for one_gene in powerset(names): | |||||
for two_genes in powerset(names - one_gene): | |||||
# Update probabilities with new joint probability | |||||
p = joint_probability(people, one_gene, two_genes, have_trait) | |||||
update(probabilities, one_gene, two_genes, have_trait, p) | |||||
# Ensure probabilities sum to 1 | |||||
normalize(probabilities) | |||||
# Print results | |||||
for person in people: | |||||
print(f"{person}:") | |||||
for field in probabilities[person]: | |||||
print(f" {field.capitalize()}:") | |||||
for value in probabilities[person][field]: | |||||
p = probabilities[person][field][value] | |||||
print(f" {value}: {p:.4f}") | |||||
def load_data(filename): | |||||
""" | |||||
Load gene and trait data from a file into a dictionary. | |||||
File assumed to be a CSV containing fields name, mother, father, trait. | |||||
mother, father must both be blank, or both be valid names in the CSV. | |||||
trait should be 0 or 1 if trait is known, blank otherwise. | |||||
""" | |||||
data = dict() | |||||
with open(filename) as f: | |||||
reader = csv.DictReader(f) | |||||
for row in reader: | |||||
name = row["name"] | |||||
data[name] = { | |||||
"name": name, | |||||
"mother": row["mother"] or None, | |||||
"father": row["father"] or None, | |||||
"trait": (True if row["trait"] == "1" else | |||||
False if row["trait"] == "0" else None) | |||||
} | |||||
return data | |||||
def powerset(s): | |||||
""" | |||||
Return a list of all possible subsets of set s. | |||||
""" | |||||
s = list(s) | |||||
return [ | |||||
set(s) for s in itertools.chain.from_iterable( | |||||
itertools.combinations(s, r) for r in range(len(s) + 1) | |||||
) | |||||
] | |||||
def get_info(person, one_gene, two_genes, have_trait): | |||||
trait = person in have_trait | |||||
gene = 0 | |||||
if person in one_gene: | |||||
gene = 1 | |||||
elif person in two_genes: | |||||
gene = 2 | |||||
return gene, trait | |||||
def joint_probability(people, one_gene, two_genes, have_trait): | |||||
""" | |||||
Compute and return a joint probability. | |||||
The probability returned should be the probability that | |||||
* everyone in set `one_gene` has one copy of the gene, and | |||||
* everyone in set `two_genes` has two copies of the gene, and | |||||
* everyone not in `one_gene` or `two_gene` does not have the gene, and | |||||
* everyone in set `have_trait` has the trait, and | |||||
* everyone not in set` have_trait` does not have the trait. | |||||
""" | |||||
def generate_prob(m_gene, f_gene, gene_combination): | |||||
if m_gene == 1: | |||||
m_prob = 0.5 | |||||
else: | |||||
m_prob = 0.99 if m_gene/2 == gene_combination[0] else 0.01 | |||||
if f_gene == 1: | |||||
f_prob = 0.5 | |||||
else: | |||||
f_prob = 0.99 if f_gene/2 == gene_combination[1] else 0.01 | |||||
return m_prob * f_prob | |||||
probabilities = [] | |||||
for person in people: | |||||
gene, trait = get_info(person, one_gene, two_genes, have_trait) | |||||
if people[person]["mother"] and people[person]["father"]: | |||||
mother_gene, foo = get_info(people[person]["mother"], one_gene, two_genes, have_trait) | |||||
father_gene, foo = get_info(people[person]["father"], one_gene, two_genes, have_trait) | |||||
if gene == 1: | |||||
gene_prob = generate_prob(mother_gene, father_gene, (0, 1)) + generate_prob(mother_gene, father_gene, (1, 0)) | |||||
else: | |||||
gene_prob = generate_prob(mother_gene, father_gene, (gene/2, gene/2)) | |||||
else: | |||||
gene_prob = PROBS["gene"][gene] | |||||
probabilities.append(gene_prob * PROBS["trait"][gene][trait]) | |||||
joint_prob = 1 | |||||
for p in probabilities: | |||||
joint_prob *= p | |||||
return joint_prob | |||||
def update(probabilities, one_gene, two_genes, have_trait, p): | |||||
for person in probabilities: | |||||
gene, trait = get_info(person, one_gene, two_genes, have_trait) | |||||
probabilities[person]["gene"][gene] += p | |||||
probabilities[person]["trait"][trait] += p | |||||
def normalize(probabilities): | |||||
for person in probabilities: | |||||
psum = 0 | |||||
for gene in probabilities[person]["gene"]: | |||||
psum += probabilities[person]["gene"][gene] | |||||
gene_ratio = 1/psum | |||||
for gene in probabilities[person]["gene"]: | |||||
probabilities[person]["gene"][gene] *= gene_ratio | |||||
psum = 0 | |||||
for trait in probabilities[person]["trait"]: | |||||
psum += probabilities[person]["trait"][trait] | |||||
trait_ratio = 1/psum | |||||
for trait in probabilities[person]["trait"]: | |||||
probabilities[person]["trait"][trait] *= trait_ratio | |||||
if __name__ == "__main__": | |||||
main() | |||||
@ -0,0 +1,8 @@ | |||||
# Default ignored files | |||||
/shelf/ | |||||
/workspace.xml | |||||
# Datasource local storage ignored files | |||||
/dataSources/ | |||||
/dataSources.local.xml | |||||
# Editor-based HTTP Client requests | |||||
/httpRequests/ |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<module type="JAVA_MODULE" version="4"> | |||||
<component name="NewModuleRootManager" inherit-compiler-output="true"> | |||||
<exclude-output /> | |||||
<content url="file://$MODULE_DIR$" /> | |||||
<orderEntry type="inheritedJdk" /> | |||||
<orderEntry type="sourceFolder" forTests="false" /> | |||||
</component> | |||||
</module> |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="JavaScriptSettings"> | |||||
<option name="languageLevel" value="ES6" /> | |||||
</component> | |||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="Python 3.8" project-jdk-type="Python SDK"> | |||||
<output url="file://$PROJECT_DIR$/out" /> | |||||
</component> | |||||
</project> |
@ -0,0 +1,8 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ProjectModuleManager"> | |||||
<modules> | |||||
<module fileurl="file://$PROJECT_DIR$/.idea/knights.iml" filepath="$PROJECT_DIR$/.idea/knights.iml" /> | |||||
</modules> | |||||
</component> | |||||
</project> |
@ -0,0 +1,263 @@ | |||||
import itertools | |||||
class Sentence(): | |||||
def evaluate(self, model): | |||||
"""Evaluates the logical sentence.""" | |||||
raise Exception("nothing to evaluate") | |||||
def formula(self): | |||||
"""Returns string formula representing logical sentence.""" | |||||
return "" | |||||
def symbols(self): | |||||
"""Returns a set of all symbols in the logical sentence.""" | |||||
return set() | |||||
@classmethod | |||||
def validate(cls, sentence): | |||||
if not isinstance(sentence, Sentence): | |||||
raise TypeError("must be a logical sentence") | |||||
@classmethod | |||||
def parenthesize(cls, s): | |||||
"""Parenthesizes an expression if not already parenthesized.""" | |||||
def balanced(s): | |||||
"""Checks if a string has balanced parentheses.""" | |||||
count = 0 | |||||
for c in s: | |||||
if c == "(": | |||||
count += 1 | |||||
elif c == ")": | |||||
if count <= 0: | |||||
return False | |||||
count -= 1 | |||||
return count == 0 | |||||
if not len(s) or s.isalpha() or ( | |||||
s[0] == "(" and s[-1] == ")" and balanced(s[1:-1]) | |||||
): | |||||
return s | |||||
else: | |||||
return f"({s})" | |||||
class Symbol(Sentence): | |||||
def __init__(self, name): | |||||
self.name = name | |||||
def __eq__(self, other): | |||||
return isinstance(other, Symbol) and self.name == other.name | |||||
def __hash__(self): | |||||
return hash(("symbol", self.name)) | |||||
def __repr__(self): | |||||
return self.name | |||||
def evaluate(self, model): | |||||
try: | |||||
return bool(model[self.name]) | |||||
except KeyError: | |||||
raise Exception(f"variable {self.name} not in model") | |||||
def formula(self): | |||||
return self.name | |||||
def symbols(self): | |||||
return {self.name} | |||||
class Not(Sentence): | |||||
def __init__(self, operand): | |||||
Sentence.validate(operand) | |||||
self.operand = operand | |||||
def __eq__(self, other): | |||||
return isinstance(other, Not) and self.operand == other.operand | |||||
def __hash__(self): | |||||
return hash(("not", hash(self.operand))) | |||||
def __repr__(self): | |||||
return f"Not({self.operand})" | |||||
def evaluate(self, model): | |||||
return not self.operand.evaluate(model) | |||||
def formula(self): | |||||
return "¬" + Sentence.parenthesize(self.operand.formula()) | |||||
def symbols(self): | |||||
return self.operand.symbols() | |||||
class And(Sentence): | |||||
def __init__(self, *conjuncts): | |||||
for conjunct in conjuncts: | |||||
Sentence.validate(conjunct) | |||||
self.conjuncts = list(conjuncts) | |||||
def __eq__(self, other): | |||||
return isinstance(other, And) and self.conjuncts == other.conjuncts | |||||
def __hash__(self): | |||||
return hash( | |||||
("and", tuple(hash(conjunct) for conjunct in self.conjuncts)) | |||||
) | |||||
def __repr__(self): | |||||
conjunctions = ", ".join( | |||||
[str(conjunct) for conjunct in self.conjuncts] | |||||
) | |||||
return f"And({conjunctions})" | |||||
def add(self, conjunct): | |||||
Sentence.validate(conjunct) | |||||
self.conjuncts.append(conjunct) | |||||
def evaluate(self, model): | |||||
return all(conjunct.evaluate(model) for conjunct in self.conjuncts) | |||||
def formula(self): | |||||
if len(self.conjuncts) == 1: | |||||
return self.conjuncts[0].formula() | |||||
return " ∧ ".join([Sentence.parenthesize(conjunct.formula()) | |||||
for conjunct in self.conjuncts]) | |||||
def symbols(self): | |||||
return set.union(*[conjunct.symbols() for conjunct in self.conjuncts]) | |||||
class Or(Sentence): | |||||
def __init__(self, *disjuncts): | |||||
for disjunct in disjuncts: | |||||
Sentence.validate(disjunct) | |||||
self.disjuncts = list(disjuncts) | |||||
def __eq__(self, other): | |||||
return isinstance(other, Or) and self.disjuncts == other.disjuncts | |||||
def __hash__(self): | |||||
return hash( | |||||
("or", tuple(hash(disjunct) for disjunct in self.disjuncts)) | |||||
) | |||||
def __repr__(self): | |||||
disjuncts = ", ".join([str(disjunct) for disjunct in self.disjuncts]) | |||||
return f"Or({disjuncts})" | |||||
def evaluate(self, model): | |||||
return any(disjunct.evaluate(model) for disjunct in self.disjuncts) | |||||
def formula(self): | |||||
if len(self.disjuncts) == 1: | |||||
return self.disjuncts[0].formula() | |||||
return " ∨ ".join([Sentence.parenthesize(disjunct.formula()) | |||||
for disjunct in self.disjuncts]) | |||||
def symbols(self): | |||||
return set.union(*[disjunct.symbols() for disjunct in self.disjuncts]) | |||||
class Implication(Sentence): | |||||
def __init__(self, antecedent, consequent): | |||||
Sentence.validate(antecedent) | |||||
Sentence.validate(consequent) | |||||
self.antecedent = antecedent | |||||
self.consequent = consequent | |||||
def __eq__(self, other): | |||||
return (isinstance(other, Implication) | |||||
and self.antecedent == other.antecedent | |||||
and self.consequent == other.consequent) | |||||
def __hash__(self): | |||||
return hash(("implies", hash(self.antecedent), hash(self.consequent))) | |||||
def __repr__(self): | |||||
return f"Implication({self.antecedent}, {self.consequent})" | |||||
def evaluate(self, model): | |||||
return ((not self.antecedent.evaluate(model)) | |||||
or self.consequent.evaluate(model)) | |||||
def formula(self): | |||||
antecedent = Sentence.parenthesize(self.antecedent.formula()) | |||||
consequent = Sentence.parenthesize(self.consequent.formula()) | |||||
return f"{antecedent} => {consequent}" | |||||
def symbols(self): | |||||
return set.union(self.antecedent.symbols(), self.consequent.symbols()) | |||||
class Biconditional(Sentence): | |||||
def __init__(self, left, right): | |||||
Sentence.validate(left) | |||||
Sentence.validate(right) | |||||
self.left = left | |||||
self.right = right | |||||
def __eq__(self, other): | |||||
return (isinstance(other, Biconditional) | |||||
and self.left == other.left | |||||
and self.right == other.right) | |||||
def __hash__(self): | |||||
return hash(("biconditional", hash(self.left), hash(self.right))) | |||||
def __repr__(self): | |||||
return f"Biconditional({self.left}, {self.right})" | |||||
def evaluate(self, model): | |||||
return ((self.left.evaluate(model) | |||||
and self.right.evaluate(model)) | |||||
or (not self.left.evaluate(model) | |||||
and not self.right.evaluate(model))) | |||||
def formula(self): | |||||
left = Sentence.parenthesize(str(self.left)) | |||||
right = Sentence.parenthesize(str(self.right)) | |||||
return f"{left} <=> {right}" | |||||
def symbols(self): | |||||
return set.union(self.left.symbols(), self.right.symbols()) | |||||
def model_check(knowledge, query): | |||||
"""Checks if knowledge base entails query.""" | |||||
def check_all(knowledge, query, symbols, model): | |||||
"""Checks if knowledge base entails query, given a particular model.""" | |||||
# If model has an assignment for each symbol | |||||
if not symbols: | |||||
# If knowledge base is true in model, then query must also be true | |||||
if knowledge.evaluate(model): | |||||
return query.evaluate(model) | |||||
return True | |||||
else: | |||||
# Choose one of the remaining unused symbols | |||||
remaining = symbols.copy() | |||||
p = remaining.pop() | |||||
# Create a model where the symbol is true | |||||
model_true = model.copy() | |||||
model_true[p] = True | |||||
# Create a model where the symbol is false | |||||
model_false = model.copy() | |||||
model_false[p] = False | |||||
# Ensure entailment holds in both models | |||||
return (check_all(knowledge, query, remaining, model_true) and | |||||
check_all(knowledge, query, remaining, model_false)) | |||||
# Get all symbols in both knowledge and query | |||||
symbols = set.union(knowledge.symbols(), query.symbols()) | |||||
# Check that knowledge entails query | |||||
return check_all(knowledge, query, symbols, dict()) |
@ -0,0 +1,76 @@ | |||||
from logic import * | |||||
AKnight = Symbol("A is a Knight") | |||||
AKnave = Symbol("A is a Knave") | |||||
BKnight = Symbol("B is a Knight") | |||||
BKnave = Symbol("B is a Knave") | |||||
CKnight = Symbol("C is a Knight") | |||||
CKnave = Symbol("C is a Knave") | |||||
# Puzzle 0 | |||||
# A says "I am both a knight and a knave." | |||||
knowledge0 = And( | |||||
Not(And(AKnave, AKnight)), | |||||
Or(AKnave, AKnight), | |||||
Implication(AKnight, And(AKnave, AKnight)) | |||||
) | |||||
# Puzzle 1 | |||||
# A says "We are both knaves." | |||||
# B says nothing. | |||||
knowledge1 = And( | |||||
Not(And(AKnave, AKnight)), Or(AKnave, AKnight), | |||||
Not(And(BKnave, BKnight)), Or(BKnave, BKnight), | |||||
Implication(AKnight, And(AKnave, BKnave)), | |||||
Implication(AKnave, Not(And(AKnave, BKnave))) | |||||
) | |||||
# Puzzle 2 | |||||
# A says "We are the same kind." | |||||
# B says "We are of different kinds." | |||||
knowledge2 = And( | |||||
Not(And(AKnave, AKnight)), Or(AKnave, AKnight), | |||||
Not(And(BKnave, BKnight)), Or(BKnave, BKnight), | |||||
Implication(AKnight, And(AKnight, BKnight)), | |||||
Implication(BKnave, And(AKnight, BKnight)), | |||||
Implication(BKnight, And(Not(And(BKnight, AKnight)), Not(And(BKnave, AKnave)))) | |||||
) | |||||
# Puzzle 3 | |||||
# A says either "I am a knight." or "I am a knave.", but you don't know which. | |||||
# B says "A said 'I am a knave'." | |||||
# B says "C is a knave." | |||||
# C says "A is a knight." | |||||
knowledge3 = And( | |||||
Not(And(AKnave, AKnight)), Or(AKnave, AKnight), | |||||
Not(And(BKnave, BKnight)), Or(BKnave, BKnight), | |||||
Not(And(CKnave, CKnight)), Or(CKnave, CKnight), | |||||
Implication(AKnave, AKnight), | |||||
Biconditional(AKnave, BKnight), | |||||
Biconditional(BKnave, CKnight), | |||||
Biconditional(AKnight, CKnight) | |||||
) | |||||
def main(): | |||||
symbols = [AKnight, AKnave, BKnight, BKnave, CKnight, CKnave] | |||||
puzzles = [ | |||||
("Puzzle 0", knowledge0), | |||||
("Puzzle 1", knowledge1), | |||||
("Puzzle 2", knowledge2), | |||||
("Puzzle 3", knowledge3) | |||||
] | |||||
for puzzle, knowledge in puzzles: | |||||
print(puzzle) | |||||
if len(knowledge.conjuncts) == 0: | |||||
print(" Not yet implemented.") | |||||
else: | |||||
for symbol in symbols: | |||||
if model_check(knowledge, symbol): | |||||
print(f" {symbol}") | |||||
if __name__ == "__main__": | |||||
main() |
@ -0,0 +1,8 @@ | |||||
# Default ignored files | |||||
/shelf/ | |||||
/workspace.xml | |||||
# Datasource local storage ignored files | |||||
/dataSources/ | |||||
/dataSources.local.xml | |||||
# Editor-based HTTP Client requests | |||||
/httpRequests/ |
@ -0,0 +1,15 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<module type="JAVA_MODULE" version="4"> | |||||
<component name="FacetManager"> | |||||
<facet type="Python" name="Python"> | |||||
<configuration sdkName="Python 3.8" /> | |||||
</facet> | |||||
</component> | |||||
<component name="NewModuleRootManager" inherit-compiler-output="true"> | |||||
<exclude-output /> | |||||
<content url="file://$MODULE_DIR$" /> | |||||
<orderEntry type="inheritedJdk" /> | |||||
<orderEntry type="sourceFolder" forTests="false" /> | |||||
<orderEntry type="library" name="Python 3.8 interpreter library" level="application" /> | |||||
</component> | |||||
</module> |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="JavaScriptSettings"> | |||||
<option name="languageLevel" value="ES6" /> | |||||
</component> | |||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="Python 3.8" project-jdk-type="Python SDK"> | |||||
<output url="file://$PROJECT_DIR$/out" /> | |||||
</component> | |||||
</project> |
@ -0,0 +1,8 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ProjectModuleManager"> | |||||
<modules> | |||||
<module fileurl="file://$PROJECT_DIR$/.idea/minesweeper.iml" filepath="$PROJECT_DIR$/.idea/minesweeper.iml" /> | |||||
</modules> | |||||
</component> | |||||
</project> |
@ -0,0 +1,253 @@ | |||||
import itertools | |||||
import random | |||||
class Minesweeper(): | |||||
""" | |||||
Minesweeper game representation | |||||
""" | |||||
def __init__(self, height=8, width=8, mines=8): | |||||
# Set initial width, height, and number of mines | |||||
self.height = height | |||||
self.width = width | |||||
self.mines = set() | |||||
# Initialize an empty field with no mines | |||||
self.board = [] | |||||
for i in range(self.height): | |||||
row = [] | |||||
for j in range(self.width): | |||||
row.append(False) | |||||
self.board.append(row) | |||||
# Add mines randomly | |||||
while len(self.mines) != mines: | |||||
i = random.randrange(height) | |||||
j = random.randrange(width) | |||||
if not self.board[i][j]: | |||||
self.mines.add((i, j)) | |||||
self.board[i][j] = True | |||||
# At first, player has found no mines | |||||
self.mines_found = set() | |||||
def print(self): | |||||
""" | |||||
Prints a text-based representation | |||||
of where mines are located. | |||||
""" | |||||
for i in range(self.height): | |||||
print("--" * self.width + "-") | |||||
for j in range(self.width): | |||||
if self.board[i][j]: | |||||
print("|X", end="") | |||||
else: | |||||
print("| ", end="") | |||||
print("|") | |||||
print("--" * self.width + "-") | |||||
def is_mine(self, cell): | |||||
i, j = cell | |||||
return self.board[i][j] | |||||
def nearby_mines(self, cell): | |||||
""" | |||||
Returns the number of mines that are | |||||
within one row and column of a given cell, | |||||
not including the cell itself. | |||||
""" | |||||
# Keep count of nearby mines | |||||
count = 0 | |||||
# Loop over all cells within one row and column | |||||
for i in range(cell[0] - 1, cell[0] + 2): | |||||
for j in range(cell[1] - 1, cell[1] + 2): | |||||
# Ignore the cell itself | |||||
if (i, j) == cell: | |||||
continue | |||||
# Update count if cell in bounds and is mine | |||||
if 0 <= i < self.height and 0 <= j < self.width: | |||||
if self.board[i][j]: | |||||
count += 1 | |||||
return count | |||||
def won(self): | |||||
""" | |||||
Checks if all mines have been flagged. | |||||
""" | |||||
return self.mines_found == self.mines | |||||
class Sentence(): | |||||
""" | |||||
Logical statement about a Minesweeper game | |||||
A sentence consists of a set of board cells, | |||||
and a count of the number of those cells which are mines. | |||||
""" | |||||
def __init__(self, cells, count): | |||||
self.cells = set(cells) | |||||
self.count = count | |||||
def __eq__(self, other): | |||||
return self.cells == other.cells and self.count == other.count | |||||
def __str__(self): | |||||
return f"{self.cells} = {self.count}" | |||||
def known_mines(self): | |||||
if self.cells.__len__() == self.count: | |||||
return self.cells | |||||
return set() | |||||
def known_safes(self): | |||||
if self.count == 0: | |||||
return self.cells | |||||
return set() | |||||
def mark_mine(self, cell): # Remove cell from set and decrease count by one | |||||
if cell in self.cells: | |||||
self.cells.remove(cell) | |||||
self.count -= 1 | |||||
def mark_safe(self, cell): # Remove safe cell from cells set | |||||
if cell in self.cells: | |||||
self.cells.remove(cell) | |||||
class MinesweeperAI(): | |||||
""" | |||||
Minesweeper game player | |||||
""" | |||||
def __init__(self, height=8, width=8): | |||||
# Set initial height and width | |||||
self.height = height | |||||
self.width = width | |||||
# Keep track of which cells have been clicked on | |||||
self.moves_made = set() | |||||
# Keep track of cells known to be safe or mines | |||||
self.mines = set() | |||||
self.safes = set() | |||||
# List of sentences about the game known to be true | |||||
self.knowledge = [] | |||||
def mark_mine(self, cell): | |||||
""" | |||||
Marks a cell as a mine, and updates all knowledge | |||||
to mark that cell as a mine as well. | |||||
""" | |||||
self.mines.add(cell) | |||||
for sentence in self.knowledge: | |||||
sentence.mark_mine(cell) | |||||
def mark_safe(self, cell): | |||||
""" | |||||
Marks a cell as safe, and updates all knowledge | |||||
to mark that cell as safe as well. | |||||
""" | |||||
self.safes.add(cell) | |||||
for sentence in self.knowledge: | |||||
sentence.mark_safe(cell) | |||||
def add_knowledge(self, cell, count): | |||||
""" | |||||
Called when the Minesweeper board tells us, for a given | |||||
safe cell, how many neighboring cells have mines in them. | |||||
This function should: | |||||
1) mark the cell as a move that has been made | |||||
2) mark the cell as safe | |||||
3) add a new sentence to the AI's knowledge base | |||||
based on the value of `cell` and `count` | |||||
4) mark any additional cells as safe or as mines | |||||
if it can be concluded based on the AI's knowledge base | |||||
5) add any new sentences to the AI's knowledge base | |||||
if they can be inferred from existing knowledge | |||||
""" | |||||
self.moves_made.add(cell) #Store information about the cell | |||||
self.mark_safe(cell) | |||||
surrounding_cells = set() | |||||
for i in range(cell[0]-1 if cell[0] - 1 >= 0 else 0, cell[0]+2 if cell[0] + 2 <= self.width else self.width): | |||||
for j in range(cell[1]-1 if cell[1] - 1 >= 0 else 0, cell[1]+2 if cell[1] + 2 <= self.height else self.height): | |||||
count -= int((i, j) in self.mines) | |||||
if (i, j) not in self.safes.union(self.mines): | |||||
surrounding_cells.add((i, j)) | |||||
new_knowledge = Sentence(surrounding_cells, count) | |||||
if new_knowledge in self.knowledge: | |||||
return | |||||
inferred_knowledge = [] | |||||
knowledge_cpy = self.knowledge.copy() | |||||
popped = 0 | |||||
for i, sentence in enumerate(knowledge_cpy): | |||||
if sentence.cells == set(): | |||||
self.knowledge.pop(i - popped) | |||||
popped += 1 | |||||
continue | |||||
if new_knowledge.cells.issubset(sentence.cells): | |||||
new_cells = sentence.cells - new_knowledge.cells | |||||
new_count = sentence.count - new_knowledge.count | |||||
new_sentence = Sentence(new_cells, new_count) | |||||
inferred_knowledge.append(Sentence(new_cells, new_count)) | |||||
elif sentence.cells.issubset(new_knowledge.cells): | |||||
new_cells = new_knowledge.cells - sentence.cells | |||||
new_count = new_knowledge.count - sentence.count | |||||
new_sentence = Sentence(new_cells, new_count) | |||||
inferred_knowledge.append(new_sentence) | |||||
for i in inferred_knowledge: | |||||
if i.known_safes() != set(): | |||||
for j in i.cells: | |||||
self.mark_safe(j) | |||||
elif i.known_mines() != set(): | |||||
for j in i.cells: | |||||
self.mark_mine(j) | |||||
for i in self.knowledge: | |||||
cells = i.cells.copy() | |||||
if i.known_safes() != set(): | |||||
for j in cells: | |||||
self.mark_safe(j) | |||||
elif i.known_mines() != set(): | |||||
for j in cells: | |||||
self.mark_mine(j) | |||||
inferred_knowledge.append(new_knowledge) | |||||
for i in inferred_knowledge: | |||||
exists = False | |||||
for j in self.knowledge: | |||||
if i == j: | |||||
exists = True | |||||
if not exists: | |||||
self.knowledge.append(i) | |||||
def make_safe_move(self): | |||||
available_moves = self.safes - self.moves_made | |||||
for s in self.knowledge: | |||||
available_moves = available_moves.union(s.known_safes()) | |||||
if available_moves.__len__() == 0: | |||||
return None | |||||
return available_moves.pop() | |||||
def make_random_move(self): | |||||
unavailable_moves = self.moves_made.union(self.mines) | |||||
available_moves = set() | |||||
for i in range(self.width): | |||||
for j in range(self.height): | |||||
if (i, j) not in unavailable_moves: | |||||
available_moves.add((i, j)) | |||||
if available_moves.__len__() == 0: | |||||
return None | |||||
return available_moves.pop() |
@ -0,0 +1 @@ | |||||
pygame |
@ -0,0 +1,222 @@ | |||||
import pygame | |||||
import sys | |||||
import time | |||||
from minesweeper import Minesweeper, MinesweeperAI | |||||
HEIGHT = 8 | |||||
WIDTH = 8 | |||||
MINES = 8 | |||||
# Colors | |||||
BLACK = (0, 0, 0) | |||||
GRAY = (180, 180, 180) | |||||
WHITE = (255, 255, 255) | |||||
# Create game | |||||
pygame.init() | |||||
size = width, height = 600, 400 | |||||
screen = pygame.display.set_mode(size) | |||||
# Fonts | |||||
OPEN_SANS = "assets/fonts/OpenSans-Regular.ttf" | |||||
smallFont = pygame.font.Font(OPEN_SANS, 20) | |||||
mediumFont = pygame.font.Font(OPEN_SANS, 28) | |||||
largeFont = pygame.font.Font(OPEN_SANS, 40) | |||||
# Compute board size | |||||
BOARD_PADDING = 20 | |||||
board_width = ((2 / 3) * width) - (BOARD_PADDING * 2) | |||||
board_height = height - (BOARD_PADDING * 2) | |||||
cell_size = int(min(board_width / WIDTH, board_height / HEIGHT)) | |||||
board_origin = (BOARD_PADDING, BOARD_PADDING) | |||||
# Add images | |||||
flag = pygame.image.load("assets/images/flag.png") | |||||
flag = pygame.transform.scale(flag, (cell_size, cell_size)) | |||||
mine = pygame.image.load("assets/images/mine.png") | |||||
mine = pygame.transform.scale(mine, (cell_size, cell_size)) | |||||
# Create game and AI agent | |||||
game = Minesweeper(height=HEIGHT, width=WIDTH, mines=MINES) | |||||
ai = MinesweeperAI(height=HEIGHT, width=WIDTH) | |||||
# Keep track of revealed cells, flagged cells, and if a mine was hit | |||||
revealed = set() | |||||
flags = set() | |||||
lost = False | |||||
# Show instructions initially | |||||
instructions = True | |||||
while True: | |||||
# Check if game quit | |||||
for event in pygame.event.get(): | |||||
if event.type == pygame.QUIT: | |||||
sys.exit() | |||||
screen.fill(BLACK) | |||||
# Show game instructions | |||||
if instructions: | |||||
# Title | |||||
title = largeFont.render("Play Minesweeper", True, WHITE) | |||||
titleRect = title.get_rect() | |||||
titleRect.center = ((width / 2), 50) | |||||
screen.blit(title, titleRect) | |||||
# Rules | |||||
rules = [ | |||||
"Click a cell to reveal it.", | |||||
"Right-click a cell to mark it as a mine.", | |||||
"Mark all mines successfully to win!" | |||||
] | |||||
for i, rule in enumerate(rules): | |||||
line = smallFont.render(rule, True, WHITE) | |||||
lineRect = line.get_rect() | |||||
lineRect.center = ((width / 2), 150 + 30 * i) | |||||
screen.blit(line, lineRect) | |||||
# Play game button | |||||
buttonRect = pygame.Rect((width / 4), (3 / 4) * height, width / 2, 50) | |||||
buttonText = mediumFont.render("Play Game", True, BLACK) | |||||
buttonTextRect = buttonText.get_rect() | |||||
buttonTextRect.center = buttonRect.center | |||||
pygame.draw.rect(screen, WHITE, buttonRect) | |||||
screen.blit(buttonText, buttonTextRect) | |||||
# Check if play button clicked | |||||
click, _, _ = pygame.mouse.get_pressed() | |||||
if click == 1: | |||||
mouse = pygame.mouse.get_pos() | |||||
if buttonRect.collidepoint(mouse): | |||||
instructions = False | |||||
time.sleep(0.3) | |||||
pygame.display.flip() | |||||
continue | |||||
# Draw board | |||||
cells = [] | |||||
for i in range(HEIGHT): | |||||
row = [] | |||||
for j in range(WIDTH): | |||||
# Draw rectangle for cell | |||||
rect = pygame.Rect( | |||||
board_origin[0] + j * cell_size, | |||||
board_origin[1] + i * cell_size, | |||||
cell_size, cell_size | |||||
) | |||||
pygame.draw.rect(screen, GRAY, rect) | |||||
pygame.draw.rect(screen, WHITE, rect, 3) | |||||
# Add a mine, flag, or number if needed | |||||
if game.is_mine((i, j)) and lost: | |||||
screen.blit(mine, rect) | |||||
elif (i, j) in flags: | |||||
screen.blit(flag, rect) | |||||
elif (i, j) in revealed: | |||||
neighbors = smallFont.render( | |||||
str(game.nearby_mines((i, j))), | |||||
True, BLACK | |||||
) | |||||
neighborsTextRect = neighbors.get_rect() | |||||
neighborsTextRect.center = rect.center | |||||
screen.blit(neighbors, neighborsTextRect) | |||||
row.append(rect) | |||||
cells.append(row) | |||||
# AI Move button | |||||
aiButton = pygame.Rect( | |||||
(2 / 3) * width + BOARD_PADDING, (1 / 3) * height - 50, | |||||
(width / 3) - BOARD_PADDING * 2, 50 | |||||
) | |||||
buttonText = mediumFont.render("AI Move", True, BLACK) | |||||
buttonRect = buttonText.get_rect() | |||||
buttonRect.center = aiButton.center | |||||
pygame.draw.rect(screen, WHITE, aiButton) | |||||
screen.blit(buttonText, buttonRect) | |||||
# Reset button | |||||
resetButton = pygame.Rect( | |||||
(2 / 3) * width + BOARD_PADDING, (1 / 3) * height + 20, | |||||
(width / 3) - BOARD_PADDING * 2, 50 | |||||
) | |||||
buttonText = mediumFont.render("Reset", True, BLACK) | |||||
buttonRect = buttonText.get_rect() | |||||
buttonRect.center = resetButton.center | |||||
pygame.draw.rect(screen, WHITE, resetButton) | |||||
screen.blit(buttonText, buttonRect) | |||||
# Display text | |||||
text = "Lost" if lost else "Won" if game.mines == flags else "" | |||||
text = mediumFont.render(text, True, WHITE) | |||||
textRect = text.get_rect() | |||||
textRect.center = ((5 / 6) * width, (2 / 3) * height) | |||||
screen.blit(text, textRect) | |||||
move = None | |||||
left, _, right = pygame.mouse.get_pressed() | |||||
# Check for a right-click to toggle flagging | |||||
if right == 1 and not lost: | |||||
mouse = pygame.mouse.get_pos() | |||||
for i in range(HEIGHT): | |||||
for j in range(WIDTH): | |||||
if cells[i][j].collidepoint(mouse) and (i, j) not in revealed: | |||||
if (i, j) in flags: | |||||
flags.remove((i, j)) | |||||
else: | |||||
flags.add((i, j)) | |||||
time.sleep(0.2) | |||||
elif left == 1: | |||||
mouse = pygame.mouse.get_pos() | |||||
# If AI button clicked, make an AI move | |||||
if aiButton.collidepoint(mouse) and not lost: | |||||
move = ai.make_safe_move() | |||||
if move is None: | |||||
move = ai.make_random_move() | |||||
if move is None: | |||||
flags = ai.mines.copy() | |||||
print("No moves left to make.") | |||||
else: | |||||
print("No known safe moves, AI making random move.") | |||||
else: | |||||
print("AI making safe move.") | |||||
time.sleep(0.2) | |||||
# Reset game state | |||||
elif resetButton.collidepoint(mouse): | |||||
game = Minesweeper(height=HEIGHT, width=WIDTH, mines=MINES) | |||||
ai = MinesweeperAI(height=HEIGHT, width=WIDTH) | |||||
revealed = set() | |||||
flags = set() | |||||
lost = False | |||||
continue | |||||
# User-made move | |||||
elif not lost: | |||||
for i in range(HEIGHT): | |||||
for j in range(WIDTH): | |||||
if (cells[i][j].collidepoint(mouse) | |||||
and (i, j) not in flags | |||||
and (i, j) not in revealed): | |||||
move = (i, j) | |||||
# Make move and update AI knowledge | |||||
if move: | |||||
if game.is_mine(move): | |||||
lost = True | |||||
else: | |||||
nearby = game.nearby_mines(move) | |||||
revealed.add(move) | |||||
ai.add_knowledge(move, nearby) | |||||
pygame.display.flip() |
@ -0,0 +1,8 @@ | |||||
# Default ignored files | |||||
/shelf/ | |||||
/workspace.xml | |||||
# Datasource local storage ignored files | |||||
/dataSources/ | |||||
/dataSources.local.xml | |||||
# Editor-based HTTP Client requests | |||||
/httpRequests/ |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="JavaScriptSettings"> | |||||
<option name="languageLevel" value="ES6" /> | |||||
</component> | |||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="Python 3.8" project-jdk-type="Python SDK"> | |||||
<output url="file://$PROJECT_DIR$/out" /> | |||||
</component> | |||||
</project> |
@ -0,0 +1,8 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ProjectModuleManager"> | |||||
<modules> | |||||
<module fileurl="file://$PROJECT_DIR$/.idea/nim.iml" filepath="$PROJECT_DIR$/.idea/nim.iml" /> | |||||
</modules> | |||||
</component> | |||||
</project> |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<module type="JAVA_MODULE" version="4"> | |||||
<component name="NewModuleRootManager" inherit-compiler-output="true"> | |||||
<exclude-output /> | |||||
<content url="file://$MODULE_DIR$" /> | |||||
<orderEntry type="inheritedJdk" /> | |||||
<orderEntry type="sourceFolder" forTests="false" /> | |||||
</component> | |||||
</module> |
@ -0,0 +1,302 @@ | |||||
import math | |||||
import random | |||||
import time | |||||
class Nim(): | |||||
def __init__(self, initial=[1, 3, 5, 7]): | |||||
""" | |||||
Initialize game board. | |||||
Each game board has | |||||
- `piles`: a list of how many elements remain in each pile | |||||
- `player`: 0 or 1 to indicate which player's turn | |||||
- `winner`: None, 0, or 1 to indicate who the winner is | |||||
""" | |||||
self.piles = initial.copy() | |||||
self.player = 0 | |||||
self.winner = None | |||||
@classmethod | |||||
def available_actions(cls, piles): | |||||
""" | |||||
Nim.available_actions(piles) takes a `piles` list as input | |||||
and returns all of the available actions `(i, j)` in that state. | |||||
Action `(i, j)` represents the action of removing `j` items | |||||
from pile `i` (where piles are 0-indexed). | |||||
""" | |||||
actions = set() | |||||
for i, pile in enumerate(piles): | |||||
for j in range(1, pile + 1): | |||||
actions.add((i, j)) | |||||
return actions | |||||
@classmethod | |||||
def other_player(cls, player): | |||||
""" | |||||
Nim.other_player(player) returns the player that is not | |||||
`player`. Assumes `player` is either 0 or 1. | |||||
""" | |||||
return 0 if player == 1 else 1 | |||||
def switch_player(self): | |||||
""" | |||||
Switch the current player to the other player. | |||||
""" | |||||
self.player = Nim.other_player(self.player) | |||||
def move(self, action): | |||||
""" | |||||
Make the move `action` for the current player. | |||||
`action` must be a tuple `(i, j)`. | |||||
""" | |||||
pile, count = action | |||||
# Check for errors | |||||
if self.winner is not None: | |||||
raise Exception("Game already won") | |||||
elif pile < 0 or pile >= len(self.piles): | |||||
raise Exception("Invalid pile") | |||||
elif count < 1 or count > self.piles[pile]: | |||||
raise Exception("Invalid number of objects") | |||||
# Update pile | |||||
self.piles[pile] -= count | |||||
self.switch_player() | |||||
# Check for a winner | |||||
if all(pile == 0 for pile in self.piles): | |||||
self.winner = self.player | |||||
class NimAI(): | |||||
def __init__(self, alpha=0.5, epsilon=0.2): | |||||
""" | |||||
Initialize AI with an empty Q-learning dictionary, | |||||
an alpha (learning) rate, and an epsilon rate. | |||||
The Q-learning dictionary maps `(state, action)` | |||||
pairs to a Q-value (a number). | |||||
- `state` is a tuple of remaining piles, e.g. (1, 1, 4, 4) | |||||
- `action` is a tuple `(i, j)` for an action | |||||
""" | |||||
self.q = dict() | |||||
self.alpha = alpha | |||||
self.epsilon = epsilon | |||||
def update(self, old_state, action, new_state, reward): | |||||
""" | |||||
Update Q-learning model, given an old state, an action taken | |||||
in that state, a new resulting state, and the reward received | |||||
from taking that action. | |||||
""" | |||||
old = self.get_q_value(old_state, action) | |||||
best_future = self.best_future_reward(new_state) | |||||
self.update_q_value(old_state, action, old, reward, best_future) | |||||
def get_q_value(self, state, action): | |||||
""" | |||||
Return the Q-value for the state `state` and the action `action`. | |||||
If no Q-value exists yet in `self.q`, return 0. | |||||
""" | |||||
if (tuple(state), action,) not in self.q: | |||||
return 0 | |||||
return self.q[(tuple(state), action)] | |||||
def update_q_value(self, state, action, old_q, reward, future_rewards): | |||||
""" | |||||
Update the Q-value for the state `state` and the action `action` | |||||
given the previous Q-value `old_q`, a current reward `reward`, | |||||
and an estiamte of future rewards `future_rewards`. | |||||
Use the formula: | |||||
Q(s, a) <- old value estimate | |||||
+ alpha * (new value estimate - old value estimate) | |||||
where `old value estimate` is the previous Q-value, | |||||
`alpha` is the learning rate, and `new value estimate` | |||||
is the sum of the current reward and estimated future rewards. | |||||
""" | |||||
self.q[(tuple(state), action)] = old_q + self.alpha*(reward + future_rewards - old_q) | |||||
def best_future_reward(self, state): | |||||
""" | |||||
Given a state `state`, consider all possible `(state, action)` | |||||
pairs available in that state and return the maximum of all | |||||
of their Q-values. | |||||
Use 0 as the Q-value if a `(state, action)` pair has no | |||||
Q-value in `self.q`. If there are no available actions in | |||||
`state`, return 0. | |||||
""" | |||||
actions = Nim.available_actions(state) | |||||
if not actions: | |||||
return 0 | |||||
best = [0, set()] | |||||
for i in actions: | |||||
val = self.get_q_value(state, i) | |||||
if not val: | |||||
continue | |||||
elif val > best[0]: | |||||
best[1] = {val} | |||||
elif val == best[0]: | |||||
best[1].add(val) | |||||
if best[1]: | |||||
return next(iter(best[1])) | |||||
else: | |||||
return 0 | |||||
def choose_action(self, state, epsilon=True): | |||||
""" | |||||
Given a state `state`, return an action `(i, j)` to take. | |||||
If `epsilon` is `False`, then return the best action | |||||
available in the state (the one with the highest Q-value, | |||||
using 0 for pairs that have no Q-values). | |||||
If `epsilon` is `True`, then with probability | |||||
`self.epsilon` choose a random available action, | |||||
otherwise choose the best action available. | |||||
If multiple actions have the same Q-value, any of those | |||||
options is an acceptable return value. | |||||
""" | |||||
actions = Nim.available_actions(state) | |||||
if epsilon: | |||||
epsilon = self.epsilon * 100 | |||||
else: | |||||
epsilon = 0 | |||||
if 100 - random.randint(0, 100) >= epsilon: | |||||
best = [0, set()] | |||||
for i in actions: | |||||
val = self.get_q_value(state, i) | |||||
if not val: | |||||
continue | |||||
elif val > best[0]: | |||||
best[1] = {i} | |||||
elif val == best[0]: | |||||
best[1].add(i) | |||||
if best[1]: | |||||
return next(iter(best[1])) | |||||
else: | |||||
return random.sample(actions, 1)[0] | |||||
else: | |||||
return random.sample(actions, 1)[0] | |||||
def train(n): | |||||
""" | |||||
Train an AI by playing `n` games against itself. | |||||
""" | |||||
player = NimAI() | |||||
# Play n games | |||||
for i in range(n): | |||||
print(f"Playing training game {i + 1}") | |||||
game = Nim() | |||||
# Keep track of last move made by either player | |||||
last = { | |||||
0: {"state": None, "action": None}, | |||||
1: {"state": None, "action": None} | |||||
} | |||||
# Game loop | |||||
while True: | |||||
# Keep track of current state and action | |||||
state = game.piles.copy() | |||||
action = player.choose_action(game.piles) | |||||
# Keep track of last state and action | |||||
last[game.player]["state"] = state | |||||
last[game.player]["action"] = action | |||||
# Make move | |||||
game.move(action) | |||||
new_state = game.piles.copy() | |||||
# When game is over, update Q values with rewards | |||||
if game.winner is not None: | |||||
player.update(state, action, new_state, -1) | |||||
player.update( | |||||
last[game.player]["state"], | |||||
last[game.player]["action"], | |||||
new_state, | |||||
1 | |||||
) | |||||
break | |||||
# If game is continuing, no rewards yet | |||||
elif last[game.player]["state"] is not None: | |||||
player.update( | |||||
last[game.player]["state"], | |||||
last[game.player]["action"], | |||||
new_state, | |||||
0 | |||||
) | |||||
print("Done training") | |||||
# Return the trained AI | |||||
return player | |||||
def play(ai, human_player=None): | |||||
""" | |||||
Play human game against the AI. | |||||
`human_player` can be set to 0 or 1 to specify whether | |||||
human player moves first or second. | |||||
""" | |||||
# If no player order set, choose human's order randomly | |||||
if human_player is None: | |||||
human_player = random.randint(0, 1) | |||||
# Create new game | |||||
game = Nim() | |||||
# Game loop | |||||
while True: | |||||
# Print contents of piles | |||||
print() | |||||
print("Piles:") | |||||
for i, pile in enumerate(game.piles): | |||||
print(f"Pile {i}: {pile}") | |||||
print() | |||||
# Compute available actions | |||||
available_actions = Nim.available_actions(game.piles) | |||||
time.sleep(1) | |||||
# Let human make a move | |||||
if game.player == human_player: | |||||
print("Your Turn") | |||||
while True: | |||||
pile = int(input("Choose Pile: ")) | |||||
count = int(input("Choose Count: ")) | |||||
if (pile, count) in available_actions: | |||||
break | |||||
print("Invalid move, try again.") | |||||
# Have AI make a move | |||||
else: | |||||
print("AI's Turn") | |||||
pile, count = ai.choose_action(game.piles, epsilon=False) | |||||
print(f"AI chose to take {count} from pile {pile}.") | |||||
# Make move | |||||
game.move((pile, count)) | |||||
# Check for winner | |||||
if game.winner is not None: | |||||
print() | |||||
print("GAME OVER") | |||||
winner = "Human" if game.winner == human_player else "AI" | |||||
print(f"Winner is {winner}") | |||||
return |
@ -0,0 +1,4 @@ | |||||
from nim import train, play | |||||
ai = train(10000) | |||||
play(ai) |
@ -0,0 +1,8 @@ | |||||
# Default ignored files | |||||
/shelf/ | |||||
/workspace.xml | |||||
# Datasource local storage ignored files | |||||
/dataSources/ | |||||
/dataSources.local.xml | |||||
# Editor-based HTTP Client requests | |||||
/httpRequests/ |
@ -0,0 +1,8 @@ | |||||
<component name="ProjectDictionaryState"> | |||||
<dictionary name="yigit"> | |||||
<words> | |||||
<w>pagerank</w> | |||||
<w>pageranks</w> | |||||
</words> | |||||
</dictionary> | |||||
</component> |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="JavaScriptSettings"> | |||||
<option name="languageLevel" value="ES6" /> | |||||
</component> | |||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="Python 3.8" project-jdk-type="Python SDK"> | |||||
<output url="file://$PROJECT_DIR$/out" /> | |||||
</component> | |||||
</project> |
@ -0,0 +1,8 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="ProjectModuleManager"> | |||||
<modules> | |||||
<module fileurl="file://$PROJECT_DIR$/.idea/pagerank.iml" filepath="$PROJECT_DIR$/.idea/pagerank.iml" /> | |||||
</modules> | |||||
</component> | |||||
</project> |
@ -0,0 +1,6 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<project version="4"> | |||||
<component name="PySciProjectComponent"> | |||||
<option name="PY_SCI_VIEW_SUGGESTED" value="true" /> | |||||
</component> | |||||
</project> |
@ -0,0 +1,9 @@ | |||||
<?xml version="1.0" encoding="UTF-8"?> | |||||
<module type="JAVA_MODULE" version="4"> | |||||
<component name="NewModuleRootManager" inherit-compiler-output="true"> | |||||
<exclude-output /> | |||||
<content url="file://$MODULE_DIR$" /> | |||||
<orderEntry type="inheritedJdk" /> | |||||
<orderEntry type="sourceFolder" forTests="false" /> | |||||
</component> | |||||
</module> |
@ -0,0 +1,14 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<title>1</title> | |||||
</head> | |||||
<body> | |||||
<h1>1</h1> | |||||
<div>Links:</div> | |||||
<ul> | |||||
<li><a href="2.html">2</a></li> | |||||
</ul> | |||||
</body> | |||||
</html> |
@ -0,0 +1,15 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<title>2</title> | |||||
</head> | |||||
<body> | |||||
<h1>2</h1> | |||||
<div>Links:</div> | |||||
<ul> | |||||
<li><a href="1.html">1</a></li> | |||||
<li><a href="3.html">3</a></li> | |||||
</ul> | |||||
</body> | |||||
</html> |
@ -0,0 +1,15 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<title>3</title> | |||||
</head> | |||||
<body> | |||||
<h1>3</h1> | |||||
<div>Links:</div> | |||||
<ul> | |||||
<li><a href="2.html">2</a></li> | |||||
<li><a href="4.html">4</a></li> | |||||
</ul> | |||||
</body> | |||||
</html> |
@ -0,0 +1,14 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<title>4</title> | |||||
</head> | |||||
<body> | |||||
<h1>4</h1> | |||||
<div>Links:</div> | |||||
<ul> | |||||
<li><a href="2.html">2</a></li> | |||||
</ul> | |||||
</body> | |||||
</html> |
@ -0,0 +1,14 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<title>BFS</title> | |||||
</head> | |||||
<body> | |||||
<h1>BFS</h1> | |||||
<div>Links:</div> | |||||
<ul> | |||||
<li><a href="search.html">Search</a></li> | |||||
</ul> | |||||
</body> | |||||
</html> |
@ -0,0 +1,15 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<title>DFS</title> | |||||
</head> | |||||
<body> | |||||
<h1>DFS</h1> | |||||
<div>Links:</div> | |||||
<ul> | |||||
<li><a href="bfs.html">BFS</a></li> | |||||
<li><a href="search.html">Search</a></li> | |||||
</ul> | |||||
</body> | |||||
</html> |
@ -0,0 +1,15 @@ | |||||
<!DOCTYPE html> | |||||
<html lang="en"> | |||||
<head> | |||||
<title>Games</title> | |||||
</head> | |||||
<body> | |||||
<h1>Games</h1> | |||||
<div>Links:</div> | |||||
<ul> | |||||
<li><a href="tictactoe.html">TicTacToe</a></li> | |||||
<li><a href="minesweeper.html">Minesweeper</a></li> | |||||
</ul> | |||||
</body> | |||||
</html> |