mmmcaffeine / nonograms Goto Github PK
View Code? Open in Web Editor NEWEngine to solve Nonogram puzzles mostly used to familiarise myself with Visual Studio 2022 and .NET 6
License: MIT License
Engine to solve Nonogram puzzles mostly used to familiarise myself with Visual Studio 2022 and .NET 6
License: MIT License
Eliminate all other cells if:
Currently a lot of this is tucked away in TheoryData
, and not close to the actual test consuming it. We originally used TheoryData
because we thought we could not pass arrays, and certainly not multiple arrays to a test method. In addition, we used to be passing e.g. new bool?[] { true, false, null }
which made the line, and its expected state less obvious than it could have been.
This improved somewhat with the introduction of Line
#1 and Hint
#2 We can now construct these in the TheoryData
, but can't use them as constructor parameters for InlineData
.
Now we have both types in place we can construct both from a string. Clearly, this is easy to use with InlineData
. We should switch tests over to using string
and consume the new overload of IStrategy.Execute
. We can then lose the TheoryData
.
The end result is we end up with a simple, short readable representation of both the hint and line, and can place it close to the method using it π
The ExecuteLeftGlue
and ExecuteRightGlue
methods are essentially reciprocals. There does not seem to be any good reason to have this duplication. We could implement ExecuteRightGlue
by reversing the array, calling ExecuteLeftGlue
then reversing the array again.
The only reason I can see to not do this would be because we would create an additional two copies of the array. Given we're only dealing with bool?
(at least for now) and are unlikely to have a huge number of elements this probably isn't a massive issue.
If we had a custom type of Line
(#1) it might be possible to implement e.g LineReverseEnumerator : IEnumerator<T>
which would give us the elements in the reverse order, but with indices from the right of the line!
The current implementation is quite naΓ―ve i.e. we find the smallest hint, then eliminate any cells in gaps smaller than that hint. We can potentially eliminate more cells by accounting for the order of the hints.
Consider:
Where a 0
means an eliminated cell, and .
means an undetermined cell.
The current implementation will not eliminate any gaps because nothing is smaller than 1. The specific strategy should be able to eliminate the first gap because it knows the gap of 1 has to come after the gap of three, and the three cannot possibly go in the first gap because it is only a length of two.
Part of the motivation for #1 and #2 was to make it easier to write tests against IStrategy
implementations, and particularly to move test data closer to the tests, rather than being forced to use TheoryData
. Previously I had (erroneously) thought I could only pass one array to a test method by using params
, not realising an array could be passed to the ctor of an attribute. The plan was to allow string
s to be used for the hint
and line
parameters which could be parsed into Hint
and Line
types respectively. This worked but led to output on failing tests similar to:
Expected newTrivialStrategy().Execute(hint,line) to be equal to {CellState { Description = Filled, Boolean = True, Character = 1 }, CellState { Description = Filled, Boolean = True, Character = 1 }, CellState { Description = Filled, Boolean = True, Character = 1 }, CellState { Description = Filled, Boolean = True, Character = 1 }, CellState { Description = Filled, Boolean = True, Character = 1 }}, but {CellState { Description = Undetermined, Boolean = , Character = . }, CellState { Description = Undetermined, Boolean = , Character = . }, CellState { Description = Undetermined, Boolean = , Character = . }, CellState { Description = Undetermined, Boolean = , Character = . }, CellState { Description = Undetermined, Boolean = , Character = . }} differs at index 0.
Clearly, this is horrendous to read and to try to work out what the actual issue is π’
The problem is caused by Line
now implementing IEnumerable<CellState>
. This means the Should
extension method returns a GenericCollectionAssertions
and compares the Line
as an enumerable, checking each element in the enumerable. If it finds a CellState
that doesn't match it then outputs the enumerable. The default behaviour here is to use the ToString
method on each element in the collection. In our case that is a record struct
so we get the default implementation of a comma delimited list of the public properties!
ToString
on CellState
This dramatically improves the format of the output, and gives us something similar to:
Expected newTrivialStrategy().Execute(hint,line) to be equal to {1, 1, 1, 1, 1}, but {., ., ., ., .} differs at index 0.
This is much more readable. It has the advantages of being low impact in terms of the number of places we would need to make changes. It also gives us the advantage of seeing the first index where there is a difference. This also keeps the ToString
implementation in line with the implicit conversion to string.
On the downside, this changes the ToString
implementation from what people might expect for a record, so is arguably a violation of the principle of least surprise. Repeated strings of .,
might also be a little tricky to read.
Line
to string
and compare thatThis dramatically improves the format of the output, and gives us something similar to:
* Expected ((string)actual) to be "11111", but "....." differs near "..." (index 0).
This also has the advantage of being able to see the index for where the first differences appear, and having much shorter output. Assuming we supply the input data as a string we see comparison against similar values. This makes sense from an implementation point of view. Finally, we also keep the expected implementation of ToString
for a record in place.
This has the downside of requiring more changes, but not excessively so.
On the down
CellState
s and compare individuallyThis requires more work so I haven't got sample output for this option. However, because we could use an AssertionScope
we would be able to see the index of all cells that did not match. In addition I would be able to use the because
and becauseArgs
params of Be
to be something along the lines of should have been {0} but was {1}
to give a more descriptive explanation.
This would require each test changing, and in a very similar way. I could introduce some form of helper that would be consumed by all test cases, but the MO so far with this repo has been to experiment with having as much code as possible in the test itself, and to try to avoid things such as shared set-up etc
This would also end up comparing CellStates
; it is just that we're doing it by hand rather than letting Fluent Assertions do it for us. That means we'll still pick up the default implementation ToString
for CellState
which could be clumsy in the test output.
Currently we're using bool?[]
to represent cells, but passed as IEnumerable<bool?>. It would be possible for an implementation of
IStrategyto cast this to the specific type and fiddle with the content. We do not want to allow this. Managing the state of any grid would be the responsibility of any kind of
Gameor
Puzzle` type, not the strategy itself.
It might make sense to implement this behaviour in StrategyBase
. That type could take a copy of the input array and pass that to the template method. That would guarantee inheritors cannot modify the original because they never see it. That way we only have to implement the tests for one type, rather than every implementation.
This might become a non-issue if we implement Line
(#1) and Hint
(#2); we'd expect to implement those as immutable types so implementations of IStrategy
would have a hard time changing them π
Representing the hint as int[]
is a little clumsy. Other types are required to validate data that really should be the responsibility of this type. There are also calculations regarding it, such as the minimum length of Line
required to fulfil the hint.
We should introduce a custom type of Hint
. This will be a wrapper on top of IEnumerable<int>
.
IEnumerable<uint>
that contains at least one item, and no zerosuint[]
(and maybe other integral types that make sense)string
uint[]
and other integral types that make sense (if we don't get this for free by virtue of the conversion operatorsunit[]
because it would allow people to change state, although you could expose e.g. IReadOnlyCollection<uint>
)Trying to represent all cells with an IEnumerable<bool?>
which we internally convert into bool?[]
is clumsy. The true, false, or null has meaning in our domain which is implicit. We can be more explicit about this. Setting up data is also clumsy and can be seen by test cases being awkward to visually parse e.g. what was the index of that true
value again? When debugging issues with IStrategy
implementations it was difficult to see what the data genuinely was.
We should introduce a custom type of Line
. This will be an IEnumerable<CellState>
where CellState
is an enumeration where we explicitly state meaning.
IEnumerable<CellState>
bool?[]
Parse
method that accepts a string of e.g. "001_1_00"TryParse
method following the accepted conventionstring
CellState[]
because it would allow people to change state, although you could expose e.g. IReadOnlyCollection<CellState>
StrategyBase
has no control over what cell states (bool?[]
) are returned by any subclass. As such it is possible for the returned array to make no sense when compared to the input array. An exception should be thrown in this scenario.
If there is a custom types of e.g. Line
() it would be reasonable for a query method to exist on it e.g. bool IsConsistentWith(Line line);
to make this calculation.
InconsistentCellStateException
Assuming we have types of Line
(#1) and Hint
(#2) the IStrategy
interface will change to accept these types, rather than arrays of primitives as it currently does. There will almost certainly be conversion operators or Parse
implementations to e.g. convert a string
to a Hint
. It would make sense for the IStrategy
interface to have overloads that accept these types with a default implementation of e.g.
public Line Execute(string hint, string line) => Execute(Hint.Parse(hint), Line.Parse(line));
This might be an abuse of the typical use case for this language feature (add new methods to an existing interface) so we might be better off putting the implementation on StrategyBase
. However, I cannot see there would ever be any other implementation of such methods, so it existing on the interface eases life for implementers who don't want to inherit StrategyBase
.
There are some inefficiencies in the implementation:
FindGaps
method)Reducing the number of copies will probably require reducing the use of yield return
. If methods such as FindAllIndices
returned e.g. List<int>
we would not need to copy the return value with e.g. ToArray()
. The implementation of FindGaps
needs to use the index to calculate things so this would require a minimum of IReadOnlyCollection<int>
as the input parameter. This has to be returned by FindAllIndices
. We only need to enumerate the return value of this method though, so that could still reasonably return an IEnumerable
.
We have a couple of methods in NoSmallGapsStrategy
of FindAllIndices
and FindGaps
. The latter is returning a tuple with information about the gap i.e. the index of the first cell, and the length. However, when we enumerate the return value of this method in Execute
we're calculating more things about the gap! Specifically this is the index of the last cell. We should keep Execute
to only doing the enumeration, and move all calculations about the gap into the FindGaps
method.
We could achieve this by changing the return tuple to either (int StartIndex, int EndIndex, int Length)
or (Range Range, int Length)
The latter would be nice as we could pass it directly to the array indexer when checking all the cells in the gap are undetermined. That would also mean we'd have to access Range.Start
and Range.End
in other places, which isn't quite as neat π€·ββοΈ
It would be possible to detect a number of error conditions in the bool?[]
returned by sub-classes, and throw an appropriate exception in these scenarios.
We should throw exceptions when:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.