From bb0aa379cebcc218e5ef11e25d2dd8e1805db218 Mon Sep 17 00:00:00 2001 From: LjAquinox <125894602+LjAquinox@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:14:11 +0100 Subject: [PATCH 1/9] Update README.md --- README.md | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index bb7bf69..e9c1812 100644 --- a/README.md +++ b/README.md @@ -1 +1,75 @@ -# Car-QLearning \ No newline at end of file +# Car-QLearning + +Requirements (versions are mostly here as an indication): + +``` +Pyglet (1.5.27) +Pygame (2.1.3) +numpy (1.24.1) +tensorflow (2.10.1) +``` + +How to use : + +I) Download the files + + +II) If you want to create your own track + + +1) Create your own .png file (I recommend not to change size if you don't want to touch the code). +When you are done name it track.png. + +3) Once done designing, in Games_Solo.py empty the set_wall function and the set-gates function but leave this line in set_gates : + self.gates.append(RewardGate(0, 1, 2, 3)) + +4) You can now run Main_Solo.py program, you should see you track + +5) Time to set up the gates. You can setup gates using your mouse left click. These gate are where the AI gain points. Once you are done close the program you should see in the console a lot of text similar to : +``` +self.gates.append(RewardGate(343, 379, 524, 405)) +self.gates.append(RewardGate(488, 326, 626, 421)) +self.gates.append(RewardGate(626, 309, 701, 411)) +self.gates.append(RewardGate(232, 309, 267, 399)) +``` +Copy this text and paste it in the set_gates function you emptied earlier. Be careful the order of the gates is important. + +6) Time to set up the Walls. In the Main_Solo.py program go to the "on_mouse_press" function then swap the lines in comments and the one that are not. You should get: + +``` + def on_mouse_press(self, x, y, button, modifiers): + if self.firstClick: + self.clickPos = [x, y] + else: + #print("self.gates.append(RewardGate({}, {}, {}, {}))".format(self.clickPos[0],displayHeight - self.clickPos[1],x, displayHeight - y)) + #self.game.gates.append(RewardGate(self.clickPos[0], displayHeight - self.clickPos[1], x, displayHeight - y)) + + print("self.walls.append(Wall({}, {}, {}, {}))".format(self.clickPos[0],displayHeight - self.clickPos[1],x, displayHeight - y)) + self.game.walls.append(Wall(self.clickPos[0], self.clickPos[1], x, y)) + + self.firstClick = not self.firstClick + pass + +``` +You can now setup walls using your mouse left click. Once you are done close the program you should see in the console a lot of text similar to before but saying : + +``` +self.walls.append(Wall(343, 379, 524, 405)) ... +``` + +Once again copy this text and paste it in the set_walls function of Games_Solo.py. + +7) Change the start and reset position/direction of the car in Games_Solo.py in the car class in the __init__ function and in the reset function (change self.x, self.y) + +8) Check everything is good by trying your track in the Main_Solo program you should die if you touch the walls and you should see gates disappear when passing them (if not you probably didn't placed the in the right order) + +9) copy your set_walls and set_gates function into the Games.py by replacing the old one. (don't forget to change your start and reset position/direction too) + +10) You are now done creating your personal track. + + +III) You now want to train the AI to perform on your track so run the Main.py program. You should see the the car moving on it's own and learning slowly the track. + +PS : It seems that the load or save fuction aren't working properly so don't close your program until your are satisfied =) (I personally got result at around Model 5000 so be patient) + +If you need some help you can contact me by discord at Aquinox#4429. I'll try to help you as best as I can. From 2c4944bcf49493216482cc3995f144be62d291e2 Mon Sep 17 00:00:00 2001 From: LjAquinox <125894602+LjAquinox@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:15:02 +0100 Subject: [PATCH 2/9] Add files via upload --- Games_Solo.py | 606 ++++++++++++++++++++++++++++++++++++++++++++++++++ Main_Solo.py | 91 ++++++++ 2 files changed, 697 insertions(+) create mode 100644 Games_Solo.py create mode 100644 Main_Solo.py diff --git a/Games_Solo.py b/Games_Solo.py new file mode 100644 index 0000000..2392ad3 --- /dev/null +++ b/Games_Solo.py @@ -0,0 +1,606 @@ +import numpy as np +from Global import * +from Draw import Drawer +from ShapeObjects import * +from PygameAdditionalMethods import * +import pygame + +drawer = Drawer() +vec2 = pygame.math.Vector2 + +class Game: + no_of_actions = 9 + state_size = 15 + + def __init__(self): + trackImg = pyglet.image.load('Track.png') + self.trackSprite = pyglet.sprite.Sprite(trackImg, x=0, y=0) + + # initiate walls + self.walls = [] + self.gates = [] + + self.set_walls() + self.set_gates() + self.firstClick = True + + self.car = Car(self.walls, self.gates) + + def set_walls(self): + self.walls.append(Wall(240, 809, 200, 583)) + self.walls.append(Wall(200, 583, 218, 395)) + self.walls.append(Wall(218, 395, 303, 255)) + self.walls.append(Wall(303, 255, 548, 173)) + self.walls.append(Wall(548, 173, 764, 179)) + self.walls.append(Wall(764, 179, 1058, 198)) + self.walls.append(Wall(1055, 199, 1180, 215)) + self.walls.append(Wall(1177, 215, 1220, 272)) + self.walls.append(Wall(1222, 273, 1218, 367)) + self.walls.append(Wall(1218, 367, 1150, 437)) + self.walls.append(Wall(1150, 437, 1044, 460)) + self.walls.append(Wall(1044, 460, 757, 600)) + self.walls.append(Wall(757, 600, 1099, 570)) + self.walls.append(Wall(1100, 570, 1187, 508)) + self.walls.append(Wall(1187, 507, 1288, 443)) + self.walls.append(Wall(1288, 443, 1463, 415)) + self.walls.append(Wall(1463, 415, 1615, 478)) + self.walls.append(Wall(1617, 479, 1727, 679)) + self.walls.append(Wall(1727, 679, 1697, 874)) + self.walls.append(Wall(1694, 872, 1520, 964)) + self.walls.append(Wall(1520, 964, 1100, 970)) + self.walls.append(Wall(1105, 970, 335, 960)) + self.walls.append(Wall(339, 960, 264, 899)) + self.walls.append(Wall(263, 897, 238, 803)) + self.walls.append(Wall(317, 782, 274, 570)) + self.walls.append(Wall(275, 569, 284, 407)) + self.walls.append(Wall(284, 407, 363, 317)) + self.walls.append(Wall(363, 317, 562, 240)) + self.walls.append(Wall(562, 240, 1114, 284)) + self.walls.append(Wall(1114, 284, 1120, 323)) + self.walls.append(Wall(1120, 323, 1045, 377)) + self.walls.append(Wall(1045, 378, 682, 548)) + self.walls.append(Wall(682, 548, 604, 610)) + self.walls.append(Wall(604, 612, 603, 695)) + self.walls.append(Wall(605, 695, 702, 713)) + self.walls.append(Wall(703, 712, 1128, 642)) + self.walls.append(Wall(1129, 642, 1320, 512)) + self.walls.append(Wall(1323, 512, 1464, 497)) + self.walls.append(Wall(1464, 497, 1579, 535)) + self.walls.append(Wall(1579, 535, 1660, 701)) + self.walls.append(Wall(1660, 697, 1634, 818)) + self.walls.append(Wall(1634, 818, 1499, 889)) + self.walls.append(Wall(1499, 889, 395, 883)) + self.walls.append(Wall(395, 883, 330, 838)) + self.walls.append(Wall(330, 838, 315, 782)) + self.walls.append(Wall(319, 798, 306, 725)) + self.walls.append(Wall(276, 580, 277, 543)) + self.walls.append(Wall(603, 639, 622, 590)) + self.walls.append(Wall(599, 655, 621, 704)) + self.walls.append(Wall(1074, 571, 1115, 558)) + self.walls.append(Wall(1314, 516, 1333, 511)) + self.walls.append(Wall(1692, 875, 1706, 830)) + self.walls.append(Wall(277, 912, 255, 872)) + self.walls.append(Wall(1214, 262, 1225, 288)) + self.walls.append(Wall(1601, 470, 1625, 490)) + self.walls.append(Wall(1119, 644, 1139, 634)) + self.walls.append(Wall(687, 710, 719, 710)) + self.walls.append(Wall(1721, 664, 1727, 696)) + self.walls.append(Wall(1015, 392, 1065, 362)) + self.walls.append(Wall(1091, 572, 1104, 568)) + self.walls.append(Wall(1157, 528, 1233, 478)) + + def set_gates(self): + + self.gates.append(RewardGate(212, 645, 288, 634)) + self.gates.append(RewardGate(206, 518, 279, 526)) + self.gates.append(RewardGate(224, 390, 286, 416)) + self.gates.append(RewardGate(302, 261, 369, 314)) + self.gates.append(RewardGate(545, 175, 561, 236)) + self.gates.append(RewardGate(846, 182, 841, 259)) + self.gates.append(RewardGate(1114, 203, 1100, 282)) + self.gates.append(RewardGate(1217, 297, 1113, 300)) + self.gates.append(RewardGate(1185, 403, 1102, 339)) + self.gates.append(RewardGate(1042, 462, 979, 408)) + self.gates.append(RewardGate(876, 543, 807, 482)) + self.gates.append(RewardGate(765, 598, 693, 545)) + self.gates.append(RewardGate(801, 596, 815, 694)) + self.gates.append(RewardGate(883, 587, 904, 680)) + self.gates.append(RewardGate(1102, 567, 1128, 640)) + self.gates.append(RewardGate(1261, 452, 1304, 514)) + self.gates.append(RewardGate(1461, 412, 1454, 499)) + self.gates.append(RewardGate(1615, 480, 1572, 535)) + self.gates.append(RewardGate(1722, 680, 1655, 698)) + self.gates.append(RewardGate(1693, 873, 1623, 815)) + self.gates.append(RewardGate(1510, 966, 1495, 886)) + self.gates.append(RewardGate(1297, 970, 1282, 888)) + self.gates.append(RewardGate(1054, 971, 1045, 887)) + self.gates.append(RewardGate(925, 969, 907, 885)) + self.gates.append(RewardGate(742, 969, 733, 884)) + self.gates.append(RewardGate(549, 965, 537, 880)) + self.gates.append(RewardGate(295, 920, 361, 864)) + self.gates.append(RewardGate(238, 766, 309, 754)) + + + def new_episode(self): + self.car.reset() + + def get_state(self): + return self.car.getState() + + def make_action(self, action): + # returns reward + actionNo = np.argmax(action) + self.car.updateWithAction(actionNo) + return self.car.reward + + def is_episode_finished(self): + return self.car.dead + + def get_score(self): + return self.car.score + + def get_lifespan(self): + return self.car.lifespan + + def render(self): + glPushMatrix() + self.trackSprite.draw() + + for w in self.walls: + w.draw() + for g in self.gates: + g.draw() + self.car.update() + self.car.show() + self.car.showCollisionVectors() + + glPopMatrix() + +class Wall: + + def __init__(self, x1, y1, x2, y2): + self.x1 = x1 + self.y1 = displayHeight - y1 + self.x2 = x2 + self.y2 = displayHeight - y2 + + self.line = Line(self.x1, self.y1, self.x2, self.y2) + self.line.setLineThinkness(5) + + """ + draw the line + """ + def draw(self): + self.line.draw() + """ + returns true if the car object has hit this wall + """ + + def hitCar(self, car): + global vec2 + cw = car.width + ch = car.height + rightVector = vec2(car.direction) + upVector = vec2(car.direction).rotate(-90) + carCorners = [] + cornerMultipliers = [[1, 1], [1, -1], [-1, -1], [-1, 1]] + carPos = vec2(car.x, car.y) + for i in range(4): + carCorners.append(carPos + (rightVector * cw / 2 * cornerMultipliers[i][0]) + + (upVector * ch / 2 * cornerMultipliers[i][1])) + + for i in range(4): + j = i + 1 + j = j % 4 + if linesCollided(self.x1, self.y1, self.x2, self.y2, carCorners[i].x, carCorners[i].y, carCorners[j].x, + carCorners[j].y): + return True + return False + + +""" +class containing all the game logic for moving and displaying the car +""" + + +class RewardGate: + + def __init__(self, x1, y1, x2, y2): + global vec2 + self.x1 = x1 + self.y1 = displayHeight - y1 + self.x2 = x2 + self.y2 = displayHeight - y2 + self.active = True + + self.line = Line(self.x1, self.y1, self.x2, self.y2) + self.line.setLineThinkness(5) + self.line.setColor([0, 255, 0]) + + self.center = vec2((self.x1 + self.x2) / 2, (self.y1 + self.y2) / 2) + + """ + draw the line + """ + + def draw(self): + if self.active: + self.line.draw() + + """ + returns true if the car object has hit this wall + """ + + def hitCar(self, car): + + if not self.active: + return False + + global vec2 + cw = car.width + ch = car.height + rightVector = vec2(car.direction) + upVector = vec2(car.direction).rotate(-90) + carCorners = [] + cornerMultipliers = [[1, 1], [1, -1], [-1, -1], [-1, 1]] + carPos = vec2(car.x, car.y) + for i in range(4): + carCorners.append(carPos + (rightVector * cw / 2 * cornerMultipliers[i][0]) + + (upVector * ch / 2 * cornerMultipliers[i][1])) + + for i in range(4): + j = i + 1 + j = j % 4 + if linesCollided(self.x1, self.y1, self.x2, self.y2, carCorners[i].x, carCorners[i].y, carCorners[j].x, + carCorners[j].y): + return True + return False + + + + + +class Car: + + def __init__(self, walls, rewardGates): + global vec2 + self.nbVect = 16 + self.angles = np.linspace(-180, 180, self.nbVect) + self.x = 256 + self.y = 288 + self.vel = 0 + self.direction = vec2(0, 1) + self.direction = self.direction.rotate(180 / 12) + self.acc = 0 + self.width = 40 + self.height = 20 + self.turningRate = 5.0 / self.width + self.friction = 0.98 + self.maxSpeed = self.width / 4.0 + self.maxReverseSpeed = -1 #self.maxSpeed / 16.0 is used as a minimum for speed + self.accelerationSpeed = self.width / 160.0 + self.dead = False + self.driftMomentum = 0 + self.driftFriction = 0.87 + self.lineCollisionPoints = [] + self.collisionLineDistances = [] + self.vectorLength = 600 + + self.carPic = pyglet.image.load('Car.png') + self.carSprite = pyglet.sprite.Sprite(self.carPic, x=self.x, y=self.y) + self.carSprite.update(rotation=0, scale_x=self.width / self.carSprite.width, + scale_y=self.height / self.carSprite.height) + + self.turningLeft = False + self.turningRight = False + self.accelerating = False + self.reversing = False + self.walls = walls + self.rewardGates = rewardGates + self.rewardNo = 0 + + self.directionToRewardGate = self.rewardGates[self.rewardNo].center - vec2(self.x, self.y) + + self.reward = 0 + + self.score = 0 + self.lifespan = 0 + """ + draws the car to the screen + """ + + def reset(self): + global vec2 + self.x = 256 + self.y = 288 + self.vel = 0 + self.direction = vec2(0, 1) + self.direction = self.direction.rotate(180 / 12) + self.acc = 0 + self.dead = False + self.driftMomentum = 0 + self.lineCollisionPoints = [] + self.collisionLineDistances = [] + + self.turningLeft = False + self.turningRight = False + self.accelerating = False + self.reversing = False + self.rewardNo = 0 + self.reward = 0 + + self.lifespan = 0 + self.score = 0 + for g in self.rewardGates: + g.active = True + + def show(self): + # first calculate the center of the car in order to allow the + # rotation of the car to be anchored around the center + upVector = self.direction.rotate(90) + drawX = self.direction.x * self.width / 2 + upVector.x * self.height / 2 + drawY = self.direction.y * self.width / 2 + upVector.y * self.height / 2 + self.carSprite.update(x=self.x - drawX, y=self.y - drawY, rotation=-get_angle(self.direction)) + self.carSprite.draw() + # self.showCollisionVectors() + + """ + returns a vector of where a point on the car is after rotation + takes the position desired relative to the center of the car when the car is facing to the right + """ + + def getPositionOnCarRelativeToCenter(self, right, up): + global vec2 + w = self.width + h = self.height + rightVector = vec2(self.direction) + rightVector.normalize() + upVector = self.direction.rotate(90) + upVector.normalize() + + return vec2(self.x, self.y) + ((rightVector * right) + (upVector * up)) + + def updateWithAction(self, actionNo): + self.turningLeft = False + self.turningRight = False + self.accelerating = False + self.reversing = False + + if actionNo == 0: + self.turningLeft = True + elif actionNo == 1: + self.turningRight = True + elif actionNo == 2: + self.accelerating = True + elif actionNo == 3: + self.reversing = True + elif actionNo == 4: + self.accelerating = True + self.turningLeft = True + elif actionNo == 5: + self.accelerating = True + self.turningRight = True + elif actionNo == 6: + self.reversing = True + self.turningLeft = True + elif actionNo == 7: + self.reversing = True + self.turningRight = True + elif actionNo == 8: + pass + totalReward = 0 + + for i in range(1): + if not self.dead: + self.lifespan+=1 + + self.updateControls() + self.move() + + if self.hitAWall(): + self.dead = True + # return + self.checkRewardGates() + totalReward += self.reward + + self.setVisionVectors() + + # self.update() + + self.reward = totalReward + + """ + called every frame + """ + + def update(self): + if not self.dead: + self.updateControls() + self.move() + + if self.hitAWall(): + self.dead = True + # return + self.checkRewardGates() + self.setVisionVectors() + + def checkRewardGates(self): + global vec2 + self.reward = -1 + if self.rewardGates[self.rewardNo].hitCar(self): + self.rewardGates[self.rewardNo].active = False + self.rewardNo += 1 + self.score += 1 + self.reward = 10 + if self.rewardNo == len(self.rewardGates): + self.rewardNo = 0 + for g in self.rewardGates: + g.active = True + self.directionToRewardGate = self.rewardGates[self.rewardNo].center - vec2(self.x, self.y) + + """ + changes the position of the car to account for acceleration, velocity, friction and drift + """ + + def move(self): + global vec2 + self.vel += self.acc + self.vel *= self.friction + self.constrainVel() + + driftVector = vec2(self.direction) + driftVector = driftVector.rotate(90) + + addVector = vec2(0, 0) + addVector.x += self.vel * self.direction.x + addVector.x += self.driftMomentum * driftVector.x + addVector.y += self.vel * self.direction.y + addVector.y += self.driftMomentum * driftVector.y + self.driftMomentum *= self.driftFriction + + if addVector.length() != 0: + addVector.normalize() + + addVector.x * abs(self.vel) + addVector.y * abs(self.vel) + + self.x += addVector.x + self.y += addVector.y + + """ + keeps the velocity of the car within the maximum and minimum speeds + """ + + def constrainVel(self): + if self.maxSpeed < self.vel: + self.vel = self.maxSpeed + elif self.vel < self.maxReverseSpeed: + self.vel = self.maxReverseSpeed + + """ + changes the cars direction and acceleration based on the users inputs + """ + + def updateControls(self): + multiplier = 1 + if abs(self.vel) < 5: + multiplier = abs(self.vel) / 5 + if self.vel < 0: + multiplier *= -1 + + driftAmount = self.vel * self.turningRate * self.width / (9.0 * 8.0) + if self.vel < 5: + driftAmount = 0 + + if self.turningLeft: + self.direction = self.direction.rotate(radiansToAngle(self.turningRate) * multiplier) + + self.driftMomentum -= driftAmount + elif self.turningRight: + self.direction = self.direction.rotate(-radiansToAngle(self.turningRate) * multiplier) + self.driftMomentum += driftAmount + self.acc = 0 + if self.accelerating: + if self.vel < 0: + self.acc = 3 * self.accelerationSpeed + else: + self.acc = self.accelerationSpeed + elif self.reversing: + if self.vel > 0: + self.acc = -2 * self.accelerationSpeed + else: + self.acc = 0 + self.vel = 0 + + """ + checks every wall and if the car has hit a wall returns true + """ + + def hitAWall(self): + for wall in self.walls: + if wall.hitCar(self): + #print(self.x,self.y) + return True + + return False + + """ + returns the point of collision of a line (x1,y1,x2,y2) with the walls, + if multiple walls are hit it returns the closest collision point + """ + + def getCollisionPointOfClosestWall(self, x1, y1, x2, y2): + global vec2 + minDist = 2 * displayWidth + closestCollisionPoint = vec2(0, 0) + for wall in self.walls: + collisionPoint = getCollisionPoint(x1, y1, x2, y2, wall.x1, wall.y1, wall.x2, wall.y2) + if collisionPoint is None: + continue + if dist(x1, y1, collisionPoint.x, collisionPoint.y) < minDist: + minDist = dist(x1, y1, collisionPoint.x, collisionPoint.y) + closestCollisionPoint = vec2(collisionPoint) + return closestCollisionPoint + + """ + by creating lines in many directions from the car and getting the closest collision point of that line + we create "vision vectors" which will allow the car to 'see' + kinda like a sonar system + """ + + def getState(self): + self.setVisionVectors() + normalizedVisionVectors = [1 - (max(1.0, line) / self.vectorLength) for line in self.collisionLineDistances] + + normalizedForwardVelocity = max(0.0, self.vel / self.maxSpeed) + normalizedReverseVelocity = max(0.0, self.vel / self.maxReverseSpeed) + if self.driftMomentum > 0: + normalizedPosDrift = self.driftMomentum / 5 + normalizedNegDrift = 0 + else: + normalizedPosDrift = 0 + normalizedNegDrift = self.driftMomentum / -5 + + normalizedAngleOfNextGate = (get_angle(self.direction) - get_angle(self.directionToRewardGate)) % 360 + if normalizedAngleOfNextGate > 180: + normalizedAngleOfNextGate = -1 * (360 - normalizedAngleOfNextGate) + + normalizedAngleOfNextGate /= 180 + + normalizedState = [*normalizedVisionVectors, normalizedForwardVelocity, normalizedReverseVelocity, + normalizedPosDrift, normalizedNegDrift, normalizedAngleOfNextGate] + return np.array(normalizedState) + + def setVisionVectors(self): + self.collisionLineDistances = [] + self.lineCollisionPoints = [] + for i in self.angles : + self.setVisionVector(0,0,i) + + """ + calculates and stores the distance to the nearest wall given a vector + """ + + def setVisionVector(self, startX, startY, angle): + collisionVectorDirection = self.direction.rotate(angle) + collisionVectorDirection = collisionVectorDirection.normalize() * self.vectorLength + startingPoint = self.getPositionOnCarRelativeToCenter(startX, startY) + collisionPoint = self.getCollisionPointOfClosestWall(startingPoint.x, startingPoint.y, + startingPoint.x + collisionVectorDirection.x, + startingPoint.y + collisionVectorDirection.y) + if collisionPoint.x == 0 and collisionPoint.y == 0: + self.collisionLineDistances.append(self.vectorLength) + else: + self.collisionLineDistances.append( + dist(startingPoint.x, startingPoint.y, collisionPoint.x, collisionPoint.y)) + self.lineCollisionPoints.append(collisionPoint) + + """ + shows dots where the collision vectors detect a wall + """ + + def showCollisionVectors(self): + global drawer + for point in self.lineCollisionPoints: + drawer.setColor([255, 0, 0]) + drawer.circle(point.x, point.y, 5) \ No newline at end of file diff --git a/Main_Solo.py b/Main_Solo.py new file mode 100644 index 0000000..766851e --- /dev/null +++ b/Main_Solo.py @@ -0,0 +1,91 @@ +import pyglet +from pyglet.window import key +from pyglet.gl import * +from Global import * +import pygame +from Games_Solo import Game, Wall, RewardGate + +vec2 = pygame.math.Vector2 + +class MyWindow(pyglet.window.Window): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_minimum_size(400, 300) + # set background color + backgroundColor = [0, 0, 0, 255] + backgroundColor = [i / 255 for i in backgroundColor] + glClearColor(*backgroundColor) + self.clear() + self.game = Game() + + + self.firstClick = True + + def on_key_press(self, symbol, modifiers): + if symbol == key.RIGHT: + self.game.car.turningRight = True + + if symbol == key.LEFT: + self.game.car.turningLeft = True + + if symbol == key.UP: + self.game.car.accelerating = True + + if symbol == key.DOWN: + self.game.car.reversing = True + + def on_key_release(self, symbol, modifiers): + if symbol == key.RIGHT: + self.game.car.turningRight = False + + if symbol == key.LEFT: + self.game.car.turningLeft = False + + if symbol == key.UP: + self.game.car.accelerating = False + + if symbol == key.DOWN: + self.game.car.reversing = False + + if symbol == key.SPACE: + self.ai.training = not self.ai.training + + def on_mouse_press(self, x, y, button, modifiers): + if self.firstClick: + self.clickPos = [x, y] + else: + print("self.gates.append(RewardGate({}, {}, {}, {}))".format(self.clickPos[0],displayHeight - self.clickPos[1],x, displayHeight - y)) + self.game.gates.append(RewardGate(self.clickPos[0], displayHeight - self.clickPos[1], x, displayHeight - y)) + + #print("self.walls.append(Wall({}, {}, {}, {}))".format(self.clickPos[0],displayHeight - self.clickPos[1],x, displayHeight - y)) + #self.game.walls.append(Wall(self.clickPos[0], self.clickPos[1], x, y)) + + self.firstClick = not self.firstClick + pass + + + def on_draw(self): + + window.set_size(width=displayWidth,height=displayHeight) + self.clear() + self.game.render() + vision = self.game.car.getState() + for i in range(len(vision)): + + label = pyglet.text.Label("{}: {}".format(i,vision[i]), + font_name='Times New Roman', + font_size=12, + x=10, y=20*i+50, + anchor_x='left', anchor_y='center') + label.draw() + + + def update(self,dt): + pass + + + +if __name__ == "__main__": + window = MyWindow(displayWidth, displayHeight, "AI Learns to Drive", resizable=False) + pyglet.clock.schedule_interval(window.update, 1 / frameRate) + pyglet.app.run() \ No newline at end of file From 6a71350ba0d6b3a3f848f111713e5c69f00fa7b7 Mon Sep 17 00:00:00 2001 From: LjAquinox <125894602+LjAquinox@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:15:26 +0100 Subject: [PATCH 3/9] Update Game.py --- Game.py | 155 ++++++++++++++++++++++++-------------------------------- 1 file changed, 66 insertions(+), 89 deletions(-) diff --git a/Game.py b/Game.py index 137b65d..41a5c4f 100644 --- a/Game.py +++ b/Game.py @@ -1,7 +1,6 @@ import numpy as np -import pyglet -from Globals import displayWidth, displayHeight -from Drawer import Drawer +from Global import * +from Draw import Drawer from ShapeObjects import * from PygameAdditionalMethods import * import pygame @@ -9,15 +8,13 @@ drawer = Drawer() vec2 = pygame.math.Vector2 - class Game: no_of_actions = 9 - state_size = 15 + state_size = 20 #self.nbVect + 4 def __init__(self): - trackImg = pyglet.image.load('images/track.png') + trackImg = pyglet.image.load('Track.png') self.trackSprite = pyglet.sprite.Sprite(trackImg, x=0, y=0) - # initiate car # initiate walls self.walls = [] @@ -93,56 +90,47 @@ def set_walls(self): self.walls.append(Wall(1157, 528, 1233, 478)) def set_gates(self): - self.gates.append(RewardGate(314, 345, 200, 326)) - self.gates.append(RewardGate(187, 435, 311, 451)) - self.gates.append(RewardGate(307, 537, 171, 555)) - self.gates.append(RewardGate(234, 681, 345, 628)) - self.gates.append(RewardGate(408, 682, 363, 788)) - self.gates.append(RewardGate(428, 816, 481, 712)) - self.gates.append(RewardGate(568, 733, 543, 854)) - self.gates.append(RewardGate(678, 858, 675, 710)) - self.gates.append(RewardGate(852, 708, 855, 848)) - self.gates.append(RewardGate(995, 836, 985, 705)) - self.gates.append(RewardGate(1059, 710, 1076, 821)) - self.gates.append(RewardGate(1078, 667, 1172, 572)) - self.gates.append(RewardGate(997, 616, 1076, 532)) - self.gates.append(RewardGate(967, 492, 909, 566)) - self.gates.append(RewardGate(788, 512, 839, 438)) - self.gates.append(RewardGate(790, 405, 781, 285)) - self.gates.append(RewardGate(891, 302, 899, 427)) - self.gates.append(RewardGate(1004, 434, 1027, 334)) - self.gates.append(RewardGate(1139, 344, 1084, 452)) - self.gates.append(RewardGate(1171, 502, 1233, 416)) - self.gates.append(RewardGate(1305, 454, 1243, 556)) - self.gates.append(RewardGate(1365, 588, 1408, 480)) - self.gates.append(RewardGate(1487, 472, 1524, 587)) - self.gates.append(RewardGate(1642, 508, 1575, 432)) - self.gates.append(RewardGate(1608, 360, 1709, 419)) - self.gates.append(RewardGate(1744, 324, 1625, 296)) - self.gates.append(RewardGate(1609, 231, 1727, 190)) - self.gates.append(RewardGate(1617, 66, 1541, 163)) - self.gates.append(RewardGate(1487, 135, 1510, 14)) - self.gates.append(RewardGate(1344, 16, 1328, 150)) - self.gates.append(RewardGate(1077, 142, 1067, 14)) - self.gates.append(RewardGate(909, 16, 900, 130)) - self.gates.append(RewardGate(718, 138, 698, 20)) - self.gates.append(RewardGate(551, 18, 567, 132)) - self.gates.append(RewardGate(445, 138, 413, 13)) - self.gates.append(RewardGate(379, 154, 243, 80)) - self.gates.append(RewardGate(357, 221, 203, 182)) + + self.gates.append(RewardGate(212, 645, 288, 634)) + self.gates.append(RewardGate(206, 518, 279, 526)) + self.gates.append(RewardGate(224, 390, 286, 416)) + self.gates.append(RewardGate(302, 261, 369, 314)) + self.gates.append(RewardGate(545, 175, 561, 236)) + self.gates.append(RewardGate(846, 182, 841, 259)) + self.gates.append(RewardGate(1114, 203, 1100, 282)) + self.gates.append(RewardGate(1217, 297, 1113, 300)) + self.gates.append(RewardGate(1185, 403, 1102, 339)) + self.gates.append(RewardGate(1042, 462, 979, 408)) + self.gates.append(RewardGate(876, 543, 807, 482)) + self.gates.append(RewardGate(765, 598, 693, 545)) + self.gates.append(RewardGate(801, 596, 815, 694)) + self.gates.append(RewardGate(883, 587, 904, 680)) + self.gates.append(RewardGate(1102, 567, 1128, 640)) + self.gates.append(RewardGate(1261, 452, 1304, 514)) + self.gates.append(RewardGate(1461, 412, 1454, 499)) + self.gates.append(RewardGate(1615, 480, 1572, 535)) + self.gates.append(RewardGate(1722, 680, 1655, 698)) + self.gates.append(RewardGate(1693, 873, 1623, 815)) + self.gates.append(RewardGate(1510, 966, 1495, 886)) + self.gates.append(RewardGate(1297, 970, 1282, 888)) + self.gates.append(RewardGate(1054, 971, 1045, 887)) + self.gates.append(RewardGate(925, 969, 907, 885)) + self.gates.append(RewardGate(742, 969, 733, 884)) + self.gates.append(RewardGate(549, 965, 537, 880)) + self.gates.append(RewardGate(295, 920, 361, 864)) + self.gates.append(RewardGate(238, 766, 309, 754)) def new_episode(self): self.car.reset() - def get_state(self): return self.car.getState() pass def make_action(self, action): # returns reward - actionNo = np.argmax(action) - self.car.updateWithAction(actionNo) + #actionNo = np.argmax(action) + self.car.updateWithAction(action) return self.car.reward def is_episode_finished(self): @@ -156,18 +144,14 @@ def get_lifespan(self): def render(self): glPushMatrix() - # - # glTranslatef(-1, -1, 0) - # glScalef(1 / (displayWidth / 2), 1 / (displayHeight / 2), 1) - - # self.clear() self.trackSprite.draw() - self.car.show() - # for w in self.walls: - # w.draw() - # for g in self.gates: - # g.draw() + for w in self.walls: + w.draw() + for g in self.gates: + g.draw() + self.car.show() + #self.car.showCollisionVectors() glPopMatrix() @@ -181,15 +165,14 @@ def __init__(self, x1, y1, x2, y2): self.y2 = displayHeight - y2 self.line = Line(self.x1, self.y1, self.x2, self.y2) - self.line.setLineThinkness(2) + self.line.setLineThinkness(5) + self.line.setColor([255, 0, 0]) """ draw the line """ - def draw(self): self.line.draw() - """ returns true if the car object has hit this wall """ @@ -197,8 +180,7 @@ def draw(self): def hitCar(self, car): global vec2 cw = car.width - # since the car sprite isn't perfectly square the hitbox is a little smaller than the width of the car - ch = car.height - 4 + ch = car.height rightVector = vec2(car.direction) upVector = vec2(car.direction).rotate(-90) carCorners = [] @@ -213,6 +195,7 @@ def hitCar(self, car): j = j % 4 if linesCollided(self.x1, self.y1, self.x2, self.y2, carCorners[i].x, carCorners[i].y, carCorners[j].x, carCorners[j].y): + #print("u ded") return True return False @@ -227,9 +210,9 @@ class RewardGate: def __init__(self, x1, y1, x2, y2): global vec2 self.x1 = x1 - self.y1 = y1 + self.y1 = displayHeight - y1 self.x2 = x2 - self.y2 = y2 + self.y2 = displayHeight - y2 self.active = True self.line = Line(self.x1, self.y1, self.x2, self.y2) @@ -256,8 +239,7 @@ def hitCar(self, car): global vec2 cw = car.width - # since the car sprite isn't perfectly square the hitbox is a little smaller than the width of the car - ch = car.height - 4 + ch = car.height rightVector = vec2(car.direction) upVector = vec2(car.direction).rotate(-90) carCorners = [] @@ -276,10 +258,13 @@ def hitCar(self, car): return False + class Car: def __init__(self, walls, rewardGates): global vec2 + self.nbVect = 16 + self.angles = np.linspace(-180, 180, self.nbVect) self.x = 258 self.y = 288 self.vel = 0 @@ -291,16 +276,16 @@ def __init__(self, walls, rewardGates): self.turningRate = 5.0 / self.width self.friction = 0.98 self.maxSpeed = self.width / 4.0 - self.maxReverseSpeed = -1 * self.maxSpeed / 2.0 + self.maxReverseSpeed = self.maxSpeed / 16.0 #used as a minimum for speed self.accelerationSpeed = self.width / 160.0 self.dead = False self.driftMomentum = 0 self.driftFriction = 0.87 self.lineCollisionPoints = [] self.collisionLineDistances = [] - self.vectorLength = 300 + self.vectorLength = 600 - self.carPic = pyglet.image.load('images/car.png') + self.carPic = pyglet.image.load('Car.png') self.carSprite = pyglet.sprite.Sprite(self.carPic, x=self.x, y=self.y) self.carSprite.update(rotation=0, scale_x=self.width / self.carSprite.width, scale_y=self.height / self.carSprite.height) @@ -349,6 +334,7 @@ def reset(self): g.active = True def show(self): + #print(self.x,self.y) # first calculate the center of the car in order to allow the # rotation of the car to be anchored around the center upVector = self.direction.rotate(90) @@ -375,6 +361,7 @@ def getPositionOnCarRelativeToCenter(self, right, up): return vec2(self.x, self.y) + ((rightVector * right) + (upVector * up)) def updateWithAction(self, actionNo): + #print("action number : " + str(actionNo)) self.turningLeft = False self.turningRight = False self.accelerating = False @@ -407,11 +394,13 @@ def updateWithAction(self, actionNo): for i in range(1): if not self.dead: self.lifespan+=1 - self.move() + self.updateControls() + self.move() if self.hitAWall(): self.dead = True + #print("dead at x: " + str(self.x) + " y : " + str(displayHeight - self.y) + "u lived for : " + str(self.lifespan) + " reward : " + str(self.score)) # return self.checkRewardGates() totalReward += self.reward @@ -520,9 +509,10 @@ def updateControls(self): self.acc = self.accelerationSpeed elif self.reversing: if self.vel > 0: - self.acc = -3 * self.accelerationSpeed + self.acc = -2 * self.accelerationSpeed else: - self.acc = -1 * self.accelerationSpeed + self.acc = 0 + self.vel = 0 """ checks every wall and if the car has hit a wall returns true @@ -563,8 +553,7 @@ def getState(self): self.setVisionVectors() normalizedVisionVectors = [1 - (max(1.0, line) / self.vectorLength) for line in self.collisionLineDistances] - normalizedForwardVelocity = max(0.0, self.vel / self.maxSpeed) - normalizedReverseVelocity = max(0.0, self.vel / self.maxReverseSpeed) + normalizedForwardVelocity = max(0, (self.vel-self.maxReverseSpeed) / (self.maxSpeed-self.maxReverseSpeed)) if self.driftMomentum > 0: normalizedPosDrift = self.driftMomentum / 5 normalizedNegDrift = 0 @@ -578,27 +567,15 @@ def getState(self): normalizedAngleOfNextGate /= 180 - normalizedState = [*normalizedVisionVectors, normalizedForwardVelocity, normalizedReverseVelocity, + normalizedState = [*normalizedVisionVectors, normalizedForwardVelocity, normalizedPosDrift, normalizedNegDrift, normalizedAngleOfNextGate] return np.array(normalizedState) def setVisionVectors(self): - h = self.height - 4 - w = self.width self.collisionLineDistances = [] self.lineCollisionPoints = [] - self.setVisionVector(w / 2, 0, 0) - self.setVisionVector(w / 2, -h / 2, -180 / 16) - self.setVisionVector(w / 2, -h / 2, -180 / 4) - self.setVisionVector(w / 2, -h / 2, -4 * 180 / 8) - - self.setVisionVector(w / 2, h / 2, 180 / 16) - self.setVisionVector(w / 2, h / 2, 180 / 4) - self.setVisionVector(w / 2, h / 2, 4 * 180 / 8) - - self.setVisionVector(-w / 2, -h / 2, -6 * 180 / 8) - self.setVisionVector(-w / 2, h / 2, 6 * 180 / 8) - self.setVisionVector(-w / 2, 0, 180) + for i in self.angles: + self.setVisionVector(0, 0, i) """ calculates and stores the distance to the nearest wall given a vector From 85f1d2c38d833c4cd1a070d4debd359dba9872a9 Mon Sep 17 00:00:00 2001 From: LjAquinox <125894602+LjAquinox@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:15:50 +0100 Subject: [PATCH 4/9] Update Globals.py --- Globals.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Globals.py b/Globals.py index 79b9eca..1675c8f 100644 --- a/Globals.py +++ b/Globals.py @@ -1,2 +1,3 @@ displayWidth = 1800 displayHeight = 1000 +frameRate = 60 From 90ab1d8776c9201948e984655f7da558d20e0596 Mon Sep 17 00:00:00 2001 From: LjAquinox <125894602+LjAquinox@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:16:16 +0100 Subject: [PATCH 5/9] Update Drawer.py --- Drawer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Drawer.py b/Drawer.py index 99466b6..536e16b 100644 --- a/Drawer.py +++ b/Drawer.py @@ -5,7 +5,7 @@ class Drawer: def __init__(self): - self.color = [0, 0, 0] + self.color = [100, 0, 0] self.lineThickness = 1 def setLineThinkness(self, thinkness): @@ -42,4 +42,4 @@ def circle(self, x, y, radius): for i in range(iterations + 1): glVertex2f(x + dx, y + dy) dx, dy = (dx * c - dy * s), (dy * c + dx * s) - glEnd() \ No newline at end of file + glEnd() From db2b6b0ebebf692051ec15ffb1eb3b1b0ecae9ba Mon Sep 17 00:00:00 2001 From: LjAquinox <125894602+LjAquinox@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:16:59 +0100 Subject: [PATCH 6/9] Update main.py --- main.py | 213 +++++++++++++++++--------------------------------------- 1 file changed, 65 insertions(+), 148 deletions(-) diff --git a/main.py b/main.py index 4f060e6..443a0e2 100644 --- a/main.py +++ b/main.py @@ -1,27 +1,19 @@ -import pyglet +from pyglet.window import key from pyglet.gl import * +import pyglet +from Global import * import pygame -import math -from pyglet.window import key -from Drawer import Drawer -# from PygameAdditionalMethods import * -from ShapeObjects import Line -import tensorflow as tf # Deep Learning library -import numpy as np # Handle matrices -from collections import deque +from Games import Game import random import os -from Globals import displayHeight, displayWidth -from Game import Game +import numpy as np +from collections import deque +import tensorflow as tf -frameRate = 30.0 +tf.compat.v1.disable_eager_execution() vec2 = pygame.math.Vector2 -""" -a line which the car object cannot touch -""" - class QLearning: def __init__(self, game): @@ -31,7 +23,7 @@ def __init__(self, game): self.stateSize = [game.state_size] self.actionSize = game.no_of_actions - self.learningRate = 0.00025 + self.learningRate = 0.00030 #default 0.00025 self.possibleActions = np.identity(self.actionSize, dtype=int) self.totalTrainingEpisodes = 100000 @@ -51,10 +43,10 @@ def __init__(self, game): self.maxTau = 10000 self.tau = 0 - # reset the graph i guess, I don't know why therefore is already a graph happening but who cares - tf.reset_default_graph() + # reset the graph i guess, I don't know why there is already a graph happening but who cares + tf.compat.v1.reset_default_graph() - self.sess = tf.Session() + self.sess = tf.compat.v1.Session() self.DQNetwork = DQN(self.stateSize, self.actionSize, self.learningRate, name='DQNetwork') self.TargetNetwork = DQN(self.stateSize, self.actionSize, self.learningRate, name='TargetNetwork') @@ -68,15 +60,15 @@ def __init__(self, game): self.newEpisode = False self.stepNo = 0 self.episodeNo = 0 - self.saver = tf.train.Saver() + self.saver = tf.compat.v1.train.Saver() load = False - loadFromEpisodeNo = 6300 + loadFromEpisodeNo = 15800 if load: self.episodeNo = loadFromEpisodeNo - self.saver.restore(self.sess, "./allModels/model{}/models/model.ckpt".format(self.episodeNo)) + self.saver.restore(self.sess, "./allModels/modelMatin{}/models/model.ckpt".format(self.episodeNo)) else: - self.sess.run(tf.global_variables_initializer()) + self.sess.run(tf.compat.v1.global_variables_initializer()) # self.sess.graph.finalize() self.sess.run(self.update_target_graph()) @@ -86,10 +78,10 @@ def __init__(self, game): def update_target_graph(self): # Get the parameters of our DQNNetwork - from_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, "DQNetwork") + from_vars = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.TRAINABLE_VARIABLES, "DQNetwork") # Get the parameters of our Target_network - to_vars = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, "TargetNetwork") + to_vars = tf.compat.v1.get_collection(tf.compat.v1.GraphKeys.TRAINABLE_VARIABLES, "TargetNetwork") op_holder = [] @@ -107,13 +99,15 @@ def pretrain(self): # choice = random.randInt(self.actionSize) # action = self.possibleActions[choice] action = random.choice(self.possibleActions) + #print(action) actionNo = np.argmax(action) + #print(actionNo) # now we need to get next state reward = self.game.make_action(actionNo) nextState = self.game.get_state() self.newEpisode = False - if self.game.is_episode_finished(): + if self.game.is_episode_finished(): #if car is dead reward = -100 self.memoryBuffer.store((state, action, reward, nextState, True)) self.game.new_episode() @@ -121,6 +115,7 @@ def pretrain(self): self.newEpisode = True else: self.memoryBuffer.store((state, action, reward, nextState, False)) + self.game.render() state = nextState print("pretrainingDone") @@ -159,14 +154,16 @@ def train(self): reward = self.game.make_action(actionNo) nextState = self.game.get_state() - + #window.clear() + #self.game.render() if (reward > 0): - print("Hell YEAH, Reward {}".format(reward)) + #print("Hell YEAH, Reward {}".format(reward)) + pass # if car is dead then finish episode if self.game.is_episode_finished(): reward = -100 self.stepNo = self.maxSteps - print("DEAD!! Reward = -100") + #print("DEAD!! Reward = -100") # print("Episode {} Step {} Action {} reward {} epsilon {} experiences stored {}" # .format(self.episodeNo, self.stepNo, actionNo, reward, epsilon, self.trainingStepNo)) @@ -272,32 +269,32 @@ def __init__(self, stateSize, actionSize, learningRate, name): self.learningRate = learningRate self.name = name - with tf.variable_scope(self.name): + with tf.compat.v1.variable_scope(self.name): # the inputs describing the state - self.inputs_ = tf.placeholder(tf.float32, [None, *self.stateSize], name="inputs") + self.inputs_ = tf.compat.v1.placeholder(tf.float32, [None, *self.stateSize], name="inputs") # the one hotted action that we took # e.g. if we took the 3rd action action_ = [0,0,1,0,0,0,0] - self.actions_ = tf.placeholder(tf.float32, [None, self.actionSize], name="actions") + self.actions_ = tf.compat.v1.placeholder(tf.float32, [None, self.actionSize], name="actions") # the target = reward + the discounted maximum possible q value of hte next state - self.targetQ = tf.placeholder(tf.float32, [None], name="target") + self.targetQ = tf.compat.v1.placeholder(tf.float32, [None], name="target") - self.ISWeights_ = tf.placeholder(tf.float32, [None, 1], name='ISWeights') + self.ISWeights_ = tf.compat.v1.placeholder(tf.float32, [None, 1], name='ISWeights') - self.dense1 = tf.layers.dense(inputs=self.inputs_, + self.dense1 = tf.compat.v1.layers.dense(inputs=self.inputs_, units=16, activation=tf.nn.elu, - kernel_initializer=tf.contrib.layers.xavier_initializer(), + kernel_initializer=tf.compat.v1.keras.initializers.VarianceScaling(scale=1.0, mode="fan_avg", distribution="uniform"), name="dense1") - self.dense2 = tf.layers.dense(inputs=self.dense1, + self.dense2 = tf.compat.v1.layers.dense(inputs=self.dense1, units=16, activation=tf.nn.elu, - kernel_initializer=tf.contrib.layers.xavier_initializer(), + kernel_initializer=tf.compat.v1.keras.initializers.VarianceScaling(scale=1.0, mode="fan_avg", distribution="uniform"), name="dense2") - self.output = tf.layers.dense(inputs=self.dense2, + self.output = tf.compat.v1.layers.dense(inputs=self.dense2, units=self.actionSize, - kernel_initializer=tf.contrib.layers.xavier_initializer(), + kernel_initializer=tf.compat.v1.keras.initializers.VarianceScaling(scale=1.0, mode="fan_avg", distribution="uniform"), activation=None, name="outputs") @@ -311,7 +308,7 @@ def __init__(self, stateSize, actionSize, learningRate, name): self.loss = tf.reduce_mean(self.ISWeights_ * tf.square(self.targetQ - self.QValue)) # use adam optimiser (its good shit) - self.optimizer = tf.train.AdamOptimizer(self.learningRate).minimize(self.loss) + self.optimizer = tf.compat.v1.train.AdamOptimizer(self.learningRate).minimize(self.loss) class DDQN: @@ -321,53 +318,53 @@ def __init__(self, stateSize, actionSize, learningRate, name): self.learningRate = learningRate self.name = name - with tf.variable_scope(self.name): + with tf.compat.v1.variable_scope(self.name): # the inputs describing the state - self.inputs_ = tf.placeholder(tf.float32, [None, *self.stateSize], name="inputs") + self.inputs_ = tf.compat.v1.placeholder(tf.float32, [None, *self.stateSize], name="inputs") # the one hotted action that we took # e.g. if we took the 3rd action action_ = [0,0,1,0,0,0,0] - self.actions_ = tf.placeholder(tf.float32, [None, self.actionSize], name="actions") + self.actions_ = tf.compat.v1.placeholder(tf.float32, [None, self.actionSize], name="actions") # the target = reward + the discounted maximum possible q value of hte next state - self.targetQ = tf.placeholder(tf.float32, [None], name="target") + self.targetQ = tf.compat.v1.placeholder(tf.float32, [None], name="target") - self.ISWeights_ = tf.placeholder(tf.float32, [None, 1], name='ISWeights') + self.ISWeights_ = tf.compat.v1.placeholder(tf.float32, [None, 1], name='ISWeights') - self.dense1 = tf.layers.dense(inputs=self.inputs_, + self.dense1 = tf.compat.v1.layers.dense(inputs=self.inputs_, units=16, activation=tf.nn.elu, - kernel_initializer=tf.contrib.layers.xavier_initializer(), + kernel_initializer=tf.compat.v1.keras.initializers.VarianceScaling(scale=1.0, mode="fan_avg", distribution="uniform"), name="dense1") ## Here we separate into two streams # The one that calculate V(s) which is the value of the input state # in other words how good this state is - self.valueLayer = tf.layers.dense(inputs=self.dense1, + self.valueLayer = tf.compat.v1.layers.dense(inputs=self.dense1, units=16, activation=tf.nn.elu, - kernel_initializer=tf.contrib.layers.xavier_initializer(), + kernel_initializer=tf.compat.v1.keras.initializers.VarianceScaling(scale=1.0, mode="fan_avg", distribution="uniform"), name="valueLayer") - self.value = tf.layers.dense(inputs=self.valueLayer, + self.value = tf.compat.v1.layers.dense(inputs=self.valueLayer, units=1, activation=None, - kernel_initializer=tf.contrib.layers.xavier_initializer(), + kernel_initializer=tf.compat.v1.keras.initializers.VarianceScaling(scale=1.0, mode="fan_avg", distribution="uniform"), name="value") # The one that calculate A(s,a) # which is the advantage of taking each action in this given state - self.advantageLayer = tf.layers.dense(inputs=self.dense1, + self.advantageLayer = tf.compat.v1.layers.dense(inputs=self.dense1, units=16, activation=tf.nn.elu, - kernel_initializer=tf.contrib.layers.xavier_initializer(), + kernel_initializer=tf.compat.v1.keras.initializers.VarianceScaling(scale=1.0, mode="fan_avg", distribution="uniform"), name="advantageLayer") - self.advantage = tf.layers.dense(inputs=self.advantageLayer, + self.advantage = tf.compat.v1.layers.dense(inputs=self.advantageLayer, units=self.actionSize, activation=None, - kernel_initializer=tf.contrib.layers.xavier_initializer(), + kernel_initializer=tf.compat.v1.keras.initializers.VarianceScaling(scale=1.0, mode="fan_avg", distribution="uniform"), name="advantages") # Aggregating layer @@ -386,7 +383,7 @@ def __init__(self, stateSize, actionSize, learningRate, name): self.loss = tf.reduce_mean(self.ISWeights_ * tf.square(self.targetQ - self.QValue)) # use adam optimiser (its good shit) - self.optimizer = tf.train.AdamOptimizer(self.learningRate).minimize(self.loss) + self.optimizer = tf.compat.v1.train.AdamOptimizer(self.learningRate).minimize(self.loss) class PrioritisedMemory: @@ -529,132 +526,52 @@ def total_priority(self): """ - a class inheriting from the pyglet window class which controls the game window and acts as the main class of the program """ - class MyWindow(pyglet.window.Window): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.set_minimum_size(400, 300) # set background color - backgroundColor = [0, 0, 0, 255] - backgroundColor = [i / 255 for i in backgroundColor] + backgroundColor = [0,0,0,1] glClearColor(*backgroundColor) - # load background image + self.game = Game() self.ai = QLearning(self.game) - """ - called when a key is hit - """ + self.firstClick = True def on_key_press(self, symbol, modifiers): pass - # if symbol == key.RIGHT: - # self.car.turningRight = True - # - # if symbol == key.LEFT: - # self.car.turningLeft = True - # - # if symbol == key.UP: - # self.car.accelerating = True - # - # if symbol == key.DOWN: - # self.car.reversing = True - - """ - called when a key is released - """ def on_close(self): self.ai.sess.close() + pass def on_key_release(self, symbol, modifiers): - pass - # if symbol == key.RIGHT: - # self.car.turningRight = False - # - # if symbol == key.LEFT: - # self.car.turningLeft = False - # - # if symbol == key.UP: - # self.car.accelerating = False - # - # if symbol == key.DOWN: - # self.car.reversing = False - # - # if symbol == key.SPACE: - # self.ai.training = not self.ai.training + if symbol == key.SPACE: + self.ai.training = not self.ai.training def on_mouse_press(self, x, y, button, modifiers): - # # print(x,y) - # if self.firstClick: - # self.clickPos = [x, y] - # else: - # # print("self.walls.append(Wall({}, {}, {}, {}))".format(self.clickPos[0], - # # displayHeight - self.clickPos[1], - # # x, displayHeight - y)) - # - # # self.gates.append(RewardGate(self.clickPos[0], self.clickPos[1], x, y)) - # - # self.firstClick = not self.firstClick pass - """ - called every frame - """ - def on_draw(self): + window.set_size(width=displayWidth, height=displayHeight) + self.clear() self.game.render() - # - # glPushMatrix() - # - # glTranslatef(-1, -1, 0) - # glScalef(1 / (displayWidth / 2), 1 / (displayHeight / 2), 1) - # - # self.clear() - # self.trackSprite.draw() - # self.car.show() - # - # for w in self.walls: - # w.draw() - # # for g in self.gates: - # # g.draw() - # vision = self.car.getState() - # - # for i in range(len(vision)): - # - # label = pyglet.text.Label("{}: {}".format(i,vision[i]), - # font_name='Times New Roman', - # font_size=24, - # x=10, y=50*i+250, - # anchor_x='left', anchor_y='center') - # label.draw() - # glPopMatrix() - - """ - called when window resized - """ - def on_resize(self, width, height): - glViewport(0, 0, width, height) - - """ - called every frame - """ def update(self, dt): for i in range(5): - if self.ai.training: self.ai.train() else: self.ai.test() return - # self.car.update() + pass + if __name__ == "__main__": From c22b0434798a4391434adc8376765a99205950a7 Mon Sep 17 00:00:00 2001 From: LjAquinox <125894602+LjAquinox@users.noreply.github.com> Date: Mon, 20 Feb 2023 16:17:56 +0100 Subject: [PATCH 7/9] Add files via upload --- Car.png | Bin 0 -> 1118 bytes Track.png | Bin 0 -> 48601 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 Car.png create mode 100644 Track.png diff --git a/Car.png b/Car.png new file mode 100644 index 0000000000000000000000000000000000000000..64a1be7bc071692c62fb30858218ea9eaac22539 GIT binary patch literal 1118 zcmeAS@N?(olHy`uVBq!ia0vp^8bB<Nn{1`ISV`@iy0XBj({-ZRBb+Kpdee4x4R3&e-K=-cll(Hva$llbReAq#6}F+ z$CsD@8LB0&5hW>!C8<`)MX5lF!N|bSK-a)R*T^Wu(89{dz{=1}*TBTez`!%r_7;kU z-29Zxv`X9>EG$~nfEpx0HU#IVm6RtIr81P4m+NKbWfvzW7NqLs7p2dBXCnnvBmq(s zl39|I$`F*AqTrlZq@b%1l3$<@mYG`ac-QVL0|T?7r;B5VhxgeqUyqPLk$?AQFMoVy zh4H^0udK=T#@%X)QL0-CRo^Z0)$0x0>Cwfa$P=}#BWvOW0gnk!v@}wpTz8u(DQYZP zdd_E7>reK@#^>ViSMGcMVoA`$6Ey`j->aU_{XO^nyyAPVR_;6QU!PvLe$}xz{}-N+ zf8(e$^T_j=l6xfOj$Jy!@`#%|K$JDA(o!ciwV--$!S$;TONxz8X=?>IZWa@B3Jv89 zGe3Cbibzau?u8eNcWo*v*LAJ7G-!CQtgLb4p4*-k94l`{m3;ZOXz^azS<7C&pM8Pz z+xIUEstN?2CmV`%3MU&%bLqNmdaW{9;L@M9f8yqCQuDYF6w`e;DvSXXniN@agZ~XmB*L=b1ODs_t66^lRxAi@CdJ&-QOUGD+plty_nt zUJZ@uaVx!F%*tvp)gz&?%hTbgN}jm)+Y^cV^nb;!TkozRa_RXg-G24El02^_=Wt9+ z^?JADalBY}USgie;wHg$k(sG?j<|k4^0=VL_@=`)9ue`!y3Ch6D)jZ%ta)(j)+{@* zpzJT2%QqgZT_iKV*X6^U#`dUaK@OG$Hd}69nl7=Hv+(=LtfTB&UwhAUd|us}b$7wz zddG6-!}3p9oc_tN3htl8mcC>AgqUShjV?H`=`-C)yjLe9-TW!)@+GCc4tGk;%?>R( zZ{!g)bNT1b$KJdzkZ^2t+B(1Guyyh*4a?l^&8GbNoTvY=Ggn-{#B??BNkhV9zRf3} zGI1{J4745dU6x;*U(~Cj z8$3&rWAb{piDx#fQhzEeb@Qy#fpf;wrx)|~<{oR&?B61}@H}svFz*TUUcW~QP(pQdnQa0{&RAg(sU8+PVfKR!3SrDhQ5%O zZ7rHTf7j}te^#FUecEYX?DlydcYUpRU+T^AN}2g*S>n{`MNN-)ep-HcKci;q-*rb8 S_00rkCNP(Nl_sQ#}SSsggnYn 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 Date: Mon, 20 Feb 2023 16:18:30 +0100 Subject: [PATCH 8/9] Delete images directory --- images/car.png | Bin 4251 -> 0 bytes images/track.png | Bin 48601 -> 0 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 images/car.png delete mode 100644 images/track.png diff --git a/images/car.png b/images/car.png deleted file mode 100644 index 116a3a6cd5d92d0212ed8f69e778c55c29b8856a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4251 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 diff --git a/images/track.png b/images/track.png deleted file mode 100644 index 2f2279d5c35323be16969d6cf8c241990401acc1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48601 zcmeFZc|2BY_XfUAkp@B~nVL|^5J@tnLM5V7$rKeyLgYz?M>NP(Nl_sQ#}SSsggnYn zsSr{jQ!;H-`~Cg@`|o`}&*$x&)3fir?{%+rt!rIt?I&=Tq23%`5nhU- z=4{j7VnR{eJQT&z$uk50$$-jmP%wv3okz(v%i>=7G&FW6?@` z@jERV(F?tv7%87lG_(ocwYFkc_0*>I<5hRr***4t?=cHH;&B^qMXli>P-LRi+O(h)BohUMUs>LC#L8;9ya=)B=!FHH5lT52Kk?d zQ2&dC|A7PbKeYV+B`<7G=BB7S!eU|}fq{X-A|jV3k-F{PDmTJ0ME&6R&Q`_!}v6}P^ zXS{y)x%_Cztl+>?OIjh9Nk+A_v_#&y6P%o!EG!{0B}ttQm*;w6`7@Wn5YW^b85l^{ z7;AfJHTLb)#^|@)n*?zov%W4m{TUnOoop^wR#UUbVCT+~FJBJtSrt(e?kZpYuv+2l zZNHzV2Ksksd0g#yWVUACHg;U*bxxayYKW9Aw{dj*rpy}}5|R+J<)-Rlsgv(M?q>)H zRA_3>41RNTupxG;!FFICuISv$zJzj4eQUWwRYRjOtL(xxKItVt5+{8b0t+g1O9#)j zosp2P*tl3+tdOvWh8gJ}*eI(Lj*ha4Nl6wf^m`k&Fx2H`eO5`uc#jXv54H0Z!`D~` z$g0OmYzBsSi@LhHWNwxxOK)nD5#lwL5lS15O$qaF+qP@Gp-yvH6koqPGBbc;ou55- zu5OlD2)-W~8F@*z+{B1o?yvo_x*7ZPZxp^iQ`P@1BFKt2L^?Jo1Q%Ti#a{2ME`JD{ zY87mEW-wHTm*1V$u*Fe*@w|!|#X&xE@U0wCPz}|+t0HV199I2Z>i|PGzL=<`4DS?| zZcq5J*Ec%lLTQ*UNC-2(|8EKE%bobmewH2jljC`mK#xg@ov5c$zv{i$H z@5gXd&flK!ZlBeLiR8Ns0l~!~rMWEw-%jDH&578|6^`>dOQfWv?wO@xOB3BGj+AQC zw!@;r!a48K``pwzN}n5Xl?;l*Nmw^|0;tNQSM5KzM+9XT1SLze^>ZD#u{GB`)HT?%z=TK+mz<|q$Am@v?bd6QR`Szm&1AoQ-w$HAw8{G&V z)tpEcZPV4$i#JW#3ZudWYfe#}E>?oV_1}LkOo#E|{#5p6NlvDC_>qsecYe2b8rA${ zKJWRz_N?0XwC+TFRgT*p_1uT>6K0Sg58GXC#6*y?x=mJn@!Sf{$ zX*{r8TwFrJw?8XWBGt5KhA&K~y&igsCaRlh3az;opm&o>Iy z(in5`^70ayU!m!wEV4dd9L~7SGMSTk98qlJ>8XYXgg0)D+oi8DlWNwHJ2ieRsrr|s zh|j9!xLMv>JlvBjIB=_EljJQpLfiwkyn5t7_cn@JzpDHpj%lHqNuH~ukI>_?}&Zmpa7*DXRYrzUfuk*=b+n zkcDqrdDU&iQ>B~B>E)VFep~$b)Ayg;YyZWw>AmF8K20OIO6XcGnH$f+zaPdoG(K z550^x2n!1{if<_@D&p39!o3uJYADGmxt{mD4mpQZ(;$LW5#x6J6f-4x&nn~OJvLrm z)R9f?paSCJhimdYDDu601s)1@9Af! zY$My2&z~(tMMaB0e~xkHp;#gOJZzn_5=>SCzhG^o5+N=&s&ge=T31)s=J4U=M~@yg zEd1Q%X5;Ith1=SCc&z{V^QX7alx#Fr%PA41e_6@laPbleKM~o%{bJ zHejcB*O!l> z_}_;QA1;0W{=L&pEpQ<_<=lubJ4MR?Vj7RTxhVrQ+a@Q*s_d0*PMla{x^JKDsZ+aU zIjK%TJV3UN5GTd_yj$+3t&fl9*N%?(kB%{=&v4ree(LT;Sh*oGTt-z%Nf16tAd0ez z+q0^qp~3w1n>PqJV_A9mvL8Plo?XCB4N3a(Gmp1#--N*P<>27R$jK4r6IZ`9JUq-8 z`+36g#EFC$E{es4MZL28{QM%LqUMn;**%mE0vOaftb6?Uacg#yk037_<;m3MVMWZI zJ^PNh#427S3qe7_0g0&riHs*tM6{;HGvoExs3(#5(s^XVY8h2c%@~9>Re7EqvY}zA zeBrpj>eZ{sGdlxEm=&vTmM0lI+t@56Ir=U)oR;zP+W`Y3qp~oqN#Qp~l}L2Gg>Q}q znIsEknSCFMEmt=?kF0bjf!s;RD!7U~=+G;vYE+ zHgDcMU^O*tHSnBItGK({p?d>G1!(hJK@J%3m)TF}4vPA{D`S7~LV3$#t*KuXqitbY zCV-C@8h-qec7Tv$LU&?f7Qi^ngM*z_@!%T+6~2>7!$K6(9QSp-yGy3Hrl#hY&~$>O zs0~^F{KF4~@EIQ{gCCGxf4Qd3Q<((YA`l~^qY2(7T=Q**n>8MCa*_i~jJHWl@y}$V z1o|BmwPP1aNQ6a2MFISP6y#|r)wR~u3BE zZC<{7`NuZ6kT?MI;9eP_){>xw1KEWWYeGXqi6uG@vMg(%+A|fL8A3PXzG;Tba037IDfBqawc5Pu%@P0Mh?hR*WcOUWg+S>YY zwP4|pJH@nIzjm#FQH;8amJY={{$=knX8JC0jnk*!dvmqiA{qbHtL+aSJSc8yIn=$u zBEHsPYf9~tceO!wwk=1%Mcu-G{ra`v$|@`?D~mq$bNhTLxs~M)3B~#N@gx2?9U$YC zV45;NB_)My3)r08F%%7xh3K}uE9`Ac5tJ8M$VNH6!WjbOt-8)GrYOzj{OsnHot>Rs zwdX83FM@%%*_=A11QS0U|)Q2>Ka5+>~}9GVs7qS2bg}doS@; z4S4HlFw#N| zc26(06HDoX@vU1oe{gURdHg$<{pfb2?9YY162QQWyu90V6EpzMu?VGSrHWEx^Y-|`33?}?ea6$Lr)x}qL-H+KK z@gyx}euZxFpm@+_s?uV6f{FT>AIqOkO^y*ed*0CI`C@kCh;d4-4YG=qwDj%@znE;~ zKCFRf^6hfw$ZlD+Z}rgwKT9q#RMbpY%?EdN+7C|yN*zCboDP-ce*7;I!11b6xXRmI zyEjx;&z(Ehux~Yk!HD#nYn}(D-u~BU^4-zoWyWmGpo3;+m*2d3gJPDxS~OjU$_2Y5 z03W^4gVamIqMgU2N9I4M>&eK>4ELPt7Tt5{a%@mgs1_IV()I*_prD|Ea{0phF`z}K zICbv!MU=Nk-o72Q(WAGxx!D>|kooER=?AH)2)Qt%$dUO0$A7Lf9%u_g@q?U!bDH71 zkaEuJ{amn^*gZS@CW$7ac|3kk*C{It&6+g}k=awSYn1o)?c2ubb++#AwF5gVkE978 zZd;|>4kegu7?Ifn;sM$hE^XA>=R0+lKI4+upMFo3G&RMshH^#tn6$LPphKx z0PeeES!Su&_`;`loo7aNkH3q!{5g_?b>bFi82tRNx*pmjv;Fux3E~X&w>$^yGSJ}i zdkYRjFQg8I=Kpot_sELhAUW1iD zIhOrkqvW(DIX&Nlji`J7t_kvX-n4P!Mwqp2Vr=B5=REPdi?iy#SH~O4d&>TRU~qNy z{2eQN&DlFw0PGhmT-a77;R_>BcYA&b%2$T}UHi8x&q5U53|JKm5SV_=LFIOpql)}^ z-*ee=%`@^PM&8sFa=(1Y(-N=fa4(v-RPnDLIw*DIIXqEKR$@~~R($}O19|0-D zBA%v6)f)=758RVmEaa`l;aq^c!_6mN+*6nSpX+ynFt*m8EDRw?%j*^~jkLr=yuCHl zq76|a<+^=5yV9rouj)(*yS{|Zvyx2t+u$8qlcUQ{dw9?*nc+#4kl6DX14~y$030d_ zrMhRCUIrebeESU^Cg*;+_*paA1ycfZj5M{*@|A>FaBY=LC)Q{$znKgaa9aa6S%hN_ z#F^A6N#=qYt`&*54$mMAO9jLo~a zwDL`FQ=vqBn?3G1s z{_bl?3RThQLyFEXYS&J`g_CLW8TF$)$oLqY8_(%V?0)LbemE*|rFU*)U? zRR6x4GcN--be2@PBwf)W+&|WLa>MX!ikY^QpWOnra&e|zg-HeTc%8j6z%QquAR1;H zLR^aJ>FK$+xw*x4_;z;}4?eq>A{dtq1S~mOII)26SrDwWC0L{jVMbFH=J6`0Y~oQQ zRi&rvB~@=jt?t@)Jl$}-VgBVb!+>5BlsCZaN$?c{AqZyI-t<2V3$loziX!zcmb(G!7&J4xpBd4A^^hmO*L30$w6jrbR5@hH3F*AeTi<$SQ---*(=*qc1IXzEV||55F>KV8 zJK8+U_kmL43_j-?)Tybf2h$0DLZ|NoH`Td9y6jGGU!U*KGbmrra$%9G34tzC5GCwG znd&u;R(a-rD>`~U4;!-qHaL8_#76z>Pubzwmrx~FFWa&>M3=DtTmU4hMju5fAFu0O zNjgi-iMkW(^Lr)n;`^LBl2LkL;w2=3F8We~ht2n$(lj+Sl^g0kO;PVQXljPGwY7by zuOItz%CPsUDsl`SIw{OZ)6ixqm@<*HaYOnjkVuz-62k5|zO-{*pAUB{`@{2p1B@q6eP9^}Q>uSW@WMDg&*ES1oA z{rf46N!9@u z!yzPq?P~LpW6I1F1WHJFc|*NIS>5)z;4EmoKx&&B%3tlzGXq$uA}F{JSqU+lV%0Y2 zxE(pN;?JqxH3C4CrWa0%l$VzejQ<)GUc7jRMqv6LRO!~%tlKkjAmkBt%Ls|Cc@^@Z zp@Fy*C~PP{7e%8>Y18D-Y#`nlAa%{D(d}TDaHlIMW1Noz>TQX*Jo|URvMq3-IJgi} zQ5$L=LM$p;R>RxMe*g9)(6@~T4~i3TeCHR#v|5 z$xdmn#nNp^Y<3kfTW=gmPO6N5oSltuUje}Z-jN8yPyioPoTGZGe_|()C zhWnA*9?b-mLf<0`=#D5~uwa3SQOmVgV81h`&mx5Jw8h)pdirqV~W;vgXTScya4bMgfvBBPBIx33E^^YH{VLq zDxj$4EgMan&EOpS&CIrI_#brj@)9Lc(A${z?}0qg!7&37XbO9NmH_Z6AK`G^WopuU zxi^LS;&#Vr~6*lCM47pabXCx6*}`~IE8EoJd$ zt2Lag8){gmy6jjirysl!^2vSW(M(S($ zAAf{0=Va5#==*mGJam~Hs$-D3hr13w;9--kiS}Eaq)*P%Gu0rIrKWIaFw{&*Ch>d{e1|I6X z0)D8dr4{QrV_%KEa_jf{Y*=pN$U=yG$4|St2@w?!9FHXp07^J=U;*~HQ^S8XOdCpw zBu*PuG;Iy<R#k^|D)r7v_y~+%8o=Il0b7Nb`yl%PsGoeN(I`k6OitFV^k}`&b z>HE*8RM#mg&I8X%w)^r4XgfYsZA6?1m`urafgX^)(jxP7%XFu~;nF2^;$qa#T=UdT zBPqb=SNqKxB|PhD!t~CcKVPGib6+?9DoJGm>`VCPO?f8uAhBR~!zjy$pT>A%y384^ zOu|P6iCicohuo=ZBSgJG$%LR4g@AzPc~7KgK)_CvE>zAvLViX^8F-p&Dco#Kbx99$6=XwAXdGw6@d_+8!-|ErZ5J;1PqWL@^3Nb6$ubd*nBZ+CB1@>~6=troKn5+LbO&NHzzG(|d&CXl@{q_eG zWx=7Li7{7Ke?Ps^vcPjbN+a5V_w7kFC5)ieI&34!gOKSL`SH8{4ekH=MZVn?FCrkC zp*$ms3w`FRZ$T(Qd3M>~n>C+2_{@MSyZIzEA#vUD)O7;K8tu*NvpIWqBkj(Yk=k0T z;`IP1JBi3@1pcKf86YaMXi*?UyBZ}|kcQIooAM<{DxHVI#5FEYAChjUFc-<#0ivEi zz67>8J8eU`-S>1%?)2%?^y#NbmOmsM4+2?2%(Hb+%ZbUC*d4XAvnAqxusPgvfT)wS zEJlpZga15G!jHY+@)9>DB<552hB?Im-#z$QUPg44Qi~bI#1IT}BC^)-*Sjee} z$etuYz|4=W#rldXc)h3ra2W2aE%gb z9;*2qy6)sbNYQN+7epg2COl9IdVj^aE2^pj2%F9zQ_)QhatS-QGeL@ivm+)(akF4f zihapY1V9DcQC}mzk2Km)5==in8!Dv#6b9$z+yL#Q;77gn1>0B#973BC&afI<>Oi%T zRsy{Z7E&sXYishC_PK96x;LSTH6W%m6bNL8;%lT4pY8~3M%JN4UXmg(s}fumCNZWo z&kCA1H6DZ=v?AZq=<02YHY?G5F{VMg8mS3HDq^+5AllaK?qmbZunKtsD!E|}TUXb$ z!@*BooSpfRgrdj5476|NdGuKlIuFUJd*u)*D~V~JVCqLhZratm7kS55*%t!{+D1oJ zY3LAyvPJ4pG^$~#i$E5-B}Zf;+^Y>_yhkSb_&d}Z5VUYV=xONh&qR9>;Uz-SrjRED zDMJ!E7Mmpl#8E7_7Ws20+YOzEIFxQx_>)@~pkso5G%UcmO3>sr7*f4^bQRhBRFbTAn24z8(A$s4e|&v&n=#UB zL8pB^oHp=`C={3rs))Pju7=(k)eME!vj##F&5)`FA=8Zi?nx(%jgnY|i3e0ybS_yr z?wJEMlg@}Kj)jiD)&@}p}NobhuceYRS39CL-!9lQA11)p^c9y@fX$SpV!6;;y2gm<+fxg@G% z@Ykb<5AWGz*sJj-f`FSORV$L!rf-B^sSppRL0RPfH%E6viAc1?MU;|!l!3#E6FcK} zy>JtH%`{6rO5aD?826DBp8d*2v|3T>eQj;T=lnA;=3zCsD5a3;r5Otbj()a*UQw{B zme#)bDh2vh;bc$$uXc0ZhFW;oFd&VXn9K%I{EyL-aM!!{1Uz zbrpnbf)>iFvT^CoxwsiKeE%FEOqm;pDV~*|@2K`{%cq8hFH6b5e)FRFL0i|z%a`}r z1+pFe{_Wde)m@R~CJOULf6is>Qn|fXc7c+ZZ7cfjuGh(4`VF47{DVZAyk+SjHQu|X zDG8nhPHLcltm3<{Z0SSUHF9#dLKR({oScSGV@-QYvR(aS^SZqWCRb0?C$>=tV9&9i zYxIKPWepr$OPUpP%?+>W7sJWcii8aP{C;qF%)8ncw__$BIdJ30uY9CKh}8yVY`yts zM-11mUr#%ffe>uIS+@M$vuEdE^CUCU0;G*Z86?!({9}?VBRcnXD2G>~WqQxIZ?DN| zFatMl;$i3``=1|+F-yJSkacVNG;4C#%viW!f%5wGc4~9`xfd>6`03N9wdA0zm%>5C zi-wvfTq=kUDfm1-k+NSF?s|C>!?due4)$kA;BOd`c%EfHeyLacj}&VVw_=TsdTC;& zX+vndwSqm?-5Xz}fAVvF)$njumkhojNc@XRSQT*@W;$YPyV<{pHSL;9!BnB9%fwccKR!|I!jyR5<@TR*OJwI- zV!dB8*3PqE9BrKq+i}hmgK4k4s?HrvK+F+zsj>=&JT)eM)}Iuady#fs11=arq0W7$ zUZikcUERM%q#|Vo80Hb%w|2RGauaIabm4iN&&2RP3oEPJp>P9*bqEH3#Lm*Im+*}K z@t__QQ{tv!(q2GGSlLcJHgWRcIF{+7pO3pp_KJ_YoIQIM?Wk#hiNPkptl?_PBJ0Gs z8IQhyKOrV2M!ST9JMOSqATR(iiaWG?{i=)kfc?PTUuJ$5Za&zCw!XR5Z}yX-Az2|O7)Mi1Hdj%mf6mK zql79gX>-XXEtB`&YZ5f}5)1x+^SeY~b-$*&Lv+SF2h%UjE#n5l5+0Ab_ z7`n-OUb%5rfG4-^M@>S#4#>^jfLqvP&?$p0rm?j20!9kxO-F7M3ScQ+E^A9%#`pwO z{rU6pbZmuVHz@I59nHL>a4u?ia^ft+t6MZoW+DUGwT_I<57FIO*8d3U5kO9dDy1HH zc5yV`JwmYd`|0TeDt-3&#C%>Z?8RBtQEhJXO!%_nY^1$@9F1=>Z#DJpOWt#>O_i6E9VZi$k(E_)Rsw!=5qY5^;9P&tTt2=iaex?W%dG$7 zqO9F2>(_5Eg^#gp$!@I=EGXmk=E&10X(Acnim1_yy@{j2vV$$px&KX6%qbnXq`$MO zEJ@G(_rt>lEi5diciXT7r`huaUIxdAAtwFa*GGFgQyzIFhOI2fM{MIZa*J(iqk?e@ zb@BCLT>TF~vCs|dg1AS#p7P@2TGBsThNtjBbr)ubrWf}LKT(fBakaRa!olnlo_o=A z@M{RhnSmkL#Coz}NrAw~(B^Fhe1INS`M6IZqF=PkSV34>jJrR-$oeiuu_8k5;tqmL z`1*`*YbWFJU)$PjrxThM58zf*FTrFR4Y$@y2?+iumv_jnr#+ocw?>%3%{Q~&9Y)08O8(jU*58G z3Gmi13C}NOm{eT_A?2P^_HmjW^^#DuG2ol5dh=$E2g(PEm17{atj>KtjhRv=@l};5 zT{egzKCRi2$Bcr{^@kZ68XD-4?*>0SaqyRyWm?Y<3o%B3zduKT@04YH)wFTdDe^4) z-A3axGnJ|_s7xu?f{US`|JPKJ_HaAoT-vjn=fORsbvKRX`V>|S+{4!yDn-`2Lfas6 zz3do_3gs|(1W3b#Nbhy-{`-#gH%`>YP81wD5G!VGX&Y08>)vF zOY}zq-vBHLxOvl`cv^YrzzyE>=Wt}p1r*_OtH40O$UoKBgC?^)8e*)6$Be)k8yXrK zn~V+)j0j^xA018cc~|!ApO21z<5Ok-SN(SpLmsNaUz(#oK-F^S6dgO(qW>R~}d=bQivl&;78nRqI9Opj_)}ZPkItIqOgi z5g?tQ&AI+lLIK2kf~^%~%3PpL2REJJG)U2nkIO#Ugma7vWq*AgC62S|_{F9Dk2#A6 z150esh6Q$lS*mT^SPhDP>y1>|8ji?SWXaZ94bs?ks%bf$YYgtfr?9Y$P)^%o#!~zZ zyco=Wofuc78N8{*c{E5u%3sUF1{PRJeN9;*c3fmM{asrPP;5@n{{(uJ>{w z@KI1q(rb!=l&u=CLM_S5U*BND=0vqIGtln94It#Rmi|VolUfZNsvqH#jmG!=i<&bZ zIx7fAhH_IQrMqV`bl~&O9;9mV+4JB2aPs6yBTU~pc5?P2gc>85g;P-Q6u)b7!0uo= z_*{72VsU`s>G`T}Hzi1%Z0X|@r?w6CnVXwOhGIdAFH?309pkGA=y4bn^zU`+ zyVDF!Me+8d))V?>TSp7i&eh4Syr72G`5Zlpb zpg72^?p|JwYP|j59P3T7IXve>Osyr@&#;dRB$MjD!ycJ7uSsr6wZjFNjkVbdu)i=;%NzU0F-ZXc3Q5hU|_m zH#2;|xyXv@>R)(FC|zV9BcxkFUL7Rgk3jYICutz1T{5iKP#fUgYHDitgp2neM61Wg z^U)*+l7n7E8I~2JFaQ>%^#F0ZnwzzyrKM@0V2hkbygW{F{5~n)@RUqDz#pOvd2>4G zXCf@|bv(r3aH+&=0Nw~c3`O$J@K3a5v%RXn{JQ?~q1KXFAj?>pIjl?>g_1K83XlN# z`;{j(izlI> z6ZhKzw$rl!p~|9jcj|E{u#rU0TG$+hA`NvsUDU8R8a{jA!jBYpa1q{JuvqfJQKhzT zWTzf`=~5xymABuliZ)bQ!6PgbqC5PfeFs#99{4D#vj^pDqf#KHGl5)Dq0KWz<-zF{ z&L9=MYJUjBkNa3I^1BG+hB=8*Rc_}?9Qr%p`Jp432ac>=^roT$C4937NSb+FM4PJG zh7HCAmUbp$tc3%Qxz*VK#Wa9`+fiDm_hF3o!80EppHQ2t-~w>8J8zxWh;tTBu~+uo z$EzD}T^8hkW!)q)^+ z;ZbHqM|hhmLD3fg3(x-B zTrLIu&Hhks=mxseMnFC9v=J9D<&eC zEgM58e+j#7DQAmbqti`!!ags+C;ki0)om4o!TB2_&V4^!@3y@8BjN>83yR41a+DVo zfZE@`pP&=HC%w+y+4*jfwKbknT6+^u@FJ^+TCGl)C}DQ2*V2*)y?k>NRY>-vI@jfo01kuQFdyulkNK~2~gZPk8LT|{>%322* z-nMk<8$FtNwE@S!sD@6*X1l^-8BV{x9SJ68=H@lz$YiLa1~f~k{rE8o9u=3)qhc0@l?``jO&$1t`e3cY-lU`?0Q)FAua0%F438FaZuiJ^F>f%t z{NeTTc2sots{TTC@?mIfqN11oK0jx#JU@@Ui;FRYo*PF{`cPVUC#fqb*2g5T@%`Jk za|O>c%RmKew`>}nS4MQuapC`jUz+&txVj#1l_ej#++bKK)Jdo{A)V2+CdK$?zwGJt z#`?xQ3=}O2y2#Nk`7%g+*<9>w5#>kl`8AP$Z{D;T{1$Cusbyl zX0q!+Axm@fzsyoigxD_(Nbv?6OY$p-6$wHgqo}87b{T?V?2>sw3xeFovdoBsg3tr5 z08EM7mtL*J$S9G~VDiIAjVpQC_)xW%UW)dXfFq$Sm&4c+RK@U@&b~rYrkNl2WSQ+v zsa*?#tp;5TIyFK^?V%*=QKAp#1!$x+>t9H(m+oK*8nDEL6&C<7p{x^RhP zzZ+;TdgNbJ=PuT#UO)Kt>&!&Eyf5CUAZiUl4d8MVq9ttC)~Zki zc>KF|RgfD?@MXIz!1d4?HfnO&RsGI%--(pBZ{K1{Db+B^mLy8@C>gRr8+oR{si7P# zrKL5T^w+OnVFmGE8-9ja#(o2g=}aebrZlh}A5f@dvsBj(#r`d1jX2Tq0E}BuEZKpv z3H0b(MKAURMB0O9qQ-qf5;a%kb`%|m1^g|s$(9!g&URN~^tW$ApERa*>9h6#R7(Sv zZPGkMKg_v*Dd+~QQjQ+K2I35)7k(& zOfO@Q`G0F`e?-hNJ3BoBrzmzY%Td4>&>3JfKx&D!e|!R&72t}X^XK;< zbJ$`rl+RMaj0uLZl+oS*!Jjs5n5(Df-_g-xXa^!>lW$aXppo%fjg3-|!_3t1usdY1 zH*a>LO$|>c`hM_CtMTcq7XJ33ESj508}`Z$kQHS>r2sQR{w%>+7oU3$w4R@a4h1Fn z^CTuP_u*u~z3ED2W~4B>^hMUgEDr~kP;SLa7`2DqNwoYLcA|sL~$&TPa)sy zVQ$zo%dFjgP4lAZrqf@f(Zx=U%0nX|J|@HqMgAQ)PnSNEG~R5VA;UgzsU*yl*$1tO zrp}f$XsLg&SAKF^_`Wb_jGo-h>Q}FLdk~Z^h+wUWp>QbPnqH$Uj}^^@1j(%HRW`nz z^52NlW8MT4LKZ|c;JozESV^h9(yneK<3P6Mg%08$6!o^R%jc3>jL1e{t9w&OH17JDU#lGf=Si0XY3xe z>?|Q5-FB&P;^*KDU8+n{s|hP?8lA}nLa4BUpXV>oBy+j)zK*@z;n?bv%^Pa`CX9lfGlOF&*?MFR%Q!)X^CL%^9R{CNz~HS7pBkxO=la<4RDV zo+3{qYS665j}wX3%uBJT@`uPJq-#vHSW?z3Z+=O9iN)C>{l}LS+TqMV0y?}{@n}V& z0tCUki5;@gQ$BWVTf6H=?2yUbU?}M1=!oj@0~%M`=yk-_$HJ#5X418nhY?yRsZXL} zx6dxI#xxJjs%leChFpKU%yJ}h1xh&IZ*Tik-;UZ^d)d$ z@CB}y_ z0!V~(_o%sw-2CTHFk{^OIk|c8Zn!M^J5YSUgiA>!w49bG#&$@B0}@ChMaWuKW(M0y zAhYMM9|?FR3;Cb9^5K&we;X#npU%UL`|bbS*omYE+*n=q_M5*!1QrBAv*0R4RCM^_S zD2_4LFAv{V;PuOdATe18Z;;c*!<&l#EVvv<%H(TJ6%z4HZE*+?C%_}!hJBv6%3#jr zO{}wy_3!bv94b(v*%E(E(e&&eLvvW!mBkf{RVL45@wr<>ol zE(d47?{Qao&L(bi;07Ao5EkfW0HxrMR`&1TY9sS(Y^ofoFuG)nbib;<1>6sBdT%u| z2(e7}Sg)wB$ivt%UEdJiDMR^G0)VU)D)Dhv%^$1C-d8*O;^sLQpUY34^wx}(Er;TR z#t%M@A1>Mj=qw?YE1dXw8%erg;I-3ERd^9R1L1_HCf{*)hEtS*F5nRWgrgZDT_6D~H4xMX|DDur zt9yn4J}HpTU`c<_sz*sf-`p}s{~TjQvrHP#2$ zL$=0;sAEAT`wO3kA4K?~wxHYUEIyP&;zbW-LzUrzO85sT2}nnwZqN60oz8>CCgjh& zh@6R;3~((RmqBoq{ZQ?@zCx|)ua8yLH8cvw?C`@K1B~%r!4TaIA`-S}Dv-zXo#?Uh z(Cc8g)XQX3XU_|pytEanU}Dh!^-{+tjlGS<%{gHj917wv4aUvzE--8nvsQF&-}ZlC zJZP|>=Xt)TH_p3cgpFZV?E$(=+a${G?3ev{;Mfa(O1$SMFRJ;Y2&>Jbsod(|wNQLa z5kF`8Vt$Z% z)ewU0Vw)D8Q%{O%!n+@Nji&e%Sl(O?^0-1EFz?+J+t7h!aO+Q>ZcR2#jroeM~5 zkzjBxcms!c#Di8NJ+<)o1Vo--3?qbq>8zaLH}i^t6v;TG8O+-+^Sq^>kE^*)%e-Rk zA|ESZvl|Hv^)#qjDE3eTR1Y*C0u@J;)H>8+Gq>&6GxeHt)*!!jL4HRTf`RB569$ql z27^Td7pznqV<`Yll)8umzPtj=25arxx36Jp{dwK^Dm%SvGuYWMhm-g0O30kilc@Yb z3EKZMW{99y#6v^+wsl%s>Dv?TO^g4g*(J?IMQmkiY=V*^Ar9W&7IYGC<^zc*T90Nn zS95Lc{M$xH&0#yxKD3@!c#m70J}4|DFw^i0r59qew9<(RV~GX$7B4&u4Qfrqc2Y=P z)dz_Su7$Z9izR~;Scjc0_F zi6$#mv|poQl&XV9UyE~lO0-u_-e+2bK(cNlZ@@luF~{0(JexgS8afB)$IHX-FnDwY zeoC*6#3d8+%vp6$eAO01*nq-CIllTQJ=_L<~Mtb7}+R`c&p4r(c3#2Gv|#$0=iuN z{kx!@17M&QKuQO$(9F9f@Z;lDOt_J$1Qju^QI4k}Y+)cD5`cL!I;4m|IW|3{<8Fb0 zMWjj0xIQsSHH{bMlnQR@tLyIm8$Qt`6DfcWEanry_wzS?9{BO&njMEaey0dJ1o~bM zXxszT(q!+%(jD$)tuv8!z$56P5_Yc(v?-zFH?KfgB4|R}`Zc(AcXW4ehi(8VOLmf7 zP>LRU3)#7vaRms9avZ%OceUue!iO6cRX6|qP>)oO3lXaZ8DoFs^}JJHD` z(i(^~bZ0PSJCMfdHh=*p`}e7f?}Ol~a{Gw4D|>2MTKCjs-_&RUw-E{k@k`taFpm>x zD?s3*;|&{r2;5!x$q#t6pzMEnG-E$T6;n-Z&&^WNn#mBXl6@Xti zsBUe&hxZN#As&V|!DONhF|ibEfgsB=qlW=yDDfpX%a6IcM{qfK)j}u% ztwy2(l}LOBP@Ik!R1&CpAQqvb`DO9VCA5w7#Axt+t?ILz;w$!F0KSw3Py?P!3&f|R z4fUWBz}#p}vX-HsBL(>{ug`8mj6Kkie*RPuL?H{k)T8Xmr^nv=793r>DD>=4%=!<3 zlTHlAPT_SY%o!*JU*KR!O7Rp1%j1Xp)GejLQJ$Qe4*CypM}*_$P1d~d?bOuNm87O& z=R@I81^r=~5FUNytXhMj5qVQo$B6FMlNIAw*MF&PFPO{D&?-OBoPvJDC#HNe< z>zB4H^&IYwplMS7-T3%rCr+Hm_%MgDE4v;lcp8!o2n0I8*w2ABWv~IF@q+n7p;fx9 zHNoN-=tp53)LgU3XU;^Wtmx;4PKbfp%IT)Jk2akKG5&l5;EHDE zNX+7E(WYR;#~rmwbYa3&sja=qkX`!EdY-sL@UOAf6NWm!uw1$cwRyoWT?_+;1{~is98V!Udg_D8=3hj*T0pAZt0M4wX{Kt?Dq_dG57XGK(%kZqm>YdknP(t4cK^~3(V)L0 zw?lPun{{5t%*txt(yJ&n#ovJfV}1@th;9L63yl=JR7KGoggXBqJhTdfyreWbPRgls zi_p7Jr>gy}%FcYDm*SXU+^5ctAHlnglOM-}wSM6($CxyYF`2B_AjJFKe@oikm1q4Fst=`*dhO+Xh{QR!XYR(-@3b%)>3ZV` z=Gf67IxlGLjND*vYwK@EG4*;z#{OtCq*a9BNxfV87=6L72_AD&UuibC83}-miWCF; z00F!e8;dbChJc-&-6u?Plg1+EfS@GeOgo}3Y=o+S$xD3543#Lg`#v}fUJE5-<516} z|50a=6Akkn-%Ml@A%j0RC=89~ps4Dl(S}5%$=DBkY~J=C@l*-FJm_%b$axQyh7DR; zb)}^j=r$UgLJ9sHM!}00^N*c5bAXV|`+?I<7c$l?6KV2Dl*K`W6omqr8H(S5-Xt)H zhQ!OX5UG(`0b;=bq_h&w7J(r@NfZv~R!#pdei$N ztGj!G@8n=CT2j}0o;;BD(Tm$vIXU^1ZbcPck87vInV4xLO>%?F(5cR!=Q^Eg;~*v> zfr$;*Y#{|(1Sa0>D-kowc@F(V9#Bc#00>V0>#bG~yszHf5$lV=l-(DwVT$)zdfT`& z#jG=)ZSx86pIPoAx@OIq0m~WN4Cb&$&W1<)M$0IXCm-VC&13t`wj4 zrcw9&OYBtsWgGr#{76cp*oy6nJg;Mr-@vcvZdCI;vea^9{i)w|g+t^Q_A%;`*v5E| z78h!TbtD>!uHXH$J5_Wro7V~XM;?VPmSuKio^YfwTo=93!k{aHgdC zg{ydJLWQFbAUv(n={Ux*@tY)x@r)bj%B!k2duWuNyTOqo{i^6>TL%ZFfZF94qyuoV zuE)X;OjGDKJvVa+FaQdev+Vf?O@xF5F9~X8ibxBF+|lLZVuUl}IuV zLWIFyF=Un)&*Fe>(Dp=#5&0F8zbR4z-fXS4n)MDkyc{o!rhwLIF)s zqadnc4b*H9;Xu1>@u~-51Ry=ANHLKhY$|sXT@5lTtLMjUZfV&Kem*@;$_mfT$*F{T zELx^0RuT>_gU&4EJHq+nyQtp4bN%%Mq>R>p=t5Ve38M8k%s!x4#KXhGAa5srak2C8 z*atp2JzdBOuLpujg}1zIOy9qMe#Qb!EF`u@Hx%fLWCZ%4 zQ{_gD6bCe94-B$V!MD*v2l3YpJ z_`H;Q9;N{?q#qMxg|`W_3JcSb+~CuG`wy}82A+R;5=0c5CO`o^o>nSZ`%z6`$QUn0 zK!RpPLq9;+g_`&;Co`eIXw+Lc7p5_u5n>IQjEZN3!jUX3-{akMs4!Ce^l4f*;nk-P zA90}9Aw7_^I4y3@ho$8)^o1;e&4|X&*@j;c%YbHIQDA~!bZF{3j$#rD^;#Su+GEI3 zXrRE{3Xn`S6gQ1}qt(s4i2ItEnYE4z8$^Q=LmLCHxx9&rok^#dI=cPd&x8htHv@XU zd?|*l`>z8ip=Qw#qE7fL`5{o^nlh1+f6g)%(&IAdz`HPY@$Q{K_3y}4&?-QH3FCvQ z!7g#(c@G)=YoraDkx`^8K#g8TVbqD23^rE(wBirZMO4CNVSXtdVhLF@9|^5x|4(~w z{+Dz5_VK@^lx3(XBa})c%uq3;iON!zlr%H8C_-i!5|OQVEM3WNsI1ZUeIDDnzK++SlBwWZuym)T`6;xK*cR5kI zip>A}v%014`QIuG^77w*w<{VW=~|^E;nMV@Xdp2=&73t@6}fNPCK@{gWKqKw?~x2m#OQF6Dda=o|@*2#b;z=`5(y^%vHkT|4&MwLIOjojezGv z{7sZQXIMGDF)V*0(MkDpk05PwXMI$h)RHN=rPY}(jg<9eLdTWh#ED{2iZHumR6|312=FIz-;_NABSK65TH4+%beNLR@N!-8jdxar&GRE)S8cq!u6S9a0Q)L(nh;>@77N-)hyB1)=e)3_}@ zbx*6=%9D2Npu=dZ#Fw@ek>sW2T1X*@iuG`SmqbydQn<-i(8)+P6c|J!9*5SpqVKFU zuJp0462FHUGQ3Q~v~JfuN*s4q{cu|g&GEc2Xd_X#pdJ97sAH;Ju; z-+VDKik#C$v=~TpEQVUQ)>EecNwAK7LCeUUFwn&(KXUY_M3AKawEIe>@1o7^Ki&fS zM0-$ivbg?Y0W!Nvk}rzQU@B=e7$>!h-V=AVJAoe#Z|U|firrN7n8?@7Bd_&Hc@Ybl zEm`jp(6Cmj@5-!$`T!>C?8- zPF@8O&HmIT1*?TbY0{(xeLff=R6<%!i%qDB!(cZ9>x%m!y#X%R2&LfCcejG1(cEKs zNiiI#xYhOsHK`fXG`9x^vjBeJWSa+#Pfh9>4ux=7<^3VgY?YIbUz~}|n0!-mmP%OSU5W&KM6Id8cFjoftN6-t##A<%;JRx7=~Mw;ONe;Ucr_7ye&kydwV$dMoV%3@Axsih@r_(MjlN@MiZI$({uEW~X0fxdz zL1%ngtWM6Hu zBZN>OJte+TxbKffVYzRfpqw^$R{|eFR$O_mjlbip&Iu+_c3JlhG_EaPb)$13Kp`Q$ z8|(W?d@mv5n3MIvCN#$L_@dX-VFF(K1M-_vk@xyZ&A3J}TV~!kaBlyE4Cl`WJ%aFA z7{ypZ_a;{PTzV24t=4A4k1ABPkjuVZHSb0PzX(C8?125r#c#XO-1p-9m^dMNf#eB5 z2#ZZLUTV6$?_Q6-Uw*ln4(POBeV{oCShsN{J|!{+jeVWWes1zy^%`YtH9~gZ6GA@^@z59`gXCfBb?x z#)RJDFlZw6C%NoqPRaWAeeV+(d6}<_PS%_;Fp2gET488eBG=~m!E6ByIyD;{J;iV| z)Kxn^ufO+MB;ItWr{kf%bMBpu(r>jJs$4dw>tZM({IZtbD(`MF_wBQ5FnAogk<~O6 zAsoA8-uz|*BWsQ?{SdOW?!)5_F;>Qm3pnx3?wqHKPkem+!>#vqH|BJy_pG;n`Qw?% zPy-e8_f!IZfi~)Dnzs-+hHkpZug`sceScVm#_gGHr`{Djw^KYM- z8ZsmWLt=M)%Au9ARXA#{gX$ zgSf)Uja{>V?pFPCxaj_$o4co{*p(7XqE)&!DeZeQ``zo>`{gfOw$((Hynk>$LC&j1 zlPGy|APPY4@4FX>#MCS3?lK@56P}rN+R0*XezR0jG2e<33XN+Rl)iQI*nD212d$`x&av_7*<5;AMzk4ltV zv5#ZWy>FDDT=QtIJH@55pWh;FpUX;6hWkhZbZUU3 z+2XZfx-I>c-&4>eA}(8cOdM{$vZ;^*BEUjrQS4K23^Ct!HGGbXaD;g1IcQLwa?yum zmw2O0Vn~#KBL)Mr;LCzhZDS5jTCb|}+)`WWYY z^@Zv36~Vv=Bi{%+%(gjMYXGo2H4A(R8v+~ImaHwR<}6cYHrUh=m_7SXPNFc-%K1?t zKtWk{Av9zPM$u0{$1%8KttjohJojg|$@U}lV|Lzn-u0Bbrl^_aeojDWNOt5WcRi<> z3UtfDv#R!bu|KIA^EQcwQKv0C^j|#nlXgn}E2aw|QMWzdyrlXyZMAk}YsI7j|hA^oTc-#ZovJX1>yxReZd3zr40c8FvK}28)UiBeK%8 zt(T@dN7OlEw`7N*6~t7;?SL$$;n_Pa%`QPHyJ>jm>R%ca-h!HKY3=6aEF(M4(IlYN~4Bz6DC{pmAypyWfld@ zC)dvJKQmQ~%fvcf8{@xd-kJ0zoVw_gy1MXZw#MLummc}2dRpcCca6S?oI4+l0@KcK z*V{$<3VYVmt7(v)Z+9C02#T~!Z06GX+V@-&K*)AF#?fNhlXpPHS9O0~Pfy*sznDY? zJ>N3Z+2tFZ@s&+IkIQ5SA%sKqali8}LWJaZEaY6&0t=y;$2b-)%-_To2doI5k2jNU zt$0R5r=7X5e0xZ|?`y{5^4oh|$; z2mG_ur8ZkB`d;Qb3}ri1^_t;)Tr8DN&dn1c4~MG?tVNy>>c|)n`>N&-Lq0@(m*w3$ z0tNPMjZX4L2#(}7;s&6+P3N&zv0AoUzOl)k|MbdUV$0vYncm{V>{WEZibd4SI`y+z zbOK1JX}BAMiABsG-Z|<2@sxiZeNO(Zmk)P|eBBGp14TY&fc$GJL6Yv1w~NKGZ14nU z-#y+s;==|1(fOK9T@5CS&g*K>sIX9+OblasVEjDXZzQi@pL3eqQ_6j>E-R1ty*EB&X9?%Hx;k6S{C{FYC(VGJ{J8LUnWw7jU#ksfj!isVCjiW(~`3RG3FhdEU)y zDhx)D8SEwwj(v4vz_(S)zQ2@qrIlqiIe4-CvEkV}Q2)l@w1qS9&aru2MEx&%ow_3U zqOHN?!DKMq99KY=LxSy|rcv^45eb+DTU(i3z;?-4fbZ-5zS{@mQyv$Z;pzY$}=e~!N3;#rhZAppXC|NvNQbKtUK51V_zk}Ol0tX zwyP@VcGp~yu;Gv39}=2Tp>gtYgd9VbKFr)|;hjMS!67)Y*q!d3TX?)9(tx-7<4#jL z1+uq3sxDsZ?HjHnZ${jayyg1AjSBW)V9@6(RWX)I%m6o8YpDjxlf@cZEhLlF#kaw# z_o%+MzN9a5#+jlh?qH;&KK+dZA>?CB_|DS6nhX01CRXvKBWcZrg7lL7+x67aR`q8D#`AjoSzft6dlc(0j z$DNp4=5DgCTdMQx4E=n~`3;v*T59Om*m4YvLS^h}sl2%l|J+<`51)>eSrnU)YS1Y50BtuyNtUlDrsJ|p;w-|etOWCYJF9WdSi2!r z-=Ws~4oM8Gd!#YFGsw|N&nicLTLdXZww{8FD3mIC$s2qf$1kO{3NW9MVM3ghuSqnC z4sKy%;E`sj>$r77#%uCx+~Eerr^1JuGk)_GK@WMQaXdEEp4bW zfB>+Ys`Kt0Mg2l`l}P_ZxGw5T0c=6fSiSmTsM-mync zQVkA!yBT!(pE;j8kdRiGEbW!ZreB)1EQ9Q_lCs&YGY6GmPjFyJSJQ@XX+{=5K}S~f zgk-HYii5>y{m*s?dO@I3==lGnW1X6*l#X zYWize_QWivaYA8{o1!`L7T3J4x?IA|ZBO--p%0kQF=>^{E<(*~zmDUa^>lVlyXj}do5@)z4Ep1xVdaZPx(g-}i| zeSddhB}{<`e&l;{_rB5DrRbSdgI>8MI_ei`l<7J!VE0IG?=%kbLGkU--ID9l%1MP0 zRj<@)Rk@1moo~rD!VDqqY62Qwo#J4sxE7LVTKIhM{x2Fw&H@)uNx+VBe9kZRxi}5Z z0xv=pG7|x#IW=x|q**`476_$(R_(h9&j&ZM^pHqFoZA>r8FhgLEw;DW&KKP%ojVNJ zP00|w{ONTX{yeZ$5_Q&*z3IZ&ru>Wx52RMuVpG{qEQq)Hn* zV^e;8^M&wQ9c93N@*Oe^hs&+CdWx4u#uhP@=-lpc`~5*c&HeD|p*F#2e4%OUxsNz6oXU`dLsN@aKxV@gA&l8q=+obtrd7tP_#;tJd{L;#7q&gX} z>#yUMuj!>tTg8v%twv+nMV0$#yW3H#w{6cu!paTKkbK1K2g?8Gh}m~?6@?!(0F@QH4TtBt$i z_~l;4&Iq%md1H+qLK$!pzl$e?jWOt8W)^1msbX^nYb8;INvVfS*CB?Ipf+^VnzHC- zUgy!)aP{bCTQlHnq zT8se?-4nY(P{g6Sk@GOmku@}Vq555Hb(fr)^lYr++LBAr9b-;%mFOSJ{A^`p40udo63C+4KC17h_6J7h)8=@ZNfZ zgN5x^rVhmF1kc`Qsuy|WUeCrQ;WFsq5bF3hk_EKH{PEYa!6@mCt#5h$7YiVMIMVme=%rhgVUdb2T1c$s>P(5avMvU)RMe+EZpviw}H3pMCFSTZuVSrWO)`1OLb)nds}$b_rrT?d8?> zmmu{d(Wg4wKxLcakka~gJ8t^nr%&f90smkY$YH81!~_$9@iN>Fw_!4l*9ND72RJEm zz`_sj(a5n)ht;sm-YuyMH=@Okxhso))>Uls$gbbXoGU3=Nf?n=7Z+D(a$_B?BRzj2 zeEf=fw;qsr{|4I3GbXx9Lh7~ePi-cKtG3ti#$SJh-l;DQBxS$K?eK(4S9+K$(L<#tu8zt6h;<&d) z_-dq?r}VFF>tmSPR_N5NAFTauM8L+ehnu*5OIyW4vgHmHFU;vyOqR(;;#UN}GU5VY zMemoMA+ZcSTG#j43@6}hOBYMw36IJzDimzzcl@7qvne1kCHxz)7TGZoV;`r z87j02iTYvY)BeG~jKKvbdoa}8x-ac|YU;K{WG~c;=JS)Mx>sx&vU61h0|ZM^?a+QO zw-31@7D?VP{cyUa+43dmlF7^3D_y|KR?YtM$?YWw_KzoY8Yv-cSx;(!^RC2k(lT~b z&81D-CqJ6GD!BKgs>orNUo7zTV3I0e5q);8RjX|@@urdU%U9!@f<2@Px*2#Uaa60- zJYB$O$6`HAAj8lPUXSXtZKUsVI_kxPm`g~e23MWu^_Y0vgt<b? zzpbJydL>Hs=y=Bn>#Fid=@dKNmnp8eU z%*5(R2e-)CBq!k%7H#jpPEDITJm#IGBDUDELYHfqhgYYKOp@1uo+0q=c{0LVQ@vyV zPJJa{bpK@^zlL!Ee_GCykUKr)fvBNGWE-W37g;?y^3Z#6HI0;wi@`=HVNgL|fB2h-2CDlge=P9nuc{q%4LSUSXJM!eyniRrOd?6ytp*O|wmu<1c^~hjL>XXLF+bnv(#4CJexD_pa`nKm zK1TnZ+@`xUqT?tdx;j7n26E7{W;LAQIBO%#t;u`^wr{oTN#;taBU9C+kj(j8fbj^pc=HqkC0!)Y=%br+1s@8l>88;gdwO~oSMboOk3-^ zuor7Js{ea1U6bu8CE=<3xS}GASN8HWjLp-vvinZziN7Aq;tD!;;m&by{-@L8=W=O$ z4YSfD0j2#l^dM5r{_xt(IRk3?;t}~Mg;Z^NlKu^I)yccaXa|pVuGwWcJ^63B8Q8~{ z_p1G`@9#jjZlMnjoT1o2wSZu%Jbpw-&&;9PV*AKI9Kh^ujj%_uMUKXDYvD>JylMLI z_g|hn)}){A2kcIw6!N$!-~QSaJ1R;ljI6oe$uLj{8e_md9_wpd z)yK^qK!*BZhSAOEZ%2D@(2m~_eP54p$HDeCx7xz{7HWeB5l;ipJ+m{~N7PrBl(mYe z8no#`__Qd0z@pTFPubHlVRQc8y?bZdL?dp9Q^?=Ck1zH|FJAjz(#7OnHX^?2BxeeW z?K@i;lRXMQVgM15=f$UiKVEn-w)x7=ejQ*AM*#~=4N?St=LuvtDBfcFko}vspu@9L{!v9}U-NBBhB^0X(1~;@#^5^87I5zwUQbk`)T@>7^#8+dD5v#V-VSIe?5mV;(|`;e$*V=Ka?xa86aTY>7muNG9SZ%IszAp_M2PsSxA&MkV31Gj30kIYN1!szjy_+p*KRhn^MC z3TN;Wrl4R@p#(PtC$Z1uTV&QBR*@M#GO6tPmvOr&UJ$fJeTq%HXU3B}V~0f;7VDN) z+8S7G(2nXO#o-Ra0{|hpm;{uhtA|jFkSqd$y!g&uep{8eu!~ zi;Fi|yi*<%Gi6N}@$o|IA_#9Jkv2MgFkBM?AlgCGrXWd*i;Ks4S-x9*3aZKSwb}1! zvcE04BM;;(AeTPTVxx`3$A7#sOJFa-=*k2pC->RB^Qj4|^j)1W?a$tREWejl{fBKb zkqi}VDy4THGmwRWp?9|>X85kjuJMyXb-RcU^mmk$XIEnd@IOq6|MB5Vmfd|i|KwBN z#B!d^qN{P$`f+nYvS(U0Mw@TN0OLk)AX&f`Dcg|ZOO@Oq5wyi#MKXO$01 z?kqm0V3g%Q2^+H0K!=x>&I7m$kmsZIux|t{^3Rd=DV7f$c}k;HgE(>~?vM&l!{N^o zFoxry?(QUv1P^2%GQ!QEEigs7N)nw5y&q$1Y!)s4t2U}jie+>QtZZJx3RkODg^h$c$MV*c-wD=OrfM$K|lx^EtCTsu`=h--Ayfw5%r!3dhI|1w2BoYM)aW8;a2$D{uHh08L= zh~z7O@fX4Hot<=!_O1JJsbbV0M34~$Z5C5L6|iySt2aB}?(Ra|(!zd77_0eV$_|z8 zULj&daR#1xa-NqN1jr2VgU!dLLgKCj{5~?oZN`d<2Q1VIA8l+enG>RTLN)ahXVEM- zf!F(yI+!#8@Mj~$E^Y;DP&0#w+W%kM;z>@n9f^U^7+zZz5+e(t2hF&)qq=XNslz7q zY`4qO`IBY?3rzJ&FOx4^(;-D)eel&zZK9D>yhCmD731^oAq8CVe06Mw7>>yVj8=ij z3}9t%t#E4mv|Bx2{^jo%e^fq^@;i@=9Oi%m0CSKY*r}}U^i`y-g@d{?|L2?@PuX~5 zoqM9C^A{}1k|i-3V@-sc_FeP$>{vxuM@`7#iAH0|Stq?|Ggwv|5aRjq=!j;y+K0M1 zZXNdTcRIE5#PI`rkRBL=ZJs&`MK)-*ZbHop?H1z5g(0QCk4+E6fZ0mCd@sHrN_?qpKV) znv`B9D=(%yQ2tR#UNfwx0meV&#`C?g_I+5>WUbf7b_|sd1S}o-@kM zzvQZsq`KF`cA)Z&D>><-47(W=!SEB3gHvcfL_Ok|=6btYrvzb~`t9V7Q+$yp=|iB^ z#yC2StKV-FwM~zL%~HHmk4W*FB>`vj1Kg3Oxq_-OlXykD7Ct3u%7&wm5tGjh&eQ(= zxGac#Mn9UqMjFNL{JI;qeWX?)YHkt|SL<7=pcA{FSiCV;yWzbH4&(XRes<0ArG<0* ztivkeYqUo}YwdZ(2gx$^N^gxa9Oy+2uwnHDz23K~TgOYG0sBdKCz;I+?Xj-fRHgcc zxA0K0ZhK=l+)#vYl5s;3(tj(IWHQZR|(S~*F zs`ZO#Jq>xFy>Z8+ZmVvqVof)_n##9#aB~Y=(C}4htJ7BfScKUv+zE&QC^hX8#Esay zcx&{uRLV?`;h*hmt0#*MIrm0WZ?Anyuhf6l+p7nlBGav>=92VXaN=jDyqmm0;s4a% zt10-)s@x_0_5D#5Xk;%x(TjJWmNEFy6AwrMP}<9`udB0o-)j38X^+%@#GzHAXw_h8 z7IrnL$66PWE(xIveTb+w>oGn2GUI_T^OrM+Hm*X4lE4)evk>Z4o<_0LB|V_3rO1-` zMEf^7pA!0@Si9>>)7)Bi!Q^vZU#VJ6=SH% zc8-Ve{iunzUi;egCs9?f3(t}CAvWgFiT3KFe%ZTsue!Zce|)<;NMsjY0|6gJ4tW7>`;^AX^ zE*`)^1ag+_#GJ30$H7j-uoFWL{pXiO{*bsa8mj6*l-X^mcv+3XR;$$~RplBQu?oDD zr5KTXG62+*<@=V+Q4rc(&_y4^?XC2z9^52q!{x^rP!l%V6A?JGgdFavqWXzb0;N*( zU@XuE<}Fhg=UN=os-2;;1(_aw(Y}9JIP0BvJtj7w z$jJsB>?@0jzXDW^3jH2SXBT$r7)cEhP5BL9?uOH~__FtJQ{L3@4v#5sNczEE5>Z2> zLqQe1L|4i8tAG!OAUo5+hi`eB2TlQ{)naTfCnsO+GMW)UfvHLWsGz>64a_Wg;2Y)> zw8}aP8OgSwFLu>b&t>En7Xt^;0a!x0}KR3J4Y zfFAzx?$8W@8tnaiNYM`7m#vABB&qw$2Gw*}CgQ5=pSrba?rX%+ehRAr-^-94itO@` zn66i&vc28HHt4KMI8?y3fcnWu_-HPc(sZF?olaG;J=buu&Zc5^~EpUt*H^J@0mE3 z9~wm9-mAgLJ_B2>4XF;Gd<38r==nL1`kxMihH#kny$d#w;LZ%MU5pLD6+9yA!PMTp zl+TE=Hvh&5miht`m1&IT)?|skL%KcjHI@QXlb0%C9+SYdZS=i!;zx{UJ-)2xaCb9# zsVX{HKU5v8|9DW~^JJ2x!Q9a1TQ76Pu{|H#28Myo`G_Co!#Q8!CWA9m^Y)@Rybe$V z;Z=Eu-n>IPQ5#`13V{bnbD-Aw?$~h(Vb_5An%8QSw8P3kXXIVT!kkJ;`A9fz7xRbRM91?8$@%jSpJS$npc4lAxrv_)20AnS(6qd}_= zm_p?)3ruiTMnrXnXq>W&4nf7Y@b{ZeUQzKzK-BbZ{jBwjE3&b#`}XfY7r_|h{6GI3 z{>*V{^a{HP+&n8dRMa>WV=IMggWs?ksXV0m;8O@A|gB3hJNGq1Ci z97ytE>acn6=hgS5ph$<6M?hHTB^UqB)vX*<^#(Stvp9c(LIqaOpXm}nfO8qEY8=(C zY?hNO!i|y){_EDTuck?|u>n}6Jp;R+r1t_P3<{rT=kK(gRAdgWb|VVblTYb8`_5{5{U2`$d_a9T-MYdEdAB1s z&QBYwja4y687>YiR@(b0SjsY!GPuRkouf9ir4`gh2^6rYkhAKQwk7vb3D`S7xT^m4 ziTA0gzrXn*$2)Ro^4Eh$xw!@$fAO*{Xx`~r^?eg}+};0T+=fAs!3L`gb^4u(xcFpU zA4|*MbdDYPWz(>C_s*MtZhULu_3_eJ>IEp5zpbO0`s3;4m8YUNO3e}{9a+@XV3%Io zE6eO1l02p3qvOC~YIspS_3$leDO39wOhEelrhXF00zFgfV_VK?^mwH9llA^K32R7; zo3?L1FZ_2sTZ3&|B9{HNV!(kxj&^qOPQyo3jr|nl&^Z~D?kKB>9~==8!7AJrE?oG` z)-MvpA?4D*U^$ z6|?TuqhzTB(13i77+EWG!Aw(`6%!XHID?J}=l8^((VN%u_%U8|2pN`}w{NebUAx$4 zeBs%%gXb?;kVm_4!+qS)v_wd-?>t?P=^rcJLo--zc+ms2?PVOUQTKcR$ z?PU(T06bdC(vsIKU%ossF>y7FD8)WQ|1!fmYTvk zGSYmd&2GzC;XkA|R-SIUFm3PG#!p_pjHTvrE#p^NS7vXT#{-uxb*`hT(pCS1Cy+TA z9&caOjD2=YG1xY!&o=47%CD#hZrHg!>_uKSU*Is;(UDg?cZLJ-OV#DjboaDx_Jts& zxnWzJniy?hv(PlN?_?SQ|A>_o8BAbM^j|3{v5<*We! zS1(_^DzKT=a&6U}m@$_I2K#>f+F~+j_`whC$@GCEnLy@ml3w4weS5Lb&BtZOqai{g zz1|=ZGu)^*+YcDffxTGoX_Mg8a!U^IJT=;N#Hbps9O&kDGg>;~nLa^tZ za#lhm5hqOr1qG|WY|?LoxMoIZ!7HFVU6o$(8{j3@{Zp=h@BjyzH zj>G7G21rIbW8i2*9RMqW*JO=9J2msaeEW8LXr5=nNSC*&l?A!>J+TsLKls@Dd6;X0hMDcWQ~03LxeR?IcUzDzU(nS+hRJ$!MSC zX%lL8fV2DILdS^?%?4U_tozDkK8`(=D(+>|(9qCxjMJVne|~T31WSEI?L=l5Og~(y zIUQx%p+hWn#n37NmM3$X1vLp$)c@-&kGDt zREP?QTKO5IS?Uwmhhm?qCTvMB%JnoOsVyui!HpOiHYF^~@yAuG0%y+b&bi?S-8#af z6lEkGv_13k^HZ|3T|!O9U!>_ns*ci1X}i>##kApyG|T<0_{NPk%a$#ZE6#rq{1G}u zE1DH9{rmTqz2bVLr~I{o3nL~L)k7Cq3kWi^#{&3Vvt~`P{m?z9PSsu=zY<%qq3E&o zj-u*sML=9T$`d$VHBedk8Kk8qQsI?oyIBwf3N?^0cmY`R@b>nmUwZ)#p{cp~dIo(7 zlym6-3*7&#&T_+o8<|AyVaRtGj;6NxG|1D5 zps3za*e}s%5Y^CyMb<{+Dq-b-0o$FLjD+e=tQ}ShnT(_15R_dU?XT?dFV~)>0V5pjfRzZFrtGvD!_%#?THoK;;Bq6qFc<_jJQVJ;w| zb{8A9vN=e_mZZS;Sw6T@HP*e);o5VhTdXbKw^>u2@uH_y_R^TBKYo6cd1ia{&fESS z; z>pvpzs@c3z_T%<{>+;p3-Z7boxqkM*N?T_9m9kH=Qr=( z<^A>7s1~1mG6W2zx&8oOZny~akt3UMGjfvJYJR1lLRK+8e&E~tX5aMbaP*7&6%G#9 z{y3A`)aiRC&DGgo6}~%IThlQ%e5BFZiOS9o_=4hF;BTMh(W*G=>O)@@i|y4S#d
acBZi~heDWIZt}d1ab^e$f?AnJQ=KGV${ZgARA!a-kBZee8q@=sX)rZ?T=XTxjU5 zP=Y&I&;D>_So3q=-magy^1WMiU*Ed8B(GQPvL5Z(`*!Zq{X<>Se;vDN$J-*?-ThpQ z3%fB`Z4E!ssk+0vb}c7N(v;b+YWwn$# zVL<2Qsgm%*N@smZLEo>N$IrhtQ8HmkQab>|%-i<1*Ru-#oJtU)D=vpNOi;2$!3j*7 zJmlD&q}T(GzsKI^|8(J@@FL8GESQfuJ0hsXe~oJ;&*iU9fc${{XTtYY)4EL*AWgs+m;M>Gf2J(RP4>$w?oiATIWvr zVj5Ky!m;(H54I&vF>yRZ-yYA3QQJr5%gXp%a1qkpEFeF#2iv%PG<$Xf+SB~Z4RsZT zSjtHPSP+a7K|Ka_h&<~ko@E$Op%(cn&kEiP^snGyh};s?6kdZV7GWgTOe|0neSH8O z^0~}UMd#7?`O~8NcTo6q-Vv%jdHQtC%a<=3Hm3)wb$=NARq2EKExc~2o5h7+M?9sn zH>d%l=E^OQQS^Y14T^6bsB0B$gW8m3hSyZ)7r-7}CO@V0K7|?-QWe5#DcPBgR~Zv9 zN`&g>+}(y;N$;cN`9U1Vs;4o-4Q1iU3l1S*Q9F9oNH{X}@S#ISN#_}P_V=On84?Q9Ue)-m|wrcaa_QZ<8SxL4{ z1wIDDf}zR9gvq`*hu1iYamiQZ!*K)V5*f!JLlkEnYUT)RGI$qNC-F7HNwc7u8sa0( zul#3<3qyEc2}fEk?ZszH0f5S9JGg|KiE7fyf*G!te2##E|BHP|Vxhx;_Jp?^7A7sE zYHXqm0QMt4jnt^Mz2&{Y&u{FCM#{H{IE(M4wJ&lk$`@?)S;Ab&RpASPV4IC|J{rKy zj^VjLWwi(W*KX$Sh>2qZp4kS!+WiRg@mmN+Ta&9vUA2*fcV*erPUz6?>Tp$Df}YacdIPCj>|2bgZyk3lU+G!e1c)UFI zy0#vvLNB=V5l zl#|oRWg;!E-F;i+KJ9m=r5^<#EA>qwZ`h4nAF8IMH7lSgK1=h`6vHgw>6!CU6Qb{6 z=P)57(;!Bf3STCC5i)^Llgu|nq_5W4g24o7??f)n6UhsyhQUsn;jra_KR#cW~IKm8D=E)R_3>Wcldn^vz%1ouZ^)TmCEFPa9rCZC8e>*c|91LCioj zCk1bcud1b)yK@@H&^?XRu*ADnFHe+cAX3htS` zDW1FYA;zZrL6Bq(XlIEQ%+2q8lt*hi5^3b~XaPa^L@rD2tUo@Fi^l&ev@B}WrONym zDDhX*0|Qt~)yge{6+NrJMzhBI^jz56*%>AzB&cRrU9x64Zk)8Q0N_>AtBqpuV8=^k zQTnk+_RuIcW9G~e+TH5`fBvVWKQ~->s7B?D>Do7(F^qMmZzIv* zfC00YsrlWi9T;wH<;p@e!Ai+cT8LNTK>&x8a}jS^a$M?5TCM?NgHe%Z(~K5srir15 z*OJt^I62@aTC{4F4|`Te8Wdwy-Q0$%s9J7K4DFDypfp6x#DLR^=kD%)pWa1n z6qj=h-)x;<4-q(k)n@W>x_t9VOA|&*G6dIou%!Fw+5%s92{kQE22>Zw*zbvL?8hJ_7N!$%1L)=1Al2YO&ssH%v8J`5D*8aWA2+{4S0H>`&q1@p`Pv_(pzV zE7N!5SU#@}dKC*Bzq;2gr33?b?smqUKvMTOAX2;{*|N<^Ex9Zm-EFfR3d&i{(M6u+ z1cDgjuxxAi5#n4)s1u~(l&YX#Wv8B?uTj#{A|RP-{TUr;ZkhKRXjx80dWyx1AqOV^ zi`YgyXjMB)Fue!-ci+WhBI}`P?Av*MCkp15SUc9b! zl0{saj3}{H+mvrPQ-?=phmbkv)448n(!9)#mlp&GXLunU@k<;y=4X=*2~Mz=TA8 z>-+gsT?yfA7;hx|ot;`Q&=jg4_XyZq{yv7;Lr{rxDqpMDUihGg@SZJ?PDy=}X7WpS1b@>pd;InYG$B<=sx7ME%XN z9{Z0jx;eBkCH3f~sSasQFaN_}*1c($!nTU#O8e&E4Z0Y3|JwODA424VTYI-kSs`b^s55>AXS(x54u0CYP`2Fv;+_vcTPj+49hj$j)5lI`* zgy{4pt3!gf^gsU3mR)`SeZQwB*{4Z|I8CU&wLfYVa*?BL%Tl&19 zaZyD Date: Mon, 20 Feb 2023 16:20:44 +0100 Subject: [PATCH 9/9] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e9c1812..f5a0054 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Requirements (versions are mostly here as an indication): Pyglet (1.5.27) Pygame (2.1.3) numpy (1.24.1) -tensorflow (2.10.1) +tensorflow (2.10.1) (no 1.X tensorflow) ``` How to use :