With both scoring and merging
Oops! I did indeed neglect to include merging. This is my mistake; I'm not a big fan of returning values via mutable parameters, so I think my brain shut off. Fortunately, while working on the merging logic, I made the scoring logic shorter.
// Comment #1
fn compress_zeroes(line: [u8, ..4]) -> (u8, u8, u8, u8) {
let mut nums: Vec<_> = line.iter().map(|x| *x).filter(|x| *x != 0).collect();
nums.resize(4, 0);
(nums[0], nums[1], nums[2], nums[3])
}
// Comment #2
fn score_line(line: [u8, ..4]) -> u64 {
let line = compress_zeroes(line);
let line = (line.0 as u64, line.1 as u64, line.2 as u64, line.3 as u64);
let r = match line {
(a, b, c, d) if a == b && c == d && a != 0 && c != 0 => a+1 + c+1,
(a, b, _, _) if a == b && a != 0 => a+1,
(_, a, b, _) if a == b && a != 0 => a+1,
(_, _, a, b) if a == b && a != 0 => a+1,
_ => 0,
};
(r as f32 * SCORE_MERGE_FACTOR).round() as u64
}
fn squish_line(line: [u8, ..4]) -> [u8, ..4] {
let line = compress_zeroes(line);
// Comment #3
match line {
(a, b, c, d) if a == b && c == d && a != 0 && c != 0 => [a+1, c+1, 0, 0],
(a, b, c, d) if a == b && a != 0 => [a+1, c, d, 0],
(a, b, c, d) if b == c && b != 0 => [a, b+1, d, 0],
(a, b, c, d) if c == d && c != 0 => [a, b, c+1, 0],
(a, b, c, d) => [a, b, c, d],
}
}
- This is probably the best enhancement. Since we don't care about zero values, we can simply remove all the zeroes and then add them back at the end of the array. This removes 3 cases from each
match
statement!
- Conceptually unchanged from the earlier solution, just smaller due to being able to ignore zeroes.
- Similar to
score_line
, we use pattern matching to combine consecutive values that are the same (and non-zero). In this case, we need to track all of the values, so I changed up the naming a bit to leave each letter in the same position.
I definitely prefer having two distinct methods here. In the original, the method is called score_line
, but it should actually be called score_and_merge_line
. I think that added to my original confusion.
Original answer
There's a lot of code in the original to move things around, just to ignore all that and count how many squares would be merged. I made use of Rust's pattern matching to express the small number of interesting cases that exist in a given line.
fn score_line(line: [u8, ..4]) -> u64 {
// Comment #1
let a0 = line[0] as u64;
let a1 = line[1] as u64;
let a2 = line[2] as u64;
let a3 = line[3] as u64;
// Comment #2
let r = match (a0, a1, a2, a3) {
(a, b, c, d) if a == b && c == d && a != 0 && c != 0 => a+1 + c+1,
(a, b, _, _) if a == b && a != 0 => a+1,
(_, a, b, _) if a == b && a != 0 => a+1,
(_, _, a, b) if a == b && a != 0 => a+1,
(a, 0, b, _) if a == b && a != 0 => a+1, // Comment #3
(a, 0, 0, b) if a == b && a != 0 => a+1,
(_, a, 0, b) if a == b && a != 0 => a+1,
_ => 0, // Comment #4
};
(r as f32 * SCORE_MERGE_FACTOR).round() as u64
}
- We start by unpacking each of the 8-bit numbers and casting them to a larger type. When we add two squares together, it's possible to require more than 8 bits. A
u16
would suffice here, but the original method returned a u64
, so we will stick with that.
- We've unpacked the array to separate variables, but we actually want to pattern match against all of them at once. We create a tuple to cram them all back together and match on that.
- All the pattern match lines are similar, but this one has the most variety.
a
and b
will be bound to the value of the number in the respective position in the tuple. _
means that we don't care about the value in that position, and 0
requires the literal value 0
in that position. We also use a pattern guard (if a == b
...) to further restrict the pattern match to pairs of numbers that equal each other. The expression after the =>
will be the result of the match
if that particular branch is chosen.
- This is the catch-all branch, which is taken when none of the above branches are picked.
I think that this solution is improved from the original because:
- Removed unneeded mutability. No variables are required to be mutated after they are created, which helps me as a programmer reason about the code better.
- Removed unused functionality. The function is designed to return the score, it doesn't need to actually move numbers around into the future positions. This relates to the lack of mutability.
- Pattern matching replaces what could be a complicated series of if-else checks with something that has more concise and has a visual component. The pattern matching looks like a line of numbers would.
- There's no loops or nested loops. Again this helps me as a programmer see the logic clearer.
I tried to stay close to the original code in terms of types, but I would probably make a few more changes if I kept going:
- Make a
Line
type or type alias.
- Make a newtype or type alias to indicate that the
u8
is to be interpreted as a power of two.
- Use
Option
for missing values, instead of zero.
- Remove the 1.2x factor, since I don't currently see the benefit of multiplying every value. If everything has the same weight, it doesn't affect anything.
Additionally, I believe there is a bug in the original code when two adjacent cells have 255
. I found this because I compared this code against the original for every possible value. This only took a few minutes, so I believe my code is performant enough for the simple case.
SCORE_MERGE_FACTOR
, and what type is it?f32?
\$\endgroup\$static SCORE_MERGE_FACTOR: f32 = 1.2f32;
). It is used for weighting in the AI calculations. \$\endgroup\$