GithubHelp home page GithubHelp logo

mmmcaffeine / nonograms Goto Github PK

View Code? Open in Web Editor NEW
0.0 1.0 0.0 77 KB

Engine to solve Nonogram puzzles mostly used to familiarise myself with Visual Studio 2022 and .NET 6

License: MIT License

C# 100.00%
csharp visual-studio xunit fluent-assertions fluentassertions netcore6 nonogram nonograms nonogram-solver

nonograms's Introduction

nonograms

Engine to solve Nonogram puzzles mostly used to familiarise myself with Visual Studio 2022 and .NET 6

nonograms's People

Contributors

mmmcaffeine avatar

Watchers

 avatar

nonograms's Issues

Remove duplicated algorithm from `GlueStrategy`

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!

Replace `int[]` with a custom type of `Hint`

Overview

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>.

Required Behaviour

  • Construct with an IEnumerable<uint> that contains at least one item, and no zeros
  • Explicit conversion to and from uint[] (and maybe other integral types that make sense)
  • Implicit conversion to string
  • Static Parse method that accepts a string of e.g. "2,1,3"
  • Static TryParse method that follows the accepted convention
  • Comparison operators with uint[] and other integral types that make sense (if we don't get this for free by virtue of the conversion operators
  • Support enumeration
  • Support indexing
  • Support equality which should be when all elements are the same
  • Be immutable (which means not exposing e.g. unit[] because it would allow people to change state, although you could expose e.g. IReadOnlyCollection<uint>)

`IStrategy` should accept multiple input types with default implementations

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.

Improve efficiency of `NoSmallGapsStrategy`

There are some inefficiencies in the implementation:

  • Short-circuit on special case of minimum hint of 1; it would not be possible to eliminate any gaps
  • Cut down the number of copies of arrays that are being made
  • Put calculation of end index of gap with calculation of start index and length (i.e. the 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.

Move calculation of data out of its enumeration in `NoSmallGapsStrategy`

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 πŸ€·β€β™‚οΈ

`StrategyBase` should throw when inheritor returns an inconsistent state for cells

Overview

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.

Required Behaviour

  • Throw if any cell was filled but is now eliminated or undetermined
  • Throw if any cell was eliminated but is now filled or undetermined
  • Do not throw if any cell was undetermined but is now filled or eliminated
  • The exception message should include the indices of incompatible cells, and the change in state
  • A custom exception type should be thrown e.g. InconsistentCellStateException

`GlueStrategy` can eliminate other cells

Eliminate all other cells if:

  • there is exactly one element in the hint and we know the filled cells are either glued on the left or on the right
  • there are exactly two elements in the hint and we know the filled cells are glued on the the left and on the right

`StrategyBase` should throw when an inheritor returns a non-sensical state for cells

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:

  • there are more filled cells than the hint calls for
  • there are not enough filled or undetermined cells to fulfil the hint
  • there are runs of cells that are longer than the hint calls for
  • there are less runs of cells than the hint calls for, and not enough undetermined cells to fit the remaining runs
  • there are more runs of cells than the hint calls for (taking into account some runs might get joined)

Replace `bool?[]` with a custom type of `Line`

Overview

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.

Required Behaviour

  • Construct with an IEnumerable<CellState> that contains at least one item
  • Implicit conversion to and from bool?[]
  • Static Parse method that accepts a string of e.g. "001_1_00"
  • Static TryParse method following the accepted convention
  • Comparison operators to string
  • Support enumeration
  • Support indexing
  • Be immutable (which means not exposing e.g. CellState[] because it would allow people to change state, although you could expose e.g. IReadOnlyCollection<CellState>

Simplify and move IStrategy test data

History

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.

Improvement

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 πŸ™‚

`NoSmallGapsStrategy` should account for order of hints to eliminate more cells

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:

  • Hint = "3,1"
  • Cells = "0..0...0."

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.

All implementations of `IStrategy` should test input cells are not modified

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 ofGameorPuzzle` 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 πŸ˜‰

Improve legibility of failing tests for `IStrategy` implementations

Overview

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 strings 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 😒

Cause

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!

Potential Solutions

Override 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.

Convert Line to string and compare that

This 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

Manually enumerate CellStates and compare individually

This 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.

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.