Skip to content

naruto-unison/naruto-unison

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Build Status License: BSD-3-Clause

Naruto Unison

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.


Character Select

Game

Changelog


Installing

  1. Install Git, Stack, PostgreSQL, and NPM. Make sure all executables are added to your path.

  2. Run stack install yesod-bin.

  3. In the elm folder of the project, run npm build.

  4. In the root directory of the project, run stack build.

  5. Start up the PostgreSQL server.

  6. Create a new database and add it to the PostgreSQL pg_hba.conf file.

  7. Configure environment variables in config/settings.yml to point to the database.

Running

Development

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 build) whenever changes are made to the elm folder.

Admin Account

To grant a user admin privilege, change the "privilege" field for that user in the "user" database to 'Admin'.

Missions

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

Production

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.

Tests

To run the game test suite:

stack test :test-game

To run the web handler test suite, first create a separate test database and configure it in config/test-settings.yml, then run:

stack test :test-yesod

To run all tests and show only failures:

stack test --test-arguments --format=failed-examples

Documentation

Documentation is generated by running stack haddock.

Model Syncing

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 build) after altering code in the elm folder.

Haskell: How and Why

This postscript was added because several of the people interacting with this project mentioned that they hadn't worked with Haskell before.

How

IDEs

The recommended IDE is Visual Studio Code with the Haskell extension. Alternatively, Emacs has various Haskell plugins.

Getting Started with Haskell

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.

Why

Although Haskell is an unusual language, its idiosyncracies make it the perfect fit for a project such as Naruto Unison.

Concurrency

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.

Separation of Pure and Impure Functions

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 Game.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 do anything else that might cause problems in a multi-threaded environment.

Clear and Concise Math

Haskell's mathematical background lends itself to defining game calculations. For example, Game.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. Haskell makes it easy and legible to pattern match against a union type within a list comprehension, as in the larger domain of all effects. Snare happens to be a unary constructor; other constructors in the Effect union type have multiple arguments, and they can be matched just as easily.

Monads

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 wrapper around adjustHealth, the function from earlier in this README.

Most Importantly

It's fun.

type Lift mClass m = (MonadTrans (Tran m), mClass (Base m), m ~ Tran m (Base m))
type family Tran m :: (Type -> Type) -> Type -> Type where Tran (t n) = t
type family Base (m :: Type -> Type) :: Type -> Type where Base (t n) = n

This is a hobby project, after all.