using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO; using System.Windows.Forms; namespace BryanKellyCapstone2017 { /* WRITTEN BY BRYAN KELLY AS PART OF THE 2017 CAPSTONE. SPECIFICATIONS GIVEN BY DR. DAVID PANKRATZ & DR. MCVEY The main (and only) form. Contains all user interaction whether it's output to the screen, selecting a file to open, selecting which type of search to perform, manually moving the robot block and even setting the screen to hide the walls and allow the user to set the robot to place visited markers behind itself. Improvements to make the searches execute faster would be to separate the drawing of the screen and the updating of the log on their own threads. My idea for this would have been to make a queue of the events that occured then on a timer check if the queue is empty. If not empty then execute whatever task is next on the queue. For updating the drawing panel I would need to save the location of the robot in the floor map in a queue as well. NUMBER MEANINGS 0 = EMPTY SPACE 1 = WALL 2 = ROBOT 3 = TARGET BLOCK 4 = DESTINATION/GOAL LOCATION COLOR ASSOCIATIONS WHITE = EMPTY SPACE BLACK = WALL BLUE = ROBOT RED = TARGET BLOCK GREEN = DESTINATION/GOAL BLOCK GREY = VISITED */ public partial class Main : Form { //ai test variables these should proabably be removed private int goalRow = -1; private int goalColumn = -1; //other cs files private FloorMap floorMap; private AITestFunctions testAI; //graphics stuff private Bitmap imgFloor; private Graphics imgFloorGraphics; //bool private bool isFileOpen = false; private bool manualMovementEnabled = false; private bool showVisitedRobot = false; private bool killThreads = false; //constants private const int NUMBRUSHES = 6; private const int EMPTY = 0; private const int WALL = 1; private const int ROBOT = 2; private const int TARGET = 3; private const int GOAL = 4; private const int VISITED = 5; //global variables private Stack stackMovesToFinish; private bool isComplete; private int totalRows; private int totalColumns; private int robotRow; private int robotColumn; private int moveCount = 0; private string fileName = ""; //Brushes private Brush brushEmpty = new SolidBrush(Color.WhiteSmoke); private Brush brushWall = new SolidBrush(Color.Black); private Brush brushRobot = new SolidBrush(Color.Cyan); private Brush brushTarget = new SolidBrush(Color.Maroon); private Brush brushGoal = new SolidBrush(Color.Green); private Brush brushVisited = new SolidBrush(Color.LightSlateGray); private Brush[] currentBrush; //additional Brush for Grid markers private Brush brushGrid = new SolidBrush(Color.LightGray); //program Main public Main() { InitializeComponent(); floorMap = new FloorMap(); testAI = new AITestFunctions(); testAI.SleepCount = Convert.ToInt32(nudSleepTimer.Value); isComplete = false; //setup brushes currentBrush = new Brush[NUMBRUSHES];//the number of options for types of squares currentBrush[EMPTY] = brushEmpty; currentBrush[WALL] = brushWall; currentBrush[ROBOT] = brushRobot; currentBrush[TARGET] = brushTarget; currentBrush[GOAL] = brushGoal; currentBrush[VISITED] = brushVisited; //setup graphics just to avoid an error imgFloor = new Bitmap(pnlFloorPlan.Width, pnlFloorPlan.Height); imgFloorGraphics = Graphics.FromImage(imgFloor); pnlFloorPlan.Invalidate(); //event handlers this.floorMap.OpenDocument += new System.EventHandler(this.updateFloorMapOnOpen); this.floorMap.UpdateCaption += new System.EventHandler(this.updateStatsLables); this.floorMap.targetReachedGoal += new System.EventHandler(this.updatelblNumTargets); this.floorMap.allTargetsReachedGoal += new System.EventHandler(this.allTargetsAtGoal); this.floorMap.multipleRobotsDetectedInFile += new System.EventHandler(this.displayMultipleRobotMessage); this.floorMap.moveRobot += new System.EventHandler(this.drawMoveRobot); this.floorMap.moveTarget += new System.EventHandler(this.drawMoveTarget); //redraw Goal after robot moves off of spot this.floorMap.redrawGoal += new System.EventHandler(this.redrawGoalLocation); //events from search class this.testAI.errorTryingToMoveRobot += new System.EventHandler(this.errorTryingToMoveRobot); this.testAI.robotReachedDestination += new System.EventHandler(this.robotReachedDestination); this.testAI.movingOnVisitedSpace += new System.EventHandler(this.movingOnVisitedSpace); this.testAI.DFSContinue += new System.EventHandler(this.DFSContinueMessage); this.testAI.DFSReverse += new System.EventHandler(this.DFSReverseMessage); this.testAI.enableShowVisitedRobot += new System.EventHandler(this.enableShowVisitedRobot); this.testAI.eventResetRobotVisited += new System.EventHandler(this.resetRobotVisitedBlocks); this.testAI.disableShowVisitedRobot += new System.EventHandler(this.disableShowVisitedRobot); } private void movingOnVisitedSpace(object sender, EventArgs e) { //this is just a function for debugging that I used rtbLog.Text += "Moving On Visted Space\n"; } private void DFSContinueMessage(object sender, EventArgs e) { //again just a function for debugging to see when going deeper in the recursive statements rtbLog.Text += "RobotRow Not synced\n"; } private void DFSReverseMessage(object sender, EventArgs e) { //a fuction I used for debugging to see when moving shallower in a recurisve statement rtbLog.Text += "RobotColumn not synced\n"; } //move Robot paint Events private void drawMoveRobot(object sender, EventArgs e) { /*will fill old robot tile with empty space and will draw robot color on new space will check if the space should be marked visited or empty then makes the draw function*/ moveCount++; lblMoveCount.Text = "Total Moves: " + moveCount; if (showVisitedRobot) drawOverSpace(VISITED, robotRow, robotColumn); else drawOverSpace(EMPTY, robotRow, robotColumn); updateLogWithMove("Robot", floorMap.RobotRow, floorMap.RobotColumn); //update robot row and column robotRow = floorMap.RobotRow; robotColumn = floorMap.RobotColumn; drawOverSpace(ROBOT, robotRow, robotColumn); updatelblRobotLocation(robotRow, robotColumn); } private void updateLogWithMove(string movedObject, int newRow, int newColumn) { //This updates the log. I think moving this to a thread might be better. rtbLog.Text += moveCount + ": " + movedObject + " moved to " + newColumn + ", " + newRow + "\n"; } private void allTargetsAtGoal(object sender, EventArgs e) { //a simple acknolegement to the user that the goal state has been reached (all targets at goal) MessageBox.Show("All targets have reached a goal. \nCompleted in " + moveCount + " moves"); } private void updatelblRobotLocation(int row, int column) { //updates the robot location on the form if (row >= 0 && column >= 0) lblRobotLocation.Text = "Robot Location: " + column + "," + row; else lblRobotLocation.Text = "Robot Location:"; } private void drawOverSpace(int brushType, int row, int column) { //this function will draw a color on a given space //want to avoid drawing over the gridlines //Maybe change this so that I don't redraw each time. This could be making this slower than it needs to be. int x = calculateCoordinate(column) + 1; int y = calculateCoordinate(row) + 1; int tileSize = calculateTileSize(); tileSize = tileSize - 1; //want to allow for one pixel of space on each side of square Rectangle tempSquare = new Rectangle(x, y, tileSize, tileSize); imgFloorGraphics.FillRectangle(currentBrush[brushType], x, y, tileSize, tileSize); pnlFloorPlan.Invalidate(tempSquare); } //move Target paint Events private void drawMoveTarget(object sender, EventArgs e) { /*this redraws the target. It doesn't need to draw over the space that was previously in because the robot will now occupy that space*/ drawOverSpace(TARGET, floorMap.TargetNewRow, floorMap.TargetNewColumn); updateLogWithMove("Target", floorMap.TargetNewRow, floorMap.TargetNewColumn); } private void redrawGoalLocation(object sender, EventArgs e) { //redraws the goal location. This is necessary after the robot block has covered the goal drawOverSpace(GOAL, floorMap.GoalRow, floorMap.GoalColumn); } //file open events private void fileOpenClick(object sender, EventArgs e) { //click event that resets the robot data and then calls the open function from FloorMap class resetRobotData(); floorMap.Open(); } private void resetRobotData() { //resets variables on form to default values moveCount = 0; lblMoveCount.Text = "Total Moves: " + moveCount; rtbLog.Text = ""; robotRow = -1; robotColumn = -1; updatelblRobotLocation(robotRow, robotColumn); } private void updateFloorMapOnOpen(object sender, EventArgs e) { //updates the floor map if the file could be opened isFileOpen = true; fileName = floorMap.GetFileName; drawEntireFloorMap(); } private int calculateTileSize() { /*This sets the size of the squares to be drawn on the screen. It is based on the largest count for both the rows and the columns then scales the size based on the panel size. */ //set rows and columns totalRows = floorMap.NumRows; totalColumns = floorMap.NumColumns; //setSize of columns and rows int tileHeight = pnlFloorPlan.Height / totalRows; int tileWidth = pnlFloorPlan.Width / totalColumns; //decides which is larger rows or columns to make squares square if (tileWidth > tileHeight) return tileHeight; else return tileWidth; } private int calculateCoordinate(int input) { //calculates an X or Y coorinate based on input of a column or row //this could be more efficient by storing the tileSize as a global varialbe. //But writing it this way may make it easier to avoid errors return input * calculateTileSize(); } private int calculateCoordinate(int input, int tileSize) { //overloaded function just to be slightly more efficient while hopefully maintaining readability return input * tileSize; } private void drawEntireFloorMap() { /*This function will draw the floor map from the file that was read*/ //reset panel image from any previous file if (!isFileOpen) return; imgFloor = new Bitmap(pnlFloorPlan.Width, pnlFloorPlan.Height); imgFloorGraphics = Graphics.FromImage(imgFloor); int tileSize = calculateTileSize(); //loop to draw the board for (int row = 0; row < totalRows; row++) { for (int column = 0; column < totalColumns; column++) { int x = calculateCoordinate(column, tileSize); int y = calculateCoordinate(row, tileSize); int brushType = WALL; //default to a wall space string tileType = ""; //check to see if the colum exists if the text file wasn't complete for that column if (floorMap.isValidLocation(row, column)) tileType = floorMap.FloorLayout[row][column]; else tileType = "1"; //WALL if location doesn't exist //need to check if the last spot is a null if (tileType != null && tileType != "") brushType = Convert.ToInt32(tileType); if (brushType >= NUMBRUSHES || brushType < 0) brushType = WALL; if (brushType == ROBOT) { robotRow = row; robotColumn = column; updatelblRobotLocation(robotRow, robotColumn); } if (brushType == GOAL) { goalRow = row; goalColumn = column; } imgFloorGraphics.FillRectangle(currentBrush[brushType], x, y, tileSize, tileSize); } } drawGridLines(); pnlFloorPlan.Invalidate(); } private void displayMultipleRobotMessage(object sender, EventArgs e) { //This is used to inform the user that only one robot from the file will be used MessageBox.Show("Multiple robots detected in file. Only using first robot found in file. Other robots will be treated as walls."); } private void updateStatsLables(object sender, EventArgs e) { /* updates the floor size label on the side of the screen */ lblFloorSize.Text = "Floor Size: " + floorMap.NumColumns + " columns X " + floorMap.NumRows + " rows"; lblNumTargets.Text = "Targets Remaining:" + floorMap.NumTargets; } private void updatelblNumTargets(object sender, EventArgs e) { //puts a message in the log that a target reached the goal and updates the number of remaning targets label rtbLog.Text += "A target reached the goal\n" + floorMap.NumTargets + " target blocks remaining.\n"; lblNumTargets.Text = "Targets Remaining:" + floorMap.NumTargets; } private void moveRobotNorth(object sender, EventArgs e) { /*calls FloorMap.MoveRobotNorth with the the ability to move target blocks This is used for manual movement with the arrow keys or buttons*/ if (isFileOpen) floorMap.moveRobotNorth(); } private void moveRobotSouth(object sender, EventArgs e) { /*calls FloorMap.MoveRobotSouth with the the ability to move target blocks This is used for manual movement with the arrow keys or buttons*/ if (isFileOpen) floorMap.moveRobotSouth(); } private void moveRobotEast(object sender, EventArgs e) { /*calls FloorMap.MoveRobotEast with the the ability to move target blocks This is used for manual movement with the arrow keys or buttons*/ if (isFileOpen) floorMap.moveRobotEast(); } private void moveRobotWest(object sender, EventArgs e) { /*calls FloorMap.MoveRobotWest with the the ability to move target blocks This is used for manual movement with the arrow keys or buttons*/ if (isFileOpen) floorMap.moveRobotWest(); } private void fileReset(object sender, EventArgs e) { //resets data from the file resetRobotData(); floorMap.Open(fileName); } private void rtbLogAutoScroll(object sender, EventArgs e) { /* This is a funciton I found on stackoverflow which automatically scrolls the rich text box I use to log the moves to the most recent log entry. If this isn't included it will become very hard to track the output to the log. function written by Omar Mahili on stackoverflow.com Link: http://stackoverflow.com/questions/9416608/rich-text-box-scroll-to-the-bottom-when-new-data-is-written-to-it */ // set the current caret position to the end rtbLog.SelectionStart = rtbLog.Text.Length; // scroll it automatically rtbLog.ScrollToCaret(); } private void drawGridLines() { /* this draws grid lines to make the floor map easier to distinguish tiles*/ int tileSize = calculateTileSize(); for (int row = 0; row <= totalRows; row++) { //draws horizontal gridlines int x = 0; int y = calculateCoordinate(row, tileSize); imgFloorGraphics.FillRectangle(brushGrid, x, y, totalColumns * tileSize, 1); } for (int column = 0; column <= totalColumns; column++) { //draws vertical gridlines int x = calculateCoordinate(column, tileSize); int y = 0; imgFloorGraphics.FillRectangle(brushGrid, x, y, 1, totalRows * tileSize); } }//end drawGrid private void pnlFloorPlanPaint(object sender, PaintEventArgs e) { /*built from Connect Four Program from CS350*/ Graphics g = pnlFloorPlan.CreateGraphics(); g.DrawImage(imgFloor, 0, 0); }//end pnlFloorPlanPaint private void errorTryingToMoveRobot(object sender, EventArgs e) { //this was a function I used to debug when I would have an error with trying to move the robot //MessageBox.Show("Error trying to move robot. Stopping now"); } private void robotReachedDestination(object sender, EventArgs e) { //I was using this function to track whenthe robot reach its goal destination I was also going to use this //to show the final output path. I didn't complete that functionality though //MessageBox.Show("Robot reached goal location"); isComplete = true; } protected override bool IsInputKey(Keys keyData) { //This function allows the arrowkeys to be used as input keys. //Without this the arrow keys would not fire events that I could use for movement //from "alpha" on stackOverflow.com //Link: http://stackoverflow.com/questions/1646998/up-down-left-and-right-arrow-keys-do-not-trigger-keydown-event switch (keyData) { case Keys.Right: case Keys.Left: case Keys.Up: case Keys.Down: return true; case Keys.Shift | Keys.Right: case Keys.Shift | Keys.Left: case Keys.Shift | Keys.Up: case Keys.Shift | Keys.Down: return true; } return base.IsInputKey(keyData); } protected override void OnKeyDown(KeyEventArgs e) { //this assigns the functionality to the key that was pressed. //from "alpha" on stackOverflow.com edited by "MicroVirus" //Additions by Bryan Kelly for WASD to have the same functionality as up,left,down and right respectivly //Link: http://stackoverflow.com/questions/1646998/up-down-left-and-right-arrow-keys-do-not-trigger-keydown-event base.OnKeyDown(e); if (manualMovementEnabled) { switch (e.KeyCode) { case Keys.A: case Keys.Left: moveRobotWest(this, e); break; case Keys.D: case Keys.Right: moveRobotEast(this, e); break; case Keys.W: case Keys.Up: moveRobotNorth(this, e); break; case Keys.S: case Keys.Down: moveRobotSouth(this, e); break; } } } private void toggleManualMovement(object sender, EventArgs e) { //this allows the user to move the robot block with the arrow keys or wasd //I set some flags here and either hide or show the manual control buttons if (manualMovementEnabled) { manualMovementEnabled = false; showHideManualControls(false); tsmiEnableManualMovementToggle.Text = "Enable Manual Movement"; } else { manualMovementEnabled = true; showHideManualControls(true); tsmiEnableManualMovementToggle.Text = "Disable Manual Movement"; } } private void showHideManualControls(bool state) { //toggles either true or false for whether the manual controls should be shown/hidden btnMoveEast.Enabled = state; btnMoveWest.Enabled = state; btnMoveNorth.Enabled = state; btnMoveSouth.Enabled = state; btnMoveEast.Visible = state; btnMoveWest.Visible = state; btnMoveNorth.Visible = state; btnMoveSouth.Visible = state; if (state) { disableAIButtons(); } } private void btnClickCancelThreads(object sender, EventArgs e) { //event for the stop button when a search is executing disableAIButtons(); } private void testDLSRobot(object sender, EventArgs e) { //calls the itterative deepeining depth limited search... I abbreviate it to depth limited search and then again to DLS //this one is for just the robot block. This requires a new thread enableAIButtons(); BackgroundWorker bw = new BackgroundWorker(); bw.WorkerReportsProgress = true; bw.DoWork += new DoWorkEventHandler( delegate (object o, DoWorkEventArgs args) { testAI.depthLimiteSearchRobotTest(floorMap, goalRow, goalColumn); }); bw.RunWorkerAsync(); } private void testDFSRobot(object sender, EventArgs e) { //Executes the depth first search for the robot block. It does not allow movement of target blocks //requires a new thread to be created stackMovesToFinish = new Stack(); enableAIButtons(); BackgroundWorker bw = new BackgroundWorker(); bw.WorkerReportsProgress = true; bw.DoWork += new DoWorkEventHandler( delegate (object o, DoWorkEventArgs args) { testAI.depthFirstSearchRobotTest(floorMap, goalRow, goalColumn); }); bw.RunWorkerAsync(); } private void testDFSTarget(object sender, EventArgs e) { //executes the depth first search for the target. Will execute until all target blocks have reached a goal //stackMovesToFinish = new Stack(); enableAIButtons(); BackgroundWorker bw = new BackgroundWorker(); bw.WorkerReportsProgress = true; bw.WorkerSupportsCancellation = true; bw.DoWork += new DoWorkEventHandler( delegate (object o, DoWorkEventArgs args) { testAI.depthFirstSearchTarget(floorMap); }); bw.RunWorkerAsync(); } private void testIterativeDeepeningDFSTarget(object sender, EventArgs e) { /*executes the iterative deepening depth limited search. requires a new thread */ enableAIButtons(); BackgroundWorker bw = new BackgroundWorker(); bw.WorkerReportsProgress = true; bw.WorkerSupportsCancellation = true; bw.DoWork += new DoWorkEventHandler( delegate (object o, DoWorkEventArgs args) { testAI.depthLimitedSearchTarget(floorMap); }); bw.RunWorkerAsync(); } private void enableAIButtons() { /*shows the controls for the search. These include the stop button and the sleep timer*/ showHideManualControls(false); lblSleepCount.Visible = true; btnCancelThreads.Visible = true; btnCancelThreads.Enabled = true; nudSleepTimer.Enabled = true; nudSleepTimer.Visible = true; testAI.AbortThread = false; } private void disableAIButtons() { /*hides the contorls for the search. These include the stop button and the sleep timer*/ lblSleepCount.Visible = false; btnCancelThreads.Visible = false; btnCancelThreads.Enabled = false; nudSleepTimer.Enabled = false; nudSleepTimer.Visible = false; testAI.AbortThread = true; } private void nudSleepTimerChange(object sender, EventArgs e) { //updates the sleep timer in the testAI class when the sleep timer numaric up down control is changed testAI.SleepCount = Convert.ToInt32(nudSleepTimer.Value); } private void resizePanel(object sender, EventArgs e) { /*resizes the panel and redraws the grid. The numaric values were simply guess and check to make sure that the grid stays within the bounds of the window. If these numbers are removed some of the controls will end up off screen. Also need to make sure that some of these values are not less than 0, that will cause an error otherwise. */ if (Size.Width - pnlFloorPlan.Location.X - 15 > 0 && Size.Height - pnlFloorPlan.Location.Y - 40 > 0) { pnlFloorPlan.Width = Size.Width - pnlFloorPlan.Location.X - 15; pnlFloorPlan.Height = Size.Height - pnlFloorPlan.Location.Y - 40; drawEntireFloorMap(); } if(Size.Height - rtbLog.Location.Y - 50 > 0) { rtbLog.Height = Size.Height - rtbLog.Location.Y - 50; } } public void ShowFinalSolution(object sender, EventArgs e) { //this is a function I didn't fully implement yet. my intenetion was to allow the user to see the finished path alone. //my plan was to create a new thread then run the finished solution. enableAIButtons(); BackgroundWorker bw = new BackgroundWorker(); bw.WorkerReportsProgress = true; bw.WorkerSupportsCancellation = true; bw.DoWork += new DoWorkEventHandler( delegate (object o, DoWorkEventArgs args) { runSolution(); }); bw.RunWorkerAsync(); } public void runSolution() { /*Not implemented in finished program. This was going to go through a stack containing only the finished path.*/ if (isComplete) { rtbLog.Text = ""; Stack tempStack = new Stack(); while (stackMovesToFinish.Count > 0) { if (testAI.AbortThread) return; System.Threading.Thread.Sleep(Convert.ToInt32(nudSleepTimer.Value)); tempStack.Push(stackMovesToFinish.Pop()); switch (tempStack.Peek()) { case 'N': floorMap.moveRobotNorth(); break; case 'S': floorMap.moveRobotSouth(); break; case 'E': floorMap.moveRobotEast(); break; case 'W': floorMap.moveRobotWest(); break; } } while (tempStack.Count > 0) { stackMovesToFinish.Push(tempStack.Pop()); } } } private void showAllToolStripMenuItem_Click(object sender, EventArgs e) { /*This allows the user to easily see the difference between empty spaces and wall spaces this is ideal for the user to be able to predict what they think the search should be doing next. */ currentBrush[EMPTY] = brushEmpty; drawEntireFloorMap(); } private void hideAllToolStripMenuItem_Click(object sender, EventArgs e) { /*This makes all of the empty spaces the same color as the wall spaces. I thought this would be a way to illustrate to the user what information the algorithm is aware of. */ currentBrush[EMPTY] = brushWall; drawEntireFloorMap(); } private void showVisitedToolStripMenuItem_Click(object sender, EventArgs e) { //this is just a small function I added last minute to allow the user to change the //empty spaces to visted to demonstrate what an uninformed search might be like for a comptuer showVisitedRobot = true; } private void hideVisitedToolStripMenuItem_Click(object sender, EventArgs e) { //sets the visited flag to false so empty spaces appear as empty spaces showVisitedRobot = false; } private void disableShowVisitedRobot(object sender, EventArgs e) { //Sets the showVisitedRobot bool to false. Triggered from the TestAIFunctions class //Used to disable the marks to show that the robot has visited a space showVisitedRobot = false; } private void enableShowVisitedRobot(object sender, EventArgs e) { //Sets the showVisitedRobot bool to true. Triggered from the TestAIFunctions class //Used to enable the marks to show that the robot has visited a space showVisitedRobot = true; } private void resetRobotVisitedBlocks(object sender, EventArgs e) { //Redraws the floormap to reset the blocks that have been marked visited on the visual drawEntireFloorMap(); } } }