Git Product home page Git Product logo

roguelike-tutorial's Introduction

Complete Roguelike Tutorial, using Rust + tcod

This is a port of the (in)famous Python+libtcod tutorial:

The code is all implemented but we plan to clean it up in a few places before splitting it into parts and writing the tutorial text.

The text is in the doc directory, you can look at the rendered tutorial at:

Running the tutorial code

If you want to try running a part of the tutorial using the reference code here, all you have to do is run this cargo command:

cargo run --bin name-of-part

For example, if you would like to run the reference code that is at the end of Part 1 then you run this command:

cargo run --bin part-1b-movement

Contributing

The Rust source files in src/bin/.rs are all auto-generated from the tutorial pages themselves. The single source of truth for both the code and the final pages is in doc/.adoc. If you want to make changes to the Rust files, you need to edit the corresponding pages in doc/. Note that since the code can be laid out in a different order than the tutorial, this involves a little bit of digging.

You can check the doc/rs/*.adoc files for how is each final Rust file constructed from the tutorial snippets.

Building the Rust files

First, you need to have Asciidoctor installed.

Next, after making some changes to the files under the doc directory, run the following:

make clean docs

This will generate the HTML tutorial as well as the corresponding Rust files in the target/tutorial/ directory. You can preview them there and verify that everything looks correct.

Updating the generated files in src/bin/

Run the following command:

make update-cargo-bin

It will regenerate the documents and copy the Rust files into src/bin

Validating that src/bin matches the source documents

Run:

make diff-rust

This will build the documents and diff them against the versions in src/bin/. They should match.

Contributing changes

Make sure you build build the documents, with no leftovers files and update the src/bin/ files:

make clean docs update-cargo-bin

Before opening a pull request, make sure that make diff-rust ends up empty.

Project Status

The tutorial is more or less finished: all the chapters have been written. You can follow them and build your own roguelike. You can also check out this repository and run for example cargo run --example part-13-adventure-gear to play the final version (or any of the previous ones).

There are of course things that could use updating, such as the language and general flow of the text. The structure of the tutorial itself could use some cleaning up too and some e.g. the rand crate we use is wildly out of date. Finally, there are improvements and fixes that could be done in the tcod-rs crate this tutorial is based on. For more suggestions, check out the Issues section.

However, the original author has moved on to other things and no one else has volunteered to take over the maintenance of this repository.

Issues and pull requests will not be ignored, but it can take weeks before you get a reply. And anything involving more than a minimal time investment will likely take even longer.

Sorry!

If you do wish to help out with the maintenance, please open an issue or send en email to <[email protected]>.

Other Rougelike tutorials for Rust

The RLTK Tutorial by TheBracket - It uses the RLTK project developed by the same person - Unlike libtcod, RLTK is written in pure Rust which makes building and distributing your game easier

(feel free to suggest other tutorials!)

Thanks!

This tutorial has been put together by the following people:

(sorted alphabetically)

roguelike-tutorial's People

Contributors

emmafornash avatar iszla avatar jcgrant avatar keisetsu avatar nathanator avatar raveline avatar smw avatar tomassedovic avatar wcarss avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

roguelike-tutorial's Issues

Group the `use` statements better

Right now, they're ordered kind of willy-nilly. I'm pretty sure the Rust style guide has something to say about ordering the various use statements.

Even if it doesn't, we should probably start using the 2018 grouped formatting.

Keypresses fall through.

The resulting game has a curious bug that causes key events to 'fall through', where if you open your inventory, then use an item that shares a key with a different command, the item will be used and the other command will be executed immediately after.

The source of this issue is unknown, but it is speculated to arise from the input event handler within menus.

@tomassedovic and I have already discussed this issue over email, and are putting this forward as an issue on the repo.

Use the `dyn` keyword for our trait objects

The latest Rust started giving us warning about this. Basically we need to replace things like &mut Console with &mut dyn Console in the function headers.

It might be better to wait for #53 to be finished. The whole point of the PR is to reduce the amount of repetition when we need to update all the examples.

Simplify struct creations with the `Default` trait

For example, the Object::new method could be simplified to something like this:

Object {
    x: x,
    y: y,
    char: char,
    color: color,
    name: name.into(),
    blocks: blocks,
    level: 1,
    .. default::Default(),
}

Removing all the None and false values. That would simplify the tutorial as we wouldn't have to change the structs so often. The Game one seems like another likely candidate.

`rand::distributions::Weighted` is deprecated

https://docs.rs/rand/0.6.5/rand/distributions/struct.Weighted.html

We use it here:

https://tomassedovic.github.io/roguelike-tutorial/part-12-monster-item-progression.html

Reported via email by Jordan Selanders [email protected] who also sent the following fix:

fn place_objects(room: Rect, map: &Map, objects: &mut Vec<Object>) {
    let num_monsters = rand::thread_rng().gen_range(0, MAX_ROOM_MONSTERS + 1);
    let monster_chances = ["orc", "troll"];
    let monster_weights = [2, 1];
    let monster_dist = WeightedIndex::new(&monster_weights).unwrap();
    let mut rng = rand::thread_rng();
 
    for _ in 0..num_monsters {
        let x = rand::thread_rng().gen_range(room.x1 + 1, room.x2);
        let y = rand::thread_rng().gen_range(room.y1 + 1, room.y2);
 
        let mut monster = match monster_chances[monster_dist.sample(&mut rng)] {
            "orc" => {
                let mut orc = Object::new(x, y, 'o', "orc", colors::DESATURATED_GREEN, true);
                orc.fighter = Some(Fighter {
                    max_hp: 10,
                    hp: 10,
                    defense: 0,
                    power: 3,
                    on_death: DeathCallback::Monster,
                    xp: 10
                });
                orc.ai = Some(Ai::Basic);
                orc
            }
            "troll" => {
                let mut troll = Object::new(x, y, 'T', "troll", colors::DARKER_GREEN, true);
                troll.fighter = Some(Fighter {
                    max_hp: 16,
                    hp: 16,
                    defense: 1,
                    power: 4,
                    on_death: DeathCallback::Monster,
                    xp: 20
                });
                troll.ai = Some(Ai::Basic);
                troll
            }
            _ => unreachable!(),
        };
 
        monster.alive = true;
        objects.push(monster);
    }
 
    let num_items = rand::thread_rng().gen_range(0, MAX_ROOM_ITEMS + 1);
    let item_chances = [Item::Heal, Item::Lightning, Item::Fireball, Item::Confuse];
    let item_weights = [7, 1, 1, 1];
    let item_dist = WeightedIndex::new(&item_weights).unwrap();
 
    for _ in 0..num_items {
        let x = rand::thread_rng().gen_range(room.x1 + 1, room.x2);
        let y = rand::thread_rng().gen_range(room.y1 + 1, room.y2);
 
        if !is_blocked(x, y, map, objects) {
            let mut item = match item_chances[item_dist.sample(&mut rng)] {
                Item::Heal => {
                    let mut object = Object::new(x, y, '!', "healing potion", colors::VIOLET, false);
 
                    object.item = Some(Item::Heal);
                    object
                }
                Item::Lightning => {
                    let mut object = Object::new(x, y, '#', "scroll of lightning bolt", colors::LIGHT_YELLOW, false);
 
                    object.item = Some(Item::Lightning);
                    object
                }
                Item::Fireball => {
                    let mut object = Object::new(x, y, '#', "scroll of fireball", colors::LIGHT_YELLOW, false);
 
                    object.item = Some(Item::Fireball);
                    object
                }
                Item::Confuse => {
                    let mut object = Object::new(x, y, '#', "scroll of confusion",
                                                 colors::LIGHT_YELLOW, false);
 
                    object.item = Some(Item::Confuse);
                    object
                }
            };
 
            item.always_visible = true;
            objects.push(item);
        }
    }
}

Update to Rust 2018

We can remove extern crate and replace usages like: Object { x: x, y: y, char: char, color: color } with Object { x, y, char, color } now.

Handle window events in the first game loop

The first time we introduce the event loop, we don't add any handling of window events (e.g. by calling wait_for_keypress). That causes confusion because depending on the OS, the window can work normally but not be closed or just hand in an infinite loop.

Bonus chapter idea: pathfinding

The monsters' pathfinding is implemented manually and has some glaring issues (not being to go around the corner for instance). Might be nice to show how to use libtcod's path finding.

Add license

I have just completed the tutorial this Sunday and would like to toy with the resulting code (for things like maybe webassembly compiling) and share it in the open.

Since my code is directly derived from your code, it should be subject to the same license (or a compatible one), hence the question: Under which license is the code of this tutorial released?

Wrong code snippet at the beginning of the Part 10

!map[x as usize][y as usize].block_sight,
!map[x as usize][y as usize].blocked,

Fix:

...
For example the FoV map code now looks like this:

for y in 0..MAP_HEIGHT {
    for x in 0..MAP_WIDTH {
        tcod.fov.set(
            x,
            y,
-            !map[x as usize][y as usize].block_sight,
-            !map[x as usize][y as usize].blocked,
+            !game.map[x as usize][y as usize].block_sight,
+            !game.map[x as usize][y as usize].blocked,
        );
    }
}

...

P.S. Thank you so much for the wonderful tutorial, it is very exciting to learn Rust with this.

Set up CI

We should ensure that every code contribution is formatted properly (using cargo fmt) and that all the standalone files compile (cargo build).

It should also run make diff-rust to verify that the src/bin/*.rs correspond to the ones generated from the tutorial Asciidoc source.

Key printable '<' is never triggered

Rust tcod version - 0.15.0
OS - Windows 10 64bit 1803 Build 17134.950

(Key { printable: '<', .. }, true) => {
// go down stairs, if the player is on them
let player_on_stairs = objects
.iter()
.any(|object| object.pos() == objects[PLAYER].pos() && object.name == "stairs");
if player_on_stairs {
next_level(tcod, objects, game);
}
DidntTakeTurn
}

But this works fine:

(Key { printable: ',', .. }, true) => {

or

(Key { code: Text, .. }, true) if key.text() == "<" => {

Maybe it is mistake from original python tutorial, but it seems libtcod does not work this way.

Doesn't show console on Mac OS (step 1)

I guess because libtcod is no longer supported on Mac OS. I tried running another tutorial that mentions fixing libtcod, but one of the files mentioned is a 404 by now.

Do you happen to know of a recent fix for Mac OS?

I also tried on Linux, but my AMD graphics card is too recent, and X segfaults all the time. Screw computers tonight :|

Switch to Serde once custom derive works on Stable

Serde is the way to go for serialization, but until we can easily derive the necessary traits on Rust stable, it's a no-go.

The "macros 1.1" support has just been merged to Rust and Serde already has a pull request that uses it to implement custom derives for its traits. Once that lands in Stable, we can drop rustc-serialize.

Tutorial fails to compile

I'm attempting to compile with rustc 1.12.1 stable, on Ubuntu Linux, it fails when compiling tcod-sys v4.0.1.

Use `Iterator::sum` instead of `fold`

Rust 1.11.0 has finally stabilised the sum iterator method:

https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.sum

That means we can change code like this:

let bonus = self.get_all_equipped(game).iter().fold(0, |sum, e| sum + e.power_bonus);

to something like:

let bonus = self.get_all_equipped(game).iter().map(|e| e.power_bonus).sum()

Fold is awesome, but this should be easier to explain & understand.

let bonus = self.get_all_equipped(game).iter().fold(0, |sum, e| sum + e.power_bonus);
base_power + bonus
}
pub fn defense(&self, game: &Game) -> i32 {
let base_defense = self.fighter.map_or(0, |f| f.base_defense);
let bonus = self.get_all_equipped(game).iter().fold(0, |sum, e| sum + e.defense_bonus);
base_defense + bonus
}
pub fn max_hp(&self, game: &Game) -> i32 {
let base_max_hp = self.fighter.map_or(0, |f| f.base_max_hp);
let bonus = self.get_all_equipped(game).iter().fold(0, |sum, e| sum + e.max_hp_bonus);

Error in part 2b: index out of bounds: the len is 50 but the index is 50

I'm getting an error when trying to compile. My code is basically part 2b, here's a gist of my code. I even ran ediff on it and made sure that it's roughly the same as your version but in a slightly different order.

Here's the error with a full backtrace:

~/Documents/projects/roguelike:no-branch*? λ RUST_BACKTRACE=1 cargo run --release
    Finished release [optimized] target(s) in 0.0 secs
     Running `target/release/roguelike`
24 bits font.
key color : 0 0 0
24bits greyscale font. converting to 32bits
Using SDL renderer...
thread 'main' panicked at 'index out of bounds: the len is 50 but the index is 50', /checkout/src/liballoc/vec.rs:1564:14
stack backtrace:
   0: std::sys::imp::backtrace::tracing::imp::unwind_backtrace
             at /checkout/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:49
   1: std::sys_common::backtrace::_print
             at /checkout/src/libstd/sys_common/backtrace.rs:71
   2: std::panicking::default_hook::{{closure}}
             at /checkout/src/libstd/sys_common/backtrace.rs:60
             at /checkout/src/libstd/panicking.rs:380
   3: std::panicking::default_hook
             at /checkout/src/libstd/panicking.rs:396
   4: std::panicking::rust_panic_with_hook
             at /checkout/src/libstd/panicking.rs:611
   5: std::panicking::begin_panic_new
             at /checkout/src/libstd/panicking.rs:553
   6: std::panicking::begin_panic_fmt
             at /checkout/src/libstd/panicking.rs:521
   7: rust_begin_unwind
             at /checkout/src/libstd/panicking.rs:497
   8: core::panicking::panic_fmt
             at /checkout/src/libcore/panicking.rs:92
   9: core::panicking::panic_bounds_check
             at /checkout/src/libcore/panicking.rs:68
  10: roguelike::main
  11: __rust_maybe_catch_panic
             at /checkout/src/libpanic_unwind/lib.rs:98
  12: std::rt::lang_start
             at /checkout/src/libstd/panicking.rs:458
             at /checkout/src/libstd/panic.rs:361
             at /checkout/src/libstd/rt.rs:59
  13: __libc_start_main
  14: _start

It seems to be a buffer overflow of some sorts, but I don't really know how it's possible.

Did I do something wrong?

Unrelated:
I also tried to implement the continuous vector version of the map (thought that it may solve the issue, and also that it could be a good exercise), but that required me to either write C-style code full of "global" functions (which seemed kind of ugly) or turn Map into a struct, which is overengineering things and also doesn't allow me to elegantly work with the map without getters and setters (they seem to be frowned upon in Rust). All in all this solution wasn't very elegant so I gave up on it.

Single rendering loop

Not sure how feasible/reasonable this is, but I'm a little unhappy about the fact that we use two nested rendering loops -- one for the main game and the other for targetting monsters/tiles.

And the menu drawing messes with the loop again by blocking on an input.

Might not be worth the trouble but it's something a more complicated game would need to implement.

"printable" key handlers trigger twice

Maybe easiest to reproduce by placing duplicate items during map generation, and trying to pick them up; whenever available, two items are picked up. The reason for this seems to be that tcod::input emits two events; one as a character, another as text:

Key { code: Char, printable: 'g', pressed: true, left_alt: false, left_ctrl: false, right_alt: false, right_ctrl: false, shift: false, alt: false, ctrl: false, text: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }
Key { code: Text, printable: 'g', pressed: true, left_alt: false, left_ctrl: false, right_alt: false, right_ctrl: false, shift: false, alt: false, ctrl: false, text: [103, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }

The solution seems to be simple: extend the match patterns to include code: Char.

Consider architecture that works well with Rust?

This is very vague but basically: the tutorial tries to follow the Python one reasonably closely and only differs where it's necessary. The original tutorial works well with Python but Rust is a very different language.

Would be nice to try to think of how would the same game be implemented in Rust proper. Maybe there are better ways of structuring the data and code. How would a Rustacean write this?

Simplify the drawing code?

Right now we need to clear all objects manually by calling Object.clear and when we forget to update something, it can stay on the screen indefinitely.

Since all of this is going through opengl anyway, we might as well just clear the game console and draw it all on every update. That result in less code and I doubt it would mean any difference performance-wise. (and if so, let's just add a note to that effect)

Minor issue on Part 1 handle_keys


let key = match root.wait_for_keypress(true);
match key {
    // movement keys
    Key { code: Up, .. } => *player_y -= 1,
    Key { code: Down, .. } => *player_y += 1,
    Key { code: Left, .. } => *player_x -= 1,
    Key { code: Right, .. } => *player_x += 1,

    _ => {},
}

has a match in the wait_for_keypress line that seems like it could be a typo.

Bonus chapter idea: split to multiple files

Show how to use modules and organise the dodebase into more than one file. That seems to be the most controversial aspect of the original tutorial. And it seems to me, modules trip beginners out.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.