The next generation of Naruto Arena, built from the ground up in Haskell and Elm.
Naruto was created by Masashi Kishimoto, published by Pierrot Co., and licensed by Viz Media.
Currently pre-alpha and in active development. Nothing is guaranteed to be stable or fully functional.
Character count: 180! All Naruto Arena characters are implemented except for the body doubles, Mecha Naruto, and Zaji.
-
Install Git, Stack, PostgreSQL, and NPM. Make sure all executables are added to your path.
-
Run
stack install yesod-bin
. -
In the elm folder of the project, run
npm install
. -
In the root directory of the project, run
stack build
. -
Start up the PostgreSQL server.
-
Create a new database and add it to the PostgreSQL pg_hba.conf file.
-
Configure environment variables in config/settings.yml to point to the database.
To use a development web server, run stack exec -- yesod devel
in the root directory of the project. Recompile the Elm frontend with (cd elm && npm install)
whenever changes are made to the elm folder.
To grant a user admin privilege, change the "privilege" field for that user in the "user" database to 'Admin'.
By default, the development server unlocks all characters with missions. In order to test missions, this behavior may be changed by editing config/settings.yml and uncommenting this line:
# unlock-all: false
to
unlock-all: false
In order to run the server in production mode, which has significantly better performance, deploy it with Keter. It is recommended to use a standalone server for hosting the Keter bundle and PostgreSQL database. The only server requirement to host Keter bundles is the Keter binary itself, which may be compiled on another machine with GHC and copied over. Docker support is planned but not yet implemented.
The test suite can be run with stack test
, or stack test --test-arguments=--format=failed-examples
to hide successes.
Documentation is generated by running stack haddock
.
After making changes that affect the JSON representation of a data structure transmitted to the client, run stack run elm-bridge
to make the corresponding changes to the client's representations. Always run (cd elm && npm install)
after altering code in the elm folder.
This postscript was added because several of the people interacting with this project mentioned that they hadn't worked with Haskell before.
The recommended IDE is Visual Studio Code with the Haskell Language Server extension for haskell-ide-engine. Alternatively, Emacs has various Haskell plugins.
For newcomers, Learn You a Haskell is an excellent introduction to the language. With Stack downloaded (per above), stack ghci
can be used whenever Learn You a Haskell says to use plain old ghci
.
Readability is one of Haskell's key strengths, so browsing through sources of well-known libraries on Hackage is also a good way to learn how to write idiomatic, practical code. In particular, the containers library is worthwhile reading material because it spans from low-level data-structure manipulation to high-level abstractions, and is well-documented and ubiquitous.
Although Haskell is an unusual language, its idiosyncracies make it the perfect fit for a project such as Naruto Unison.
Haskell is excellent at parallel computing. Naruto Unison is built on top of the Yesod framework, a fully asynchronous web server. With lightweight green threads and event-based system calls, every connection to the server runs smoothly in separate non-blocking processes, communicating via transactional channels.
Unless quarantined in specific monads, Haskell functions are referentially transparent, meaning they always produce the same output if given the same inputs and do not cause side-effects. This is ideal for a game in which the game engine is an independent, quasi-mathematical process that can (and should!) be separate from all the effectful work of HTTP handling and websockets and so on. Separating pure and impure functions makes the codebase much easier to test, prevents numerous bugs that could otherwise occur, and promotes healthy concurrency.
As an example, all the functions in Engine.Ninjas are guaranteed to be pure. This one modifies a Ninja's health constrained within a range:
adjustHealth :: (Int -> Int) -> Ninja -> Ninja
adjustHealth f n = n { health = min 100 . max (Ninja.minHealth n) . f $ health n }
It is a simple transformation of data. Because adjustHealth
is pure, Ninja.minHealth
is also guaranteed to be pure. These functions have consistent output and cannot modify shared state, perform network operations, or anything else that might cause problems in a multi-threaded environment.
Haskell's mathematical background lends itself to defining game calculations. For example, Engine.Effects has functions like this one:
snare :: Ninja -> Int
snare n = sum [x | Snare x <- Ninja.effects n]
This function does exactly what it looks like: sums up all effects with the Snare Int
constructor. Pattern matching inside list comprehensions is another distinguishing feature of Haskell. Snare
happens to be a unary constructor; other constructors in the Effect
sum type have multiple arguments, and they can be matched just as easily.
Another cool thing Haskell can do is define custom procedural contexts. Naruto Unison's MonadPlay
monad typeclass is a purity-agnostic game-state transformation that provides the context of the current user and target. What that means in practice is that character implementations, even fairly complex ones, can be written very simply. For example, Chiyo's Self-Sacrifice Reanimation skill has the description, "Chiyo prepares to use her forbidden healing technique on an ally. The next time their health reaches 0, their health is fully restored, they are cured of harmful effects, and Chiyo's health is reduced to 1." This is its implementation:
trap 0 OnRes do
cureAll
setHealth 100
self $ setHealth 1
Haskell's brevity and readability in this regard are clear winners over other languages. There isn't any hidden complexity behind the scenes, either: setHealth
is just a thin wrapper around adjustHealth . const
, the function earlier in this README!
type Lift mClass m = (MonadTrans (Tran m), mClass (Base m), m ~ Tran m (Base m))
type family Tran m :: (* -> *) -> * -> * where Tran (t n) = t
type family Base (m :: * -> *) :: * -> * where Base (t n) = n
This is a hobby project, after all.