A fast wordfeud solver library

Hi Rust community!

I recently published wordfeud-solver: a Rust library that evaluates all possible moves given a wordfeud game board and a set of letters.
The solver is ported from a Python package. The Rust version is more than 100 times faster, and adds support for multiple languages and smart evaluation of opponent moves. Python bindings are included.

This is my first Rust project, and I would very much appreciate to get some feedback on it.
I would specifically welcome comments on:

  • Advice on writing "idiomatic Rust"
  • Is the code documented well enough?
  • API conventions
  • directory layout: I use a workspace with 2 crates: the main library in lib/ and Python bindings in pylib/
  • Any tips for improving the speed, quality or usability

You can find wordfeud-solver on github and crates.io.

I also prepared a draft article explaining the design and operation of the software:
A fast Wordfeud Solver in Rust

Thanks in advance!

A couple comments:

  • Sou should check out clippy, it's a versatile linter that helps you catch common mistakes otherwise undetected by the Rust compiler.
  • Your Display impl for Board is not exactly efficient: you are allocating a whole string before writing it to the formatter. Not having to do that is exactly the point of having a mutable formatter instead of returning a String immediately from Display::fmt().
  • In Board::is_occupied(), you don't need to parenthesize gratuitously: (x < N) && (y < N) should be x < N && y < N – finally, Rust got operator precedence right (as opposed to C and languages that decided to copy C's precedence rules), and omitting the parentheses will do what you meant.
  • In Board::calc_rowdata(), it is unnecessary to allocate intermediate vectors. zip works with iterators. Everything except the last collect() could be removed. Even if you don't, you could still remove the map(): there's an into_iter() method on Vec that provides an iterator over owned values. You don't have to go through iter() and references.
  • result.sort_by(|a, b| (b.score).cmp(&a.score)) could be simplified to result.sort_by_key(|item| Reverse(item.score)). Better yet, impl Ord for Score and result.sort_by_key(Reverse).
    Similarly, scores.sort_by_key(|n| u32::MAX - n) should be scores.sort_by_key(Reverse).

I didn't go through the rest of the code in detail, but a quick glance suggests that similar improvements can be made in some more files as well.

Thanks for your swift reply and useful suggestions!
I am already running clippy on save, with the default settings. It does not give any of the suggestions you have. I would have expected it to warn about redundant parentheses but it does not.
Any ideas?

Try running cargo clean before running clippy. It has a habit of not showing warnings it has already shown you.

1 Like

Furthermore, some of the more useful lints are turned off by default. Here you can see a comprehensive list of all possible lints. For your convenience, I reproduce below the list of lints I like to turn on and off, respectively:

#![allow(clippy::single_match, clippy::match_same_arms, clippy::match_ref_pats,
         clippy::clone_on_ref_ptr, clippy::needless_pass_by_value,
         clippy::redundant_field_names, clippy::redundant_pattern)]
#![deny(clippy::wrong_pub_self_convention, clippy::used_underscore_binding,
        clippy::similar_names, clippy::pub_enum_variant_names,
        clippy::non_ascii_literal, clippy::unicode_not_nfc,
        clippy::result_unwrap_used, clippy::option_unwrap_used,
        clippy::option_map_unwrap_or_else, clippy::option_map_unwrap_or,
        clippy::shadow_unrelated, clippy::shadow_reuse, clippy::shadow_same,
        clippy::int_plus_one, clippy::string_add_assign, clippy::if_not_else,
        clippy::cast_precision_loss, clippy::cast_lossless,
        clippy::cast_possible_wrap, clippy::cast_possible_truncation,
        clippy::mutex_integer, clippy::mut_mut, clippy::items_after_statements,
        clippy::print_stdout, clippy::mem_forget, clippy::maybe_infinite_iter)]