Developer Diary - mikestockdale/robot-world GitHub Wiki
Starting with an empty code base. commit.
I begin exploring a few concepts of RobotWorld. There are bots, and bots have a location in a 2-D world . Also there's an idea that RobotWorld will be a client/server app. Looking at Ron's code, there 's a method to add a bot, so I'll start there.
I'm starting with a single file, main.rkt. When I understand some concepts better, I'll split them into separate files.
I have a choice in Racket: simple types called structs or full-blown classes. I'm choosing the first because, well, they're simpler and I'm a Racket novice. Also, I want to lean back from OO on this project and try something different. I don't want a Java design written in Racket.
Racket has a nice feature where you can include a test sub-module in a file. I like having tests and implementation in the same file when possible.
So I have a location struct and a bot struct and a test creating a bot at a location. commit.
(I want to implement just enough to pass the test - and end up with a add-bot! method that just returns a location. What!? Things are a bit confusing as I figure out how to get started. Spoiler: it gets better soon.)
Step: bots also have ids. So I add an id field to the bot structure and add-bot! now returns a new bot. commit
Step: bots can move. I add a world structure with a bots field that is a hash table of bot ids and locations. The add-bot! method adds the new bot to the hash table. (Finally the name makes sense!) A move-bot! method finds the old location, calculates a new location, and updates the bots table. A connect-world method creates the world structure. commit
(BTW, those method names ending in ! - that's a Racket idiom to say they mutate something.)
I introduce some methods to make the code more readable: move-location, place-bot!, and locate-bot. Now move-bot! reads more clearly. commit
Trying to design a client-server API as the code evolves isn't going anywhere. The "client" methods are just wrappers of the "server" methods. So I drop the idea for now. I'm encouraged that Ron came to the same conclusion. commit
Step: create bots at a requested location. Currently, all bots are created with a default location, which isn't too useful. So I add a location argument to the add-bot! method. commit
Step: each bot is assigned a unique id. Currently, all bots are created with a default id. So the world struct gets a next-id field, and add-bot! increments it. commit
Step: bots move in four directions. Bots can jump to any location - that's a little too much! I define 4 directions; north, south, east, and west. Now the move-bot! method takes a direction argument instead of delta-x and delta-y. commit
Time to organize the code a bit. I split main.rkt into location.rkt and world.rkt. commit
Step: draw the world as strings. This is a first step to being able to visualize the bots' locations on a 2-D map. I initialize an array of blank strings, then place each bot as a letter 'O' at its location. commit
I investigate some GUI basics in Racket. From this, I create a simple GUI demo that shows some bots moving across the world map. commit
Step: check for valid locations. I change the 4 defined directions to be indexes to an array of location offsets. So now only these 4 offsets can be used in move-location! I add a size field to the world structure and a method is-valid-location? to see if a location is outside the boundaries of the world map. Then add-bot! and move-location! can check the requested location is valid. commit
The viewer app has a few problems to be fixed. The window is locked while the animation is running, so I change it to use a timer instead of a loop with sleeps. I add an event handler to pause and resume the animation when the window is clicked.
I want to start to separate the GUI logic from the bot actions, so I add an action structure with fields bot-id and procedure, to run a bot action. I add an actions structure with a world field and a list of actions. The make-actions method creates the actions and the perform-actions method performs each action in the list. commit
I like the action concept. So I separate it into a new file. commit
(I don't have a clear idea yet about how to split the code into files. I'm mostly relying on my OO experience: "I'd probably create a new class here".)
Step: performing an action list generates a new list. In a non-trivial scenario, the action list will need to change over time. So each action procedure returns an action, and perform-actions builds them into a new list. An action procedure can just return its input, if there's no changes. I create a simple-action method to wrap my existing action procedures. The viewer uses a new list for each step on the animation. commit
Step: a wandering bot may change direction. A wandering bot moves in one direction, and may change direction at random. I add a wandering structure that inherits from the action structure, with a direction field and a direction-change-chance. The wander method checks a random number to determine whether to change to a new random direction. It then moves the bot in the current direction. commit
Step: a wandering bot changes direction if it can't move. This happens when the bot reaches an edge of the world. I change the wander method to check if the bot has moved to a new location. If it hasn't, the wander method returns a new wandering structure with a new direction. I fix a bug from the previous commit - if there was a random direction change, this wasn't being saved in a new wandering structure. To tidy up, I create a new-direction method in location.rkt. I add a make-wandering method to set up a wandering bot, and change the viewer app to show a couple of wandering bots. commit
Step: add blocks to the world. The world will contains other types of entities, like blocks. I add an entity structure, with id, type and location fields. I add an entities field to the world structure, to be a hash table of entities. The place-entity!, entity-ref and add-entity! methods are similar to the existing bot methods, generalized to handle other entity types. (I'll remove the bots table and the bot methods soon, once the entity replacements are tested.) commit
Step: put blocks and bots in the same table. I remove the bots field in the world structure and change the bot methods to use the entities field. I add a type-symbols array to draw each entity type with a different character. I add a block to the viewer app. commit
Step: replace the old bot methods with new entity methods. I replace all calls to add-bot! with add-entity!, and locate-bot with entity-location and entity-ref. Then I remove the bot methods and the bot structure. I rename a couple of methods, to make the code clearer: new-direction to change-direction, and change-direction? to direction-change?. commit
Step: re-organize for client-server: It's time to start thinking about how client apps will direct the bots in the server world. As a first step, I create a server structure with a world field. This will represent a server facade that client code must use, instead of accessing the world module directly. I add add-bot! and move-bot! methods for client code to use. Initially these are just wrappers for the world methods. I create entity.rkt, with definitions that both client and server will use. I change all the client code to use the server and entity modules instead of the world module. commit
Step: neighbors are nearby entities. Clients are going to need a list of nearby entities for each bot. I create a distance method in the location module and a neighbors method in the world module, to make a list of entities at distance of one from a bot. commit
Step: Server adds neighbors to response. The server methods will now also return a list of neighbors, to provide more information for the callers. I create an info structure with bot and neighbors fields. The add-bot! and move-bot! methods populate and return this structure. I change the action structure to include an info field so the action procedures can use it. commit
Step: load block onto bot: If a bot is next to a block, it will pick up the block. I add a cargo field to the entity structure. I add a load-entity! method in the world module. This removes the block from the entites table and puts it the bot's cargo field. I add a load-block! method in the server module. I change the wander method to check for a nearby block, and to load it. I change the entity-symbol method to use different characters for empty and laden bots. I add some more blocks to the viewer app. commit
Step: block is dropped. I add a drop-entity! method to the world module. This removes the block from the bot's cargo field and puts it back into the entities table, next to the bot. commit
Step: rename load to take. Ron uses the terminology "drop" and "take" so I rename the load methods to "take-". commit
Step: drop a block near another. I change the wander method in the wandering module to call a drop-block method if the bot has blocks nearby and is laden with a block. The drop block method finds a direction from the bot to an empty location, and drops the block in that direction. To check all directions for a free one, I add an all-directions list in the location module. I add a drop-block! method in the server module that calls the drop-entity! method. commit
Step: delay taking a block after dropping one. To avoid taking a block that's just been dropped, the bot should just wander for several turns. I add a take-delay field to the wandering structure which is set to 10 after dropping a block. The take-delay is decremented with each move. The wander method only looks at nearby blocks when the take-delay is zero. commit
Step: draw entities directly. Drawing is a bit cumbersome as the draw-world method builds an array of strings and the viewer app has to break them apart. I replace the draw-world method with a draw-entities method that just calls a viewer drawing procedure for each entity. commit
Step: create a direction module. The direction code has been accumulating in the location module. There are two separate concepts, direction and location, that have been conflated, because they both have x and y fields. So I create "direction.rkt" and move in the direction code. commit
Step: create a bot-info module. There's an info structure in the server module, that represents the data returned from the server bot methods. I rename it to bot-info and move it into "bot-info.rkt". (I'm not really happy with that name, but I can't think of a better one right now). There's a find-free-direction in the wandering module that uses only the bot-info fields, so I move it in, too. commit
Step: separate domain and GUI code. The viewer app contains robot world code and GUI code. I split it into "local-viewer.rkt" and "viewer-rkt". (I call the new app "local viewer" because I'm thinking there'll be a "web viewer" when the world code is running inside a web server.) commit
Step: use a nicer font. The viewer symbols don't line up perfectly. I find a font that displays them better. commit
I investigate and write spike code for Racket web servers, HTTP clients and generic interfaces, to help me understand how to implement the client/server version of Robot World.
Step: define generic server. I create a generic interface in "gen-server.rkt". I move the current server implementation into "local-server.rkt". The existing "server.rkt" becomes just a wrapper that exposes the required methods. (This lets me do polymorphism in Racket - the local-server implementation and a future remote-server implementation are hidden from the client code.) commit
Step: send and receive structures as lists. I'll need to pass structures over the wire between client and server. There is a serialization capability in Racket, but given all the other new stuff I'm dealing with, I don't have bandwidth to learn it too. I only have three structures to pass: bot-info, entity and location, so I write simple methods for each structure to convert to and from lists. commit
Clean up: I have some arguments mis-named, an unused require, and I pick a different function for the viewer. commit
Step: remote server uses remote world. I create a remote-server module to send requests from the client and a remote-world module to process requests on the web server. In order to test these modules, I use a test double to replace the actual network traffic between them. commit
Step: move, drop, and take remote. The previous step implemented a client/server add-bot! method. I now add drop-block!, move-bot!, and take-block!. I also discover and fix a bug with serializing an entity with cargo. commit
Step: implement client, server, and viewer, Finally I can complete the client/server implementation. I add web-server, which uses URL dispatching to call methods in the remote-world. I add web-client, which connects to the web server and keeps a couple bots wandering around the remote world. I add web-viewer, which connects to the web server and calls the draw method to display the entities from the remote world. commit
I add some blocks to the remote world and change the viewer titles, before running a demo. commit
Step: build world for tests. Some of the wandering tests seem rather cumbersome. I move find-nearby-blocks to the bot-info module and test it. I create a build-world method to streamline the set up of a world for each test. Some improvement but I'm not completely satisfied with the result, I'll look at it again later. commit
Step: don't drop outside the world. There's no check that the drop location is valid. So I add a size field to the bot-info structure so I can check for invalid locations in the find-free-location method. [commit] (https://github.com/mikestockdale/robot-world/commit/1dd99308789f0c5f11ccdb654eb3a1a4b9b182a3)