├── .gitignore ├── LICENSE ├── README.org ├── canvas.nim ├── display.nim ├── wireworld-nim.gif ├── wireworld.html └── wireworld.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | /display 3 | /wireworld 4 | /display.exe 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 John Honaker 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Wireworld 2 | 3 | [[./wireworld-nim.gif]] 4 | 5 | Everyone knows the classic example of cellular automata, Conway's Game of Life. Instead of rehashing an implementation of that, I wanted to build something just a little bit different while learning the [[http://www.nim-lang.org][Nim language]]. 6 | 7 | I decided to implement another type of cellular automata, [[https://en.wikipedia.org/wiki/Wireworld][Wireworld]], first conceived by Brian Silverman in 1987. The code for this could easily be modified to produce Conway's Game of Life by adjusting the =State= type and the =process(world: ref World)= procedure. 8 | 9 | ** Build 10 | **** C target 11 | Build the project from source by typing =nim c display.nim= into your terminal. You must have Nim installed already, as well as the SDL 2 module. You can install the SDL 2 module by running =nimble install sdl2=. 12 | 13 | Run the project by calling =./display= in the appropriate directory. 14 | 15 | You may need do create a =libSDL2.so= symlink if you get the following error: =could not load: libSDL2.so= 16 | 17 | =/usr/lib/x86_64-linux-gnu$ ln -s libSDL2-2.0.so.0 libSDL2.so= 18 | 19 | **** JavaScript target 20 | Nim =0.17.0= is required, it won't build with Nim =0.15.2= (eg. which is the distributed version on the latest Ubuntu, [[https://launchpad.net/~jonathonf/+archive/ubuntu/nimlang/+sourcepub/7822957/+listing-archive-extra][get the 0.17 .deb]]) 21 | 22 | Install html5_canvas: =nimble install html5_canvas= 23 | 24 | Build the JS: =nim js canvas.nim= 25 | 26 | Open the webpage: =firefox wireworld.html= 27 | 28 | ** Description 29 | 30 | This automata simulates a simple abstraction of electricity. 31 | 32 | It has four states: 33 | - Ground 34 | - Wire 35 | - Electron head 36 | - Electron tail 37 | 38 | The propagation rules are: 39 | - Ground -> Ground 40 | - Electron head -> Electron tail 41 | - Electron tail -> Wire 42 | - If Wire has exactly 1 or 2 neighbors == Electron Head: 43 | - Wire -> Electron Head 44 | - Otherwise: 45 | - Wire -> Wire 46 | 47 | ** Implementation 48 | 49 | This implementation is built in Nim using the SDL 2 library. 50 | 51 | Thanks to GitHub user [[https://github.com/tylorr][tylorr]] for taking it upon himself to make a web frontend for this project as well. 52 | 53 | *** Features and Instructions 54 | 55 | Clicking on the map changes the tile under the cursor to the current drawmode's type. 56 | 57 | The info bar at the bottom of the window indicates the currently selected draw mode on the left half, and the paused/play state of the simulation on the right half. 58 | 59 | There are four drawing modes, one for each state: 60 | - =A=: Ground mode 61 | - =S=: Wire mode 62 | - =D=: Electron head mode 63 | - =F=: Electron tail mode 64 | 65 | In addition there are a few other keybindings: 66 | - =Up=: Increases the speed of the simulation by 1 tick per second (Min: 1, Max: 60) 67 | - =Down=: Decreases the speed of the simulation by 1 tick per second 68 | - =Q=: Quit Wireworld 69 | - =R=: Clear the current world 70 | - =P=: Pause or Play the simulation (can draw while paused or playing) 71 | 72 | *** Future Additions 73 | - [ ] Undo 74 | - [ ] Save and load worlds from file 75 | -------------------------------------------------------------------------------- /canvas.nim: -------------------------------------------------------------------------------- 1 | import dom, jsconsole, times 2 | import html5_canvas 3 | import wireworld 4 | 5 | const 6 | TileSize = 10 7 | MinTicksPerSecond = 1 8 | MaxTicksPerSecond = 60 9 | WindowSize = WorldSize * TileSize 10 | BottomBarHeight = 1 11 | black = rgb(0, 0, 0) 12 | green = rgb(0, 255, 0) 13 | red = rgb(255, 0, 0) 14 | groundColor = rgb(28, 20, 14) 15 | wireColor = rgb(234, 225, 93) 16 | headColor = rgb(43, 145, 255) 17 | tailColor = rgb(255, 57, 43) 18 | 19 | var TicksPerSecond = 10 20 | 21 | proc clampSpeedChange(speed, delta: int): int = 22 | if speed + delta < 1: 23 | result = MinTicksPerSecond 24 | elif speed + delta > MaxTicksPerSecond: 25 | result = MaxTicksPerSecond 26 | else: 27 | result = speed + delta 28 | 29 | type 30 | Input {.pure.} = enum 31 | none, reset, 32 | groundmode, wiremode, headmode, tailmode, 33 | place, 34 | pauseplay, 35 | speedup, speeddown 36 | 37 | DrawMode = enum 38 | GroundMode, 39 | WireMode, 40 | HeadMode, 41 | TailMode 42 | 43 | Game = ref object 44 | inputs: array[Input, bool] 45 | mx, my: int 46 | tx, ty: int 47 | canvas: Canvas 48 | context: CanvasRenderingContext2D 49 | world: World 50 | prevFrame: World 51 | drawmode: DrawMode 52 | paused: bool 53 | 54 | Rect* = tuple[x, y, w, h: float] 55 | 56 | proc newGame(canvas: Canvas): Game = 57 | new result 58 | 59 | result.world = newWorld() 60 | result.prevFrame = newWorld(head) 61 | 62 | result.drawmode = WireMode 63 | result.paused = false 64 | result.canvas = canvas 65 | result.context = canvas.getContext2D() 66 | 67 | proc color(tile: State): cstring = 68 | case tile: 69 | of ground: groundColor 70 | of wire: wireColor 71 | of head: headColor 72 | of tail: tailColor 73 | 74 | proc fillRect(ctx: CanvasRenderingContext2D, rect: Rect) = 75 | ctx.fillRect(rect.x, rect.y, rect.w, rect.h) 76 | 77 | proc strokeRect(ctx: CanvasRenderingContext2D, rect: Rect) = 78 | ctx.strokeRect(rect.x, rect.y, rect.w, rect.h) 79 | 80 | proc screenToWorld(x, y: int): tuple[x, y: int] = (x div TileSize, y div TileSize) 81 | 82 | proc keyToInput(key: int): Input = 83 | case key: 84 | of 65: Input.groundmode #A 85 | of 83: Input.wiremode #S 86 | of 68: Input.headmode #D 87 | of 70: Input.tailmode #F 88 | of 82: Input.reset #R 89 | of 80: Input.pauseplay #P 90 | of 38: Input.speedup #up 91 | of 40: Input.speeddown #down 92 | else: Input.none 93 | 94 | proc processKeyUp(game: var Game, keyCode: int) = 95 | case keyCode.keyToInput: 96 | of Input.pauseplay: 97 | game.paused = not game.paused 98 | of Input.groundmode: 99 | game.drawmode = GroundMode 100 | of Input.wiremode: 101 | game.drawmode = WireMode 102 | of Input.headmode: 103 | game.drawmode = HeadMode 104 | of Input.tailmode: 105 | game.drawmode = TailMode 106 | of Input.reset: 107 | game = newGame(game.canvas) 108 | else: discard 109 | 110 | proc processKeyDown(game: var Game, keyCode: int) = 111 | case keyCode.keyToInput: 112 | of Input.speedup: 113 | TicksPerSecond = TicksPerSecond.clampSpeedChange(1) 114 | of Input.speeddown: 115 | TicksPerSecond = TicksPerSecond.clampSpeedChange(-1) 116 | else: discard 117 | 118 | proc drawState(game: Game): State = 119 | case game.drawmode: 120 | of GroundMode: ground 121 | of WireMode: wire 122 | of HeadMode: head 123 | of TailMode: tail 124 | 125 | proc processClicks(game: var Game) = 126 | if game.inputs[Input.place]: 127 | game.world.get(game.tx, game.ty) = game.drawState 128 | 129 | proc renderTile(game: Game, x, y: int) = 130 | let 131 | tileState = game.world.get(x, y) 132 | prevState = game.prevFrame.get(x,y) 133 | 134 | if tileState == prevState: return 135 | 136 | game.prevFrame.get(x,y) = tileState 137 | 138 | let 139 | tileColor = tileState.color 140 | tileRect: Rect = (float(x * TileSize), float(y * TileSize), float(TileSize), float(TileSize)) 141 | 142 | # Draw the tile background 143 | game.context.fillStyle = tileColor 144 | game.context.fillRect(tileRect) 145 | # Draw the outline 146 | game.context.strokeStyle = black 147 | game.context.strokeRect(tileRect) 148 | 149 | proc renderDrawMode(game: Game) = 150 | let 151 | x: float = 0 152 | y: float = WorldSize 153 | width = WorldSize / 2.0 154 | barRect: Rect = ( 155 | x * TileSize, 156 | y * TileSize, 157 | width * TileSize, 158 | float(BottomBarHeight * TileSize)) 159 | 160 | game.context.fillStyle = game.drawState.color 161 | game.context.fillRect(barRect) 162 | 163 | proc renderPauseState(game: Game) = 164 | let 165 | x = WorldSize / 2.0 166 | y: float = WorldSize 167 | width = WorldSize / 2.0 168 | color = case game.paused: 169 | of true: red 170 | of false: green 171 | barRect: Rect = ( 172 | x * TileSize, 173 | y * TileSize, 174 | width * TileSize, 175 | float(BottomBarHeight * TileSize) 176 | ) 177 | 178 | game.context.fillStyle = color 179 | game.context.fillRect(barRect) 180 | 181 | proc renderBottomBar(game: Game) = 182 | game.renderDrawMode 183 | game.renderPauseState 184 | 185 | proc renderWorld(game: Game) = 186 | for x in 0.. MaxTicksPerSecond: 21 | result = MaxTicksPerSecond 22 | else: 23 | result = speed + delta 24 | 25 | type SDLException = object of Exception 26 | 27 | type 28 | Input {.pure.} = enum 29 | none, reset, exit, 30 | groundmode, wiremode, headmode, tailmode, 31 | place, 32 | pauseplay, 33 | speedup, speeddown 34 | 35 | DrawMode = enum 36 | GroundMode, 37 | WireMode, 38 | HeadMode, 39 | TailMode 40 | 41 | Game = ref object 42 | inputs: array[Input, bool] 43 | mx, my: int 44 | tx, ty: int 45 | renderer: RendererPtr 46 | world: World 47 | drawmode: DrawMode 48 | paused: bool 49 | dirty: seq[bool] 50 | 51 | proc newGame(renderer: RendererPtr): Game = 52 | new result 53 | 54 | result.world = newWorld() 55 | 56 | newSeq(result.dirty, WorldSize * WorldSize) 57 | for x in 0.. 2 | 3 | 4 | Wireworld 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /wireworld.nim: -------------------------------------------------------------------------------- 1 | type 2 | State* = enum 3 | ground, wire, head, tail 4 | 5 | World* = seq[State] 6 | 7 | const WorldSize* = 75 8 | 9 | template get*[T](collection: openarray[T], x, y: int): T = 10 | collection[x + WorldSize * y] 11 | 12 | proc newWorld*(state = ground): World = 13 | newSeq(result, WorldSize * WorldSize) 14 | for idx in 0..< (WorldSize * WorldSize): 15 | result[idx] = state 16 | 17 | proc `$`*(world: World): string = 18 | result = "" 19 | for y in 0..= 0 and i < WorldSize and j >= 0 and j < WorldSize: 34 | result[world.get(i, j)] += 1 35 | 36 | proc newState(world: World, x, y: int): State = 37 | # Returns the new state for the cell after its update 38 | # 39 | # The update algorithm is: 40 | # Ground -> Ground 41 | # Head -> Tail 42 | # Tail -> Wire 43 | # Wire -> Head if '1 or 2 neighbors are head' else Wire 44 | 45 | let 46 | current = world.get(x, y) 47 | neighbors = world.neighbors(x, y) 48 | headcount = neighbors[head] 49 | 50 | case current: 51 | of ground: result = ground 52 | of head: result = tail 53 | of tail: result = wire 54 | of wire: 55 | if headcount == 1 or headcount == 2: 56 | result = head 57 | else: 58 | result = wire 59 | 60 | proc process*(world: var World) = 61 | # Updates the entire world according to the automata rules. 62 | 63 | # We must copy world first though, otherwise we'll get incorrect updating 64 | var newworld = world 65 | for x in 0..