I’ve been doing the Weekly
Challenges. The
latest
involved list evaluation and poker hands. (Note that this ends today.)
Task 1: Middle Index
You are given an array of integers, @ints
.
Write a script to find the leftmost middle index (MI) i.e. the smallest amongst all the possible ones.
A middle index is an index where ints[0] + ints[1] + … + ints[MI-1] == ints[MI+1] + ints[MI+2] + … + ints[ints.length-1]
.
If MI == 0
, the left side sum is considered to be 0. Similarly, if
MI == ints.length - 1
, the right side sum is considered to be 0.
Return the leftmost MI that satisfies the condition, or -1 if there
is no such index.
The obvious way to do this is to slice the array and do progressive
summations, to test whether sum(all previous elements) == sum(all
succeeding elements). But that could be a lot of summations.
So instead I take a single sum of the whole list ("all following
elements" at the start), then progressively subtract elements from
that and add them to the "all preceding elements" sum which starts at
0.
Raku:
sub middleindex(@a) {
my $r = @a.sum;
my $l = 0;
for @a.kv -> $i, $c {
$r -= $c;
if ($r == $l) {
return $i;
}
$l += $c;
}
-1;
}
Task 2: Poker Hand Rankings
A draw poker hand consists of 5 cards, drawn from a pack of 52: no
jokers, no wild cards. An ace can rank either high or low.
Write a script to determine the following three things:
-
How many different 5-card hands can be dealt?
-
How many different hands of each of the 10 ranks can be dealt?
See here for descriptions of the 10 ranks of Poker hands:
https://en.wikipedia.org/wiki/List_of_poker_hands#Hand-ranking_categories
-
Check the ten numbers you get in step 2 by adding them together
and showing that they're equal to the number you get in step 1.
The efficient way to do this is the combinatorics which are listed at
Wikipedia. But
that's no fun, so instead I exhaustively examine every possible hand
and consider its rank.
Given the constraints we can't get five of a kind. I roll royal flush
and straight flush into a single bin number 2, to end up with:
hands |
rank |
count |
Royal or straight flush |
2 |
40 |
Four of a kind |
3 |
624 |
Full hours |
4 |
3744 |
Flush |
5 |
5108 |
Straight |
6 |
10200 |
Three of a kind |
7 |
54912 |
Two pair |
8 |
123552 |
One pair |
9 |
1098240 |
High card |
10 |
1302540 |
Total |
|
2598960 |
The analysis is most readily done with a counter class: Rust's
Counter
is ideal for this, as is Raku's Bag
, but one can make it
work in anything that supports a hash. For each of rank and suit in
the hand:
- make a list in ascending order (e.g.
[1, 7, 7, 8, 8]
);
- make a counter or hash of the counts of each value (e.g.
[1 => 1, 7 => 2, 8 => 2]
);
- make a list of the values in that counter, sorted descending (e.g.
[2, 2, 1]
).
Then use those lists and hashes to build the tests. For example, a
royal flush needs ranks [1, 10, 11, 12, 13]
all in the same suit.
Full house has three of the most common rank and two of the second
most common. And so on.
This can obviously take a while even with a mere 2.5 million
combinations to evaluate, so it's a good test for a language speed
comparison. I haven't done one of these since challenge 191, before I
started writing Scala or Crystal. Using the same algorithm in each
language, I got:
Language |
time/s |
Rust (release) |
2.433 |
Crystal (release) |
4.749 |
Kotlin |
5.873 |
JavaScript (Node.js) |
11.453 |
Kotlin (inc. compile) |
12.348 |
Ruby |
19.333 |
Python |
21.143 |
Crystal (debug) |
24.345 |
Crystal (inc. compile) |
25.813 |
Rust (debug) |
33.825 |
Lua |
44.739 |
Perl |
52.599 |
Raku |
225.112 |
And this is why I keep not using Raku for anything real: yeah, you can
have lazy sequences and infinite lists and bags and so on, there are
lots of lovely features, but the moment you ask it to do anything
difficult it's the slowest language that's asking to be taken at all
seriously—unless you write it in such a way that your whole problem
fits inside one Raku operator, and can therefore stay in the compiled
core and not touch the outer language. I've been told before that "oh,
that's the old interpreter, the new one is much faster". I've been
told this since 2019. This is running under the 2022.12 version.
I was unable to evaluate Scala because the combination generator ran
out of memory on the 16GB test machine, and PostScript threw a stack
overflow in the same place. So there's that.
In Rust:
fn main() {
let mut deck: Vec<(usize, usize)> = Vec::new();
for suit in 0 ..= 3 {
for rank in 1 ..= 13 {
deck.push((rank, suit));
}
}
let mut hands: Counter<usize> = Counter::new();
for hand in deck.iter().combinations(5) {
let mut bin: usize = 0;
let ranks = hand.iter().map(|x| x.0).sorted().collect::<Vec<_>>();
let ranksc = ranks.iter().collect::<Counter<_>>();
let ranksk = ranksc.values().sorted().rev().collect::<Vec<_>>();
let suits = hand.iter().map(|x| x.1).sorted().collect::<Vec<_>>();
let suitsc = suits.iter().collect::<Counter<_>>();
let suitsk = suitsc.values().sorted().rev().collect::<Vec<_>>();
// Can't generate 5 of a kind.
// Royal flush
if suitsc.len() == 1 &&
ranks == vec![1, 10, 11, 12, 13] {
bin = 2;
}
// Straight flush
if bin == 0 &&
suitsc.len() == 1 &&
ranks.iter().min().unwrap() + 4 == *ranks.iter().max().unwrap() {
bin = 2;
}
// Four of a kind
if bin == 0 && *ranksk[0] == 4 {
bin = 3;
}
// Full house
if bin == 0 && *ranksk[0] == 3 && *ranksk[1] == 2 {
bin = 4;
}
// Flush
if bin == 0 && *suitsk[0] == 5 {
bin = 5;
}
// Straight
if bin == 0 &&
// match full rank struct
(
(ranks[0] + 1 == ranks[1] &&
ranks[1] + 1 == ranks[2] &&
ranks[2] + 1 == ranks[3] &&
ranks[3] + 1 == ranks[4])
||
ranks == vec![1, 10, 11, 12, 13]
) {
bin = 6;
}
// Three of a kind
if bin == 0 && *ranksk[0] == 3 {
bin = 7;
}
// Two pair
if bin == 0 && *ranksk[0] == 2 && *ranksk[1] == 2 {
bin = 8;
}
// One pair
if bin == 0 && *ranksk[0] == 2 {
bin = 9;
}
// Anything else is High card
if bin == 0 {
bin = 10;
}
hands[&bin] += 1;
}
let mut tot = 0;
for k in hands.keys().sorted() {
println!("{} {}",k,hands[&k]);
tot += hands[&k]
}
println!("total {}",tot);
}
Full code on
github.