├── .gitignore ├── README.md ├── hyperdungeon.js ├── mock-server.js ├── package.json └── util.js /.gitignore: -------------------------------------------------------------------------------- 1 | #~top ignores~ 2 | node_modules/ 3 | *.css 4 | *.vim 5 | *bundle*.js 6 | /html/*.html 7 | *.swo 8 | config.conf 9 | config.js 10 | *.txt 11 | words.db/ 12 | 13 | 14 | ################# 15 | ## Eclipse 16 | ################# 17 | *.pydevproject 18 | .project 19 | .metadata 20 | bin/ 21 | tmp/ 22 | *.tmp 23 | *.bak 24 | *.swp 25 | *~.nib 26 | local.properties 27 | .classpath 28 | .settings/ 29 | .loadpath 30 | 31 | # External tool builders 32 | .externalToolBuilders/ 33 | 34 | # Locally stored "Eclipse launch configurations" 35 | *.launch 36 | 37 | # CDT-specific 38 | .cproject 39 | 40 | # PDT-specific 41 | .buildpath 42 | 43 | 44 | ################# 45 | ## Visual Studio 46 | ################# 47 | 48 | ## Ignore Visual Studio temporary files, build results, and 49 | ## files generated by popular Visual Studio add-ons. 50 | 51 | # User-specific files 52 | *.suo 53 | *.user 54 | *.sln.docstates 55 | 56 | # Build results 57 | 58 | [Dd]ebug/ 59 | [Rr]elease/ 60 | x64/ 61 | build/ 62 | [Bb]in/ 63 | [Oo]bj/ 64 | 65 | # MSTest test Results 66 | [Tt]est[Rr]esult*/ 67 | [Bb]uild[Ll]og.* 68 | 69 | *_i.c 70 | *_p.c 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.pch 75 | *.pdb 76 | *.pgc 77 | *.pgd 78 | *.rsp 79 | *.sbr 80 | *.tlb 81 | *.tli 82 | *.tlh 83 | *.tmp 84 | *.tmp_proj 85 | *.log 86 | *.vspscc 87 | *.vssscc 88 | .builds 89 | *.pidb 90 | *.log 91 | *.scc 92 | 93 | # Visual C++ cache files 94 | ipch/ 95 | *.aps 96 | *.ncb 97 | *.opensdf 98 | *.sdf 99 | *.cachefile 100 | 101 | # Visual Studio profiler 102 | *.psess 103 | *.vsp 104 | *.vspx 105 | 106 | # Guidance Automation Toolkit 107 | *.gpState 108 | 109 | # ReSharper is a .NET coding add-in 110 | _ReSharper*/ 111 | *.[Rr]e[Ss]harper 112 | 113 | # TeamCity is a build add-in 114 | _TeamCity* 115 | 116 | # DotCover is a Code Coverage Tool 117 | *.dotCover 118 | 119 | # NCrunch 120 | *.ncrunch* 121 | .*crunch*.local.xml 122 | 123 | # Installshield output folder 124 | [Ee]xpress/ 125 | 126 | # DocProject is a documentation generator add-in 127 | DocProject/buildhelp/ 128 | DocProject/Help/*.HxT 129 | DocProject/Help/*.HxC 130 | DocProject/Help/*.hhc 131 | DocProject/Help/*.hhk 132 | DocProject/Help/*.hhp 133 | DocProject/Help/Html2 134 | DocProject/Help/html 135 | 136 | # Click-Once directory 137 | publish/ 138 | 139 | # Publish Web Output 140 | *.Publish.xml 141 | *.pubxml 142 | 143 | # NuGet Packages Directory 144 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 145 | #packages/ 146 | 147 | # Windows Azure Build Output 148 | csx 149 | *.build.csdef 150 | 151 | # Windows Store app package directory 152 | AppPackages/ 153 | 154 | # Others 155 | sql/ 156 | *.Cache 157 | ClientBin/ 158 | [Ss]tyle[Cc]op.* 159 | ~$* 160 | *~ 161 | *.dbmdl 162 | *.[Pp]ublish.xml 163 | *.pfx 164 | *.publishsettings 165 | 166 | # RIA/Silverlight projects 167 | Generated_Code/ 168 | 169 | # Backup & report files from converting an old project file to a newer 170 | # Visual Studio version. Backup files are not needed, because we have git ;-) 171 | _UpgradeReport_Files/ 172 | Backup*/ 173 | UpgradeLog*.XML 174 | UpgradeLog*.htm 175 | 176 | # SQL Server files 177 | App_Data/*.mdf 178 | App_Data/*.ldf 179 | 180 | ############# 181 | ## Windows detritus 182 | ############# 183 | 184 | # Windows image file caches 185 | Thumbs.db 186 | ehthumbs.db 187 | 188 | # Folder config file 189 | Desktop.ini 190 | 191 | # Recycle Bin used on file shares 192 | $RECYCLE.BIN/ 193 | 194 | # Mac crap 195 | .DS_Store 196 | 197 | 198 | ############# 199 | ## Python 200 | ############# 201 | 202 | *.py[co] 203 | 204 | # Packages 205 | *.egg 206 | *.egg-info 207 | dist/ 208 | build/ 209 | eggs/ 210 | parts/ 211 | var/ 212 | sdist/ 213 | develop-eggs/ 214 | .installed.cfg 215 | 216 | # Installer logs 217 | pip-log.txt 218 | 219 | # Unit test / coverage reports 220 | .coverage 221 | .tox 222 | 223 | #Translations 224 | *.mo 225 | 226 | #Mr Developer 227 | .mr.developer.cfg 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hyperdungeon 2 | a distributed mud experiment ontop of [hypercore](https://github.com/mafintosh/hypercore) & [hyperdb](https://github.com/mafintosh/hyperdb) 3 | 4 | ☠️ _a dire work-in-progress_ ☠️ 5 | ⚠️ _to potential adventurers: 6 | this is not only a work in progress for a distrbuted MUD, it is also built ontop of a distributed database that's currently without a stable release. tread warily, stranger!_ 7 | 8 | ## Tutorial 9 | #### `npm install && npm start` 10 | The above installs all the packages needed and runs the game. Currently you'll need at least another peer (that is, a hyperdungeon session/player) running to be able to do anything. 11 | 12 | When you start your id will be printed out as (but with your id instead!) 13 | `local key 94dc2d7a9801034e1159525c1bf29a893a5fdf7a4e998d65e17f44ed86868dd9` 14 | 15 | ### Commands 16 | #### `[n]orth|[w]est|[s]outh|[e]ast` 17 | #### `alias =` 18 | `alias me=94dc2d7a9801034e1159525c1bf29a893a5fdf7a4e998d65e17f44ed86868dd9` 19 | #### `warp =x,y` 20 | `warp 94dc2d7a9801034e1159525c1bf29a893a5fdf7a4e998d65e17f44ed86868dd9=1000,1000` 21 | #### `write ` 22 | `write global hey anyone else here?` 23 | #### `describe ` 24 | ``` 25 | describe you stand in the middle of a dark hallway. you hear whirring machines in the distance. the hyperlord is nowhere in sight. 26 | ``` 27 | #### `whereis ` 28 | `whereis cblgh` 29 | 30 | #### `reply ` 31 | replies to the person who most recently wrote to you 32 | 33 | ### Other Commands 34 | ##### `whoami|look|aliases|messages|help|exit` 35 | -------------------------------------------------------------------------------- /hyperdungeon.js: -------------------------------------------------------------------------------- 1 | var hyperdb = require("hyperdb") 2 | var Readable = require("stream").Readable 3 | var hyperdiscovery = require("hyperdiscovery") 4 | var readline = require("readline") 5 | var util = require("./util.js") 6 | var mock = require("./mock-server.js") 7 | // use peer-network to connect new peers to the distributed mud instance 8 | var peernet = require("peer-network") 9 | var network = peernet() 10 | var server = network.createServer() 11 | var local = util.local 12 | var db 13 | 14 | var rl = readline.createInterface({ 15 | input: process.stdin, 16 | output: process.stdout 17 | }) 18 | 19 | // META TODO: somehow allow people to just get this entire codebase as a dat itself 20 | 21 | function printHelp() { 22 | console.log("directions:\n\tnorth\n\tsouth\n\teast\n\twest") 23 | console.log("commands:\n\tlook\n\twhoami\n\twrite \n\treply \n\taliases\n\tmessages\n\twhereis \n\talias =\n\tdescribe \n\texit\n\twarp =x,y") 24 | } 25 | 26 | function split(input) { 27 | input = input.split(" ") 28 | var command = input.splice(0, 1)[0] // splice out the first command part 29 | return [command, input.join(" ")] // and keep the rest of the string 30 | } 31 | 32 | // before we do anything else, we try to connect to the peernet hyperdungeon server. 33 | // if that fails, we create an instance ourselves 34 | local.ready(function() { 35 | var localKey = local.key.toString("hex") 36 | // try to connect to an existing server 37 | var stream = network.connect("hyperdungeon") 38 | stream.write(localKey) // tell server our id 39 | 40 | // if that fails, start a server 41 | stream.on("error", function() { 42 | console.log("no such server found") 43 | mock("hyperdungeon", localKey).then(function(feeds) { 44 | start(feeds) 45 | }) 46 | }) 47 | 48 | // server replies with a list all of the instances that have connected to hyperdb 49 | // (including our key) 50 | stream.on("data", function (data) { 51 | var feeds = JSON.parse(data.toString()) 52 | start(feeds) 53 | }) 54 | 55 | function start(keys) { 56 | var feeds = util.join(keys, local.key.toString("hex")) 57 | db = hyperdb(feeds) 58 | db.ready(hyperdungeon) 59 | } 60 | }) 61 | 62 | function hyperdungeon() { 63 | var id = local.key.toString("hex") 64 | console.log("local key", id) 65 | 66 | var sw = hyperdiscovery(db, {live: true}) 67 | if (process.argv.indexOf("--sync") > -1) { 68 | db.feeds[0].download({start: 0, end: -1}) 69 | return 70 | } 71 | 72 | sw.on("connection", function(peer, type) { 73 | var peerId = peer.key.toString("hex") 74 | console.log("a new peer has joined, zarathystras's forces grow stronger") 75 | peer.on("close", function() { 76 | console.log("a peer has left, zarathystras's forces grow weaker") 77 | }) 78 | }) 79 | 80 | function getState(playerId) { 81 | var getPos = get(playerId + "/pos") 82 | var getAlias = get(playerId + "/aliases") 83 | return Promise.all([getPos, getAlias]).then(function(values) { 84 | // console.log(values) 85 | var pos = values[0] || {x: 0, y: 0} 86 | var aliases = values[1] || {} 87 | return {id: playerId, pos: pos, aliases: aliases} 88 | }) 89 | } 90 | 91 | function updatePos(player, oldPos) { 92 | var newPos = player.pos.x + "," + player.pos.y 93 | // move player 94 | return update(player.id + "/pos", player.pos).then(function() { 95 | // update old & new tile arrays 96 | return moveItem(oldPos + "/players", newPos + "/players", player.id) 97 | }) 98 | } 99 | 100 | function monitorMessages(channel) { 101 | var lastIndex = -1 102 | get(channel + "/messages").then(function(msgs) { 103 | msgs = msgs || [] 104 | lastIndex = msgs.length - 1 105 | // check for new messages every second 106 | setInterval(function() { 107 | var getMessages = get(channel + "/messages") 108 | var getAliases = get(id + "/aliases") // aliases belongs to this user, thus id is used and not channel 109 | Promise.all([getMessages, getAliases]).then(function(values) { 110 | var msgs, aliases 111 | msgs = values[0] || [] 112 | aliases = values[1] || {} 113 | // if we have a new message 114 | if (msgs.length > lastIndex) { 115 | // go through each new message 116 | for (var i = lastIndex + 1; i < msgs.length; i++) { 117 | // getting its contents & sender 118 | var msg = msgs[i] 119 | var sender = msg.sender 120 | // reverse lookup in our aliases for a nickname of the sender 121 | for (var key in aliases) { 122 | if (aliases[key] === sender) { 123 | sender = key 124 | break 125 | } 126 | } 127 | // and printing it out 128 | console.log(sender + ":", msg.msg) 129 | } 130 | lastIndex = msgs.length - 1 131 | } 132 | }) 133 | }, 1000) 134 | }) 135 | } 136 | 137 | // monitor messages adress to ourself 138 | monitorMessages(id) 139 | // monitor global messages 140 | monitorMessages("global") 141 | 142 | var cursor = "> " 143 | var readCommand = function() { 144 | rl.question(cursor, function(reply) { 145 | var command, input 146 | [command, input] = split(reply) 147 | 148 | // replace movement verbs with direction 149 | switch (command) { 150 | case "go": 151 | case "move": 152 | case "walk": 153 | case "tunnel": 154 | case "crawl": 155 | case "ambulate": 156 | command = input 157 | } 158 | 159 | // get latest state information (useful in case of a warp by another player) 160 | getState(id) 161 | .then(function(player) { 162 | var oldPos = player.pos.x + "," + player.pos.y 163 | // handle commands 164 | switch (command) { 165 | case "n": 166 | case "north": 167 | console.log("you move north") 168 | player.pos.y += 1 169 | //updatePos(player, oldPos) 170 | update(id + "/pos", player.pos) 171 | break 172 | case "s": 173 | case "south": 174 | console.log("you move south") 175 | player.pos.y -= 1 176 | //updatePos(player, oldPos) 177 | update(id + "/pos", player.pos) 178 | break 179 | case "e": 180 | case "east": 181 | console.log("you move east") 182 | player.pos.x += 1 183 | //updatePos(player, oldPos) 184 | update(id + "/pos", player.pos) 185 | break 186 | case "w": 187 | case "west": 188 | console.log("you move west") 189 | player.pos.x -= 1 190 | //updatePos(player, oldPos) 191 | update(id + "/pos", player.pos) 192 | break 193 | case "help": 194 | printHelp() 195 | break 196 | case "reply": 197 | // syntax: reply 198 | // reply to the latest received correspondent 199 | return get(player.id + "/messages").then(function(msgs) { 200 | return msgs[msgs.length - 1] 201 | }).then(function(last) { 202 | var msg = {sender: player.id, msg: input} 203 | return append(last.sender + "/messages", msg) 204 | }).then(function() { 205 | return player 206 | }) 207 | case "write": 208 | // syntax: write msg 209 | var recipient, msg 210 | input = input.split(" ") 211 | recipient = input.splice(0, 1) 212 | // remap if an alias used and it exists 213 | if (recipient in player.aliases) { recipient = player.aliases[recipient] } 214 | msg = input.join(" ") 215 | // console.log("to:", recipient, "msg:", msg) 216 | msg = {msg: msg, sender: player.id} 217 | return append(recipient + "/messages", msg).then(function() { 218 | return player 219 | }) 220 | case "messages": 221 | return get(player.id + "/messages").then(function(msgs) { 222 | msgs = msgs || [] 223 | msgs.forEach(function(msg) { 224 | var sender = msg.sender 225 | // reverse lookup in our aliases for a nickname of the sender 226 | for (var key in player.aliases) { 227 | if (player.aliases[key] === sender) { 228 | sender = key 229 | break 230 | } 231 | } 232 | console.log(sender + ":", msg.msg) }) 233 | return player 234 | }) 235 | case "warp": 236 | // syntax: warp = 256 | var target = input 257 | // get id if alias was used 258 | if (target in player.aliases) { target = player.aliases[target] } 259 | return get(target + "/pos").then(function(pos) { 260 | if (pos) { 261 | console.log("%s is at %j", target, pos) 262 | } else { 263 | console.log("%s appears to be lost in the void..", target) 264 | } 265 | return player 266 | }) 267 | case "alias": 268 | // syntax: alias = 269 | var alias, friendId 270 | [alias, friendId] = input.split("=") 271 | player.aliases[alias] = friendId 272 | console.log("%s is now known as %s", friendId, alias) 273 | update(player.id + "/aliases", player.aliases) 274 | break 275 | case "whoami": 276 | var pos = player.pos.x + "," + player.pos.y 277 | console.log("you are " + player.id) 278 | console.log("your position is currently %s", pos) 279 | break 280 | case "aliases": 281 | console.log("%j", player.aliases) 282 | break 283 | case "key": 284 | get(input).then(function(val) { 285 | console.log(val) 286 | return player 287 | }) 288 | break 289 | case "look": 290 | var pos = player.pos.x + "," + player.pos.y 291 | console.log("your position is currently %s", pos) 292 | return get(pos + "/description").then(function(description) { 293 | if (!description) { 294 | description = "you're surrounded by the rock walls you've known since birth" 295 | } 296 | console.log(description) 297 | // return get(pos + "/players") 298 | // }).then(function(players) { 299 | // players = players || [] 300 | // var index = players.indexOf(player.id) 301 | // if (index >= 0) { 302 | // players = players.splice(index, 1) // remove our player from on-tile players 303 | // } 304 | // if (players.length > 0) { 305 | // console.log("you see %d other%s", players.length, players.length > 1 ? "s" : "") 306 | // players.forEach(function(p) { 307 | // if (p in player.aliases) { p = player.aliases[p] } 308 | // console.log(p) 309 | // }) 310 | // } 311 | return player 312 | }) 313 | case "describe": 314 | var pos = player.pos.x + "," + player.pos.y 315 | return update(pos + "/description", input).then(function() { 316 | console.log("your description will be remembered..") 317 | return player 318 | }) 319 | case "quit": 320 | case "exit": 321 | console.log("Closing...") 322 | sw.destroy() 323 | process.exit() 324 | break 325 | default: 326 | console.log("didn't recognize " + reply) 327 | } 328 | return player 329 | }).then(function(player) { 330 | cursor = player.pos.x + "," + player.pos.y + " > " 331 | readCommand() 332 | }) 333 | }) 334 | } 335 | 336 | // start reading input from the player 337 | get(id + "/pos").then(function(pos) { 338 | pos = pos || {x: 0, y: 0} 339 | cursor = pos.x + "," + pos.y + " > " 340 | readCommand() 341 | }) 342 | } 343 | 344 | 345 | // moves a value from one array to another 346 | function moveItem(oldKey, newKey, value) { 347 | return get(oldKey).then(function(arr) { 348 | // remove from old array 349 | arr = arr || [] 350 | var index = arr.indexOf(value) 351 | if (index >= 0) { // only remove index if it's actually in arr 352 | arr.splice(index, 1) 353 | } 354 | return update(oldKey, arr) 355 | }).then(function() { 356 | return get(newKey) 357 | .then(function(arr) { 358 | // add to new array 359 | arr = arr || [] 360 | arr.push(value) 361 | return update(newKey, arr) 362 | }) 363 | }) 364 | } 365 | 366 | // appends to an array 367 | function append(key, val) { 368 | return get(key).then(function(arr) { 369 | if (!arr) { arr = [] } 370 | arr.push(val) 371 | return arr 372 | }).then(function(arr) { 373 | return update(key, arr) 374 | }) 375 | } 376 | 377 | function update(key, val) { 378 | return new Promise(function(resolve, reject) { 379 | db.put(key, val, function(err) { 380 | if (err) { 381 | console.log(err) 382 | reject(err) 383 | } 384 | resolve() 385 | }) 386 | }) 387 | } 388 | 389 | function get(key) { 390 | // console.log("1: GETTING KEY", key) 391 | return new Promise(function(resolve, reject) { 392 | db.get(key, function(err, nodes) { 393 | // console.log("GETTING KEY", key) 394 | if (err) { 395 | resolve(null) 396 | } else if (nodes && nodes[0]) { 397 | resolve(nodes[0].value) 398 | } else { 399 | resolve(null) 400 | } 401 | }) 402 | }) 403 | } 404 | 405 | function noop () {} 406 | -------------------------------------------------------------------------------- /mock-server.js: -------------------------------------------------------------------------------- 1 | var peernet = require("peer-network") 2 | var network = peernet() 3 | var server = network.createServer() 4 | var Readable = require("stream").Readable 5 | var fs = require("fs") 6 | 7 | function connect(name) { 8 | console.log("starting server at " + name + "..") 9 | 10 | server.listen("hyperdungeon") // listen on a name 11 | server.on("listening", function() { 12 | console.log("ready for connections") 13 | }) 14 | 15 | // when someone connects, we receive their feed key. if they are joining for the first time we add them to the end of 16 | // the feed list and then pass them the entire list 17 | // they then use that list to propagate their hyperdb instance 18 | server.on("connection", function (stream) { 19 | console.log("new connection") 20 | stream.on("data", function (data) { 21 | console.log("received:", data.toString()) 22 | var peerKey = data.toString() 23 | if (feeds.indexOf(peerKey) < 0) { // new peer connected, add them to our feeds 24 | feeds.push(peerKey) 25 | console.log(feeds) 26 | fs.writeFile("./feeds.json", JSON.stringify(feeds), function(err) { 27 | if (err) { console.log(err) } 28 | }) 29 | } 30 | var readStream = new Readable() 31 | readStream.push(JSON.stringify(feeds)) 32 | readStream.push(null) // signals end of the read stream 33 | readStream.pipe(stream) // reply 34 | }) 35 | }) 36 | server.on("error", function(err) { 37 | console.log(err) 38 | }) 39 | } 40 | 41 | var feeds = [] 42 | function start(name, key) { 43 | return new Promise(function(resolve, reject) { 44 | fs.readFile("./feeds.json", function(err, data) { 45 | if (!err) { 46 | feeds = JSON.parse(data) 47 | } else { 48 | feeds.push(key) // add the first key i.e. our key to feeds 49 | } 50 | resolve(feeds) 51 | connect(name) 52 | }) 53 | }) 54 | } 55 | module.exports = start 56 | module.exports.connect = function(name) { 57 | fs.readFile("./feeds.json", function(err, data) { 58 | if (!err) { 59 | feeds = JSON.parse(data) 60 | } 61 | connect(name) 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperdungeon", 3 | "version": "0.0.0", 4 | "main": "hyperdungeon.js", 5 | "scripts": { 6 | "start": "node hyperdungeon.js" 7 | }, 8 | "dependencies": { 9 | "hypercore": "^6.3.8", 10 | "hyperdb": "github:mafintosh/hyperdb", 11 | "hyperdiscovery": "^6.0.4", 12 | "peer-network": "^2.0.2", 13 | "random-access-memory": "^2.4.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | var hypercore = require("hypercore") 2 | var ram = require("random-access-memory") 3 | var st = process.argv.indexOf("--storage") > -1 ? storage : ram 4 | var local = hypercore("./dungeon-dir", {valueEncoding: "json", sparse: true}) 5 | 6 | module.exports = {local: local, feeds: [ 7 | local, // macbook 8 | hypercore(st, "5c73d8199d83875b62b19b28893b374189e439e760dc070497cfbd643bfb8fbe", {valueEncoding: "json", sparse: true}), // wintermute 9 | hypercore(st, "7cfc122d7ce9e73d8324f49cb0ab4cd4a92e708e87757102b06d0ed757f7d4aa", {valueEncoding: "json", sparse: true}) // clone client 10 | ], join: join} 11 | 12 | function createFeed(key) { 13 | return hypercore(st, key, {valueEncoding: "json", sparse: true}) 14 | } 15 | 16 | function join(arr, key) { 17 | var feeds = [] 18 | for (var i = 0; i < arr.length; i++) { 19 | var feed = arr[i] 20 | if (feed === key) { 21 | feeds.push(local) 22 | } else { 23 | feeds.push(createFeed(feed)) 24 | } 25 | } 26 | return feeds 27 | } 28 | 29 | function storage (name) { 30 | return raf("dungeon.map/" + name) 31 | } 32 | --------------------------------------------------------------------------------