tutor.py 9.37 KB
Newer Older
mrk022's avatar
mrk022 committed
1
import ccm
2
import re
mrk022's avatar
mrk022 committed
3
from ccm.lib.actr import * 
4
5
import tree
import queue
mrk022's avatar
mrk022 committed
6

7
class TutorState:
mrk022's avatar
mrk022 committed
8
9
10
	''' Class to store the state that the tutor thinks the equation is in
		Tracks this by generating an equation tree from the tree.py file, and moving nodes accordingly
	'''
11
	def __init__(self,equation):
mrk022's avatar
mrk022 committed
12
		'''Initializes tree with an equation, and generates equation tree from this equation.'''
13
		self.equation = equation
14
15
16
		self.expressions = equation.split("=")
		self.tree = tree.generate_tree(equation)

17
	def move_values(self, left, constants):
mrk022's avatar
mrk022 committed
18
19
20
		''' If the user is moving either variables or constants, the tutor will call this 
			function to rearrange nodes in the tree accordingly
		'''
21
		moveNode = None
22
23
24
25
26
27
28
29
30
31
32
33
34
35
		newOp = None
		q = queue.Queue()
		if left: # if moving values from right to left
			node = self.tree.root.right
			q.enqueue(self.tree.root.left) 
		else: # if moving values from left to right
			node = self.tree.root.left 
			q.enqueue(self.tree.root.right)
		if constants: # if moving constants
			nodeType = tree.NumNode
			emptyNode = tree.VariableNode("0x")
		else: # if moving variables
			nodeType = tree.VariableNode
			emptyNode = tree.NumNode(0)
36
37
38
		if isinstance(node, tree.PlusNode) or isinstance(node, tree.MinusNode):
			# if plus or minus node
			if node.canEval() == False:
mrk022's avatar
mrk022 committed
39
				# if can't evaluate children, need to move one to the other side
40
				newOp = node.getInverse()
41
				if isinstance(node.left, nodeType):
42
					moveNode = node.left
43
44
					node.left = emptyNode
				elif isinstance(node.right, nodeType):
45
					moveNode = node.right
46
					node.right = emptyNode
mrk022's avatar
mrk022 committed
47
		moved = False # trackes if a node has been moved yet
48
49
50
		while not q.isEmpty() and not moved:
			currentNode = q.front()
			if currentNode.left != None:
51
				if isinstance(currentNode.left, nodeType):
52
53
54
55
56
57
58
					oldNum = currentNode.left
					currentNode.left = newOp
					currentNode.left.left = oldNum
					currentNode.left.right = moveNode
					moved = True
				q.enqueue(currentNode.left)
			if currentNode.right != None and not moved:
59
				if isinstance(currentNode.right, nodeType):
60
61
62
63
64
65
					oldNum = currentNode.right
					currentNode.right = newOp
					currentNode.right.left = oldNum
					currentNode.right.right = moveNode
					moved = True
				q.enqueue(currentNode.right)
66
			if isinstance(currentNode, nodeType) and not moved:
mrk022's avatar
mrk022 committed
67
				# case where there is no operator and want to shift node over
68
69
70
71
72
73
74
75
76
77
				oldNum =currentNode
				if left:
					self.tree.root.left = newOp
					self.tree.root.left.left = oldNum
					self.tree.root.left.right = moveNode
				else:
					self.tree.root.right = newOp
					self.tree.root.right.left = oldNum
					self.tree.root.right.right = moveNode
				moved=True
78
			q.dequeue()
mrk022's avatar
mrk022 committed
79
80
		if left: self.combine_like_terms(False) 
		else: self.combine_like_terms(True)
mrk022's avatar
mrk022 committed
81
		return self.tree.get_expression()
82
83

	def combine_like_terms(self, left):
mrk022's avatar
mrk022 committed
84
85
		''' Combines like terms on a designated side of the tree by simplifying like terms.
		'''
86
87
88
89
90
		self.tree.get_expression()
		if left:
			self.tree.evaluate(self.tree.root, True)
		else:
			self.tree.evaluate(self.tree.root, False)
91
		return self.tree.get_expression()
92

mrk022's avatar
mrk022 committed
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
	def compare_Equations(self, equation1, equation2):
		''' Compares two equations to see if both sides are equivalent, regardless of their order
			Assumes equations have operators that do not require a specific order
		'''
		equation1 = "".join(equation1.split())
		equation2 = "".join(equation2.split())
		eq1 = equation1.split('=')
		eq2 = equation2.split('=')
		eq1list = []
		eq2list = []
		for side in eq1:
			eq1list.append(sorted(re.split('[-|+|*|/]',side)))
		for side in eq2:
			eq2list.append(sorted(re.split('[-|+|*|/]',side)))
		#print(eq1list,eq2list)
		return eq1list==eq2list
109

mrk022's avatar
mrk022 committed
110

mrk022's avatar
mrk022 committed
111
112
113
class UserState(ccm.Model): 
	''' Class for the user state to track where the user is in their solution
	'''
114
	def __init__(self, equation):
mrk022's avatar
mrk022 committed
115
116
117
		''' Initializes the model itself, saves the equation, initial sides of the equation
			and initial count of variable types.
		'''
118
		ccm.Model.__init__(self)
mrk022's avatar
mrk022 committed
119
		self.equation = equation
120
		self.oldEquation = None
121
		self.sides = self.getLeftandRightVals()
mrk022's avatar
mrk022 committed
122
		self.constantCount = self.getConstantCount()
mrk022's avatar
mrk022 committed
123
		self.state = None
mrk022's avatar
mrk022 committed
124

125
	def get_input(self):
mrk022's avatar
mrk022 committed
126
127
128
		''' Gets input from the user by showing them the current state and asking for the next one.
			Takes their input and saves all needed information to process it accordingly.
		'''
129
		outputString = "Equation is currently: " + str(self.equation) +  "\nWhat is your next step? "
mrk022's avatar
mrk022 committed
130
		user_step = input(outputString) 
131
		self.oldEquation = self.equation
mrk022's avatar
mrk022 committed
132
		self.equation = user_step
133
134
135
136
		oldSides = self.sides
		self.sides = self.getLeftandRightVals()
		oldConstantCount = self.constantCount
		self.constantCount = self.getConstantCount() 
137
		self.state = self.get_state(oldConstantCount)
138
139

	def getLeftandRightVals(self):
mrk022's avatar
mrk022 committed
140
141
		''' Splits equation to get left and right sides of equation
		'''
142
		expressions = self.equation.split("=")
143
144
145
146
147
148
149
		try:
			leftVars = re.split("[+-]", expressions[0])
			rightVars = re.split("[+-]", expressions[1])
		except:
			print("INVALID INPUT, try again")
			self.equation=self.oldEquation
			self.get_input()
150
151
		return [leftVars, rightVars]
	
mrk022's avatar
mrk022 committed
152
	def getConstantCount(self):
mrk022's avatar
mrk022 committed
153
154
		''' Counts number of variables of each type on each side of the equation
		'''
155
156
157
158
159
160
161
		constantsOnLeft = 0
		variablesOnLeft = 0
		constantsOnRight = 0
		variablesOnRight = 0
		for value in self.sides[0]:
			if self.isConstant(value):
				constantsOnLeft += 1
162
			elif 'x' in value:
163
164
165
166
				variablesOnLeft += 1
		for value in self.sides[1]:
			if self.isConstant(value):
				constantsOnRight += 1
167
			elif 'x' in value:
168
				variablesOnRight += 1
mrk022's avatar
mrk022 committed
169
		return [variablesOnLeft, constantsOnLeft, variablesOnRight, constantsOnRight]
170
171

	def isConstant(self, value):
mrk022's avatar
mrk022 committed
172
173
		''' Given a value, checks if it is a constant.
		'''
174
		if not value.isdigit():
175
176
177
			return False
		return True

178

179
	def get_state(self, oldConstantCount):
mrk022's avatar
mrk022 committed
180
181
182
		''' Based on number of variables and constants on each side of the equation
			the agent guesses which state the user is moving to
		'''
183
		if oldConstantCount[0] != self.constantCount[0] and oldConstantCount[2] != self.constantCount[2]:
mrk022's avatar
mrk022 committed
184
			# if not same number of variables as before for both sides then moved variables
mrk022's avatar
mrk022 committed
185
186
			if oldConstantCount[0] < self.constantCount[0]: self.state = "move_variables 1"
			else: self.state = "move_variables 0"
187
		elif oldConstantCount[0] != self.constantCount[0] or oldConstantCount[2] != self.constantCount[2]:
188
189
			if oldConstantCount[0] > self.constantCount[0]: self.state = "add_variables 1"
			else: self.state = "add_variables 0"
190
		elif oldConstantCount[1] != self.constantCount[1] and oldConstantCount[3] != self.constantCount[3]:
191
192
193
194
			if oldConstantCount[1] < self.constantCount[1]: #moving constants to left
				self.state = "move_constants 1"
			else:
				self.state = "move_constants 0"
195
		elif oldConstantCount[1] != self.constantCount[1] or oldConstantCount[3] != self.constantCount[3]:
mrk022's avatar
mrk022 committed
196
197
			if oldConstantCount[1] > self.constantCount[1]: self.state = "add_constants 1"
			else: self.state = "add_constants 0"
mrk022's avatar
mrk022 committed
198
199
200
201
202
203
204
		elif ((self.constantCount[1] == 1 and self.constantCount[2] == 1) or 
			(self.constantCount[0] == 1 and self.constantCount[3] == 1)) and (self.sides[0][0] == 'x' or self.sides[1][0] == 'x'):
			# case where simplifying to solve for x
			self.state = "solve_for_x"
		else: 
			print("nothing triggered")
			self.state = "invalid_state nothing"
mrk022's avatar
mrk022 committed
205
		return self.state	
206

mrk022's avatar
mrk022 committed
207
class IntelligentTutor(ACTR):
mrk022's avatar
mrk022 committed
208
209
	''' RBES for the tutor itself. Tracks states
	'''
mrk022's avatar
mrk022 committed
210
	goal = Buffer()
211
	user = UserState("4x+7=5x+2")
mrk022's avatar
mrk022 committed
212
	tutor = TutorState("4x + 7 = 5x + 2")
mrk022's avatar
mrk022 committed
213

214
	def init():
215
		user.get_input()
216
		print("USER STATE IS ", user.state) # need to get user input to translate to state
mrk022's avatar
mrk022 committed
217
		goal.set(user.state) 
218
219
220
221

	def moved_constants(goal="move_constants ?left"): #true if moving constants left
		tutorEq = tutor.move_values(bool(int(left)), True)
		print("user equation ",user.equation)
mrk022's avatar
mrk022 committed
222
		goal.set("check_state " + tutorEq + " 0")
223
224
225
226

	def add_constants(goal= "add_constants ?left"):
		tutorEq = tutor.combine_like_terms(bool(int(left)))
		print("user equation ",user.equation)
mrk022's avatar
mrk022 committed
227
		goal.set("check_state " + tutorEq + " 0")
228
229
230
231

	def moved_variables(goal="move_variables ?left"):
		tutorEq = tutor.move_values(bool(int(left)), False)
		print("user equation ",user.equation)
mrk022's avatar
mrk022 committed
232
		goal.set("check_state " + tutorEq + " 0")
233
234
235
236

	def add_variables(goal = "add_variables ?left"):
		tutorEq = tutor.combine_like_terms(bool(int(left)))
		print("user equation", user.equation)
mrk022's avatar
mrk022 committed
237
		goal.set("check_state " + tutorEq + " 0")
mrk022's avatar
mrk022 committed
238

mrk022's avatar
mrk022 committed
239
240
241
242
243
244
	def solve(goal = "solve_for_x"):
		tutorEq = tutor.tree.solve()
		goal.set("check_state " + tutorEq + " 1")

	def check_state(goal = "check_state ?tutorEq ?solved"):
		print("checking state")
mrk022's avatar
mrk022 committed
245
		if tutor.compare_Equations(tutorEq, user.equation):
mrk022's avatar
mrk022 committed
246
247
248
249
250
251
			if bool(int(solved)):
				print("You have solved the equation by isolating x.")
				goal.set("end_process")
			else:
				user.get_input()
				goal.set(user.state)
mrk022's avatar
mrk022 committed
252
		else:
253
			goal.set("invalid_state " + tutorEq)
mrk022's avatar
mrk022 committed
254

255
256
257
	def incorrect_state(goal = "invalid_state ?tutorEqn"):
		print("You have entered an invalid state. We anticipated the correct step to be: ", tutorEqn, "\nPlease continue solving the problem with the corrected equation.")
		user.equation = tutorEqn
mrk022's avatar
mrk022 committed
258
259
260
		user.get_input()
		goal.set(user.state)

mrk022's avatar
mrk022 committed
261
262
263
264
265
266
267
268
269
270
	def end_process(goal = "end_process"):
		goal.set("Simulation Complete!")
		self.stop()

class EmptyEnvironment(ccm.Model):
	pass

def main():
	env_name = EmptyEnvironment()
	agent_name = IntelligentTutor()
mrk022's avatar
mrk022 committed
271
	env_name.agent = agent_name 
mrk022's avatar
mrk022 committed
272
	ccm.log_everything(env_name) # log statement, uncomment if you want to log information
mrk022's avatar
mrk022 committed
273
274
275
	env_name.run()

main()