Topics - Deozaan [ switch to compact view ]

Pages: prev1 2 3 [4] 5 6 7 8 9 ... 93next
16
Developer's Corner / Constructing a minimal Sudoku puzzle
« on: July 28, 2021, 10:35 AM »
Hi all,

I've been working on programming a Sudoku game and I've run into a problem with my code that I've been unable to solve for the past few days and realize I need some fresh eyes on the problem. This may not be the best place to ask about this, but as best as I can tell this problem requires a bit of an in-depth explanation and I don't really feel comfortable asking on other sites that may be more closely aligned with the specific area of inquiry, but where I have not really established myself as part of the community. And maybe it could be asked more succinctly, but unfortunately for anyone who reads this, I tend to be overly verbose in an effort to make sure I'm explaining myself clearly. So here goes.

First of all, I assume the reader is familiar with Sudoku puzzles and understands the basic rules. But I figure I should establish some Sudoku terminology (mostly from Peter Norvig), so we're all speaking the same language here.

Square: A place where a number goes. A Sudoku puzzle is made up of 81 squares arranged in a 9x9 grid.
Row: Nine squares arranged horizontally (9x1), in which the numbers 1 through 9 can only appear once.
Column: Nine squares arranged vertically (1x9), in which the numbers 1 through 9 can only appear once.
Box: Nine squares arranged in a 3x3 grid, in which the numbers 1 through 9 can only appear once.
Unit: Any nine squares that compose a row, column, or box. Each square is a member of exactly 3 units.
Peers: Any squares that share a unit are considered peers. Each square has exactly 20 peers.
Given: The "freebie" clues that are known at the start of the puzzle.
Minimal Puzzle: A Sudoku puzzle that has the fewest amount of givens possible, while still only having one unique solution.

And here's a crude diagram showing a minimal puzzle with some of the above terminology highlighted:

Sudoku Terminology.png

So what I'm trying to accomplish is making a game that can generate an infinite number of Sudoku puzzles (well, as many of the 6,670,903,752,021,072,936,960 possible puzzles as a player might want/be able to play). It turns out that generating solutions is easy. You can sort of just make it up as you go along. The part I'm having trouble with is going from the solution to the minimal puzzle.

In other words, in order to present the player with a puzzle to solve, I want to go from something like what's on the left to something like what's on the right:

Code: Text [Select]
  1. 1 2 7 | 4 5 8 | 3 6 9     . . . | . . . | . 6 .
  2. 5 3 9 | 6 7 2 | 1 8 4     . 3 . | . . 2 | . . .
  3. 6 4 8 | 9 3 1 | 7 2 5     . . . | . 3 . | . . .
  4. ------+-------+------     ------+-------+------
  5. 3 1 5 | 7 8 6 | 4 9 2     . . . | 7 . 6 | . . .
  6. 4 8 2 | 3 1 9 | 6 5 7     . 8 2 | . 1 . | . . .
  7. 9 7 6 | 2 4 5 | 8 3 1     9 . 6 | . 4 . | . . .
  8. ------+-------+------     ------+-------+------
  9. 8 9 4 | 5 6 7 | 2 1 3     8 9 4 | 5 . 7 | . . 3
  10. 2 6 3 | 1 9 4 | 5 7 8     . . . | 1 . . | 5 7 8
  11. 7 5 1 | 8 2 3 | 9 4 6     . 5 . | . . . | . . .

I've written code that accomplishes it. In fact, the above puzzle was generated by my code. But it's far too slow, and unpredictably so, to be useful in a real-time game. Sometimes it only takes a couple of seconds to generate a puzzle, which in itself is too slow. But other times it takes up to a few minutes. And in a few cases it has taken so long that I've been too impatient to let it finish. I'm coding this in GDScript, which is a custom language created for Godot, but which is very similar to Python.

I'm storing the solution of the 9x9 grid in a special kind of one-dimensional array in GDScript called a PoolIntArray, which is passed by value instead of reference. It would look like this as a traditional array:

Code: Python [Select]
  1. var answer := [
  2.         1, 2, 7, 4, 5, 8, 3, 6, 9,
  3.         5, 3, 9, 6, 7, 2, 1, 8, 4,
  4.         6, 4, 8, 9, 3, 1, 7, 2, 5,
  5.         3, 1, 5, 7, 8, 6, 4, 9, 2,
  6.         4, 8, 2, 3, 1, 9, 6, 5, 7,
  7.         9, 7, 6, 2, 4, 5, 8, 3, 1,
  8.         8, 9, 4, 5, 6, 7, 2, 1, 3,
  9.         2, 6, 3, 1, 9, 4, 5, 7, 8,
  10.         7, 5, 1, 8, 2, 3, 9, 4, 6,
  11. ]

I also have a similar array of booleans, called "givens" which indicates if a particular square is a given or not.

I then follow a method I read about in a paper called Sudoku Puzzles Generating: from Easy to Evil (link to archive, since original link is dead) which the authors refer to as "digging holes" in the puzzle, and offer this flowchart as part of their explanation:

Digging Strategy.png

I'll summarize my understanding and (attempted) implementation as follows:

  • Order a list of all the squares to "dig" however you see fit. (I just shuffle an array of all the squares)
  • For each square in the list, attempt to dig the known value N. This means to set the value as "unknown" or "empty" (represented internally by the number 0).
  • With the newly dug square, attempt to solve the puzzle with all possible values (1-9) other than N.
  • If you find a solution, it means the puzzle is no longer unique (it has multiple solutions), so the square is considered a given and must display the value N to maintain uniqueness.
  • If you don't find a solution, the square can remain "dug" and you continue iterating the list to dig more holes.
  • Once you've iterated the entire list, you should have dug out all squares that are not essential to maintaining the uniqueness of the puzzle, which results in a minimal puzzle.

Here's the code which I think does what I described above:

Code: Python [Select]
  1. # digs a solved grid to find a minimal puzzle
  2. func find_givens() -> void:
  3.         # make a list of all 81 indices of the grid
  4.         var dig_list := []
  5.         for idx in 81:
  6.                 dig_list.push_back(idx) # push_back() just adds elements to the end of an array
  7.         # randomize the order of squares we "dig"
  8.         dig_list.shuffle()
  9.         # prepare some variables we'll be using repeatedly
  10.         var nums = answer # grab a copy of the answer so we don't make changes to it
  11.         var v : int # temporarily stores the "dug" value in case we need to replace it
  12.         var coords := Vector2() # x,y coordinates
  13.         # iterate the randomized list
  14.         for i in dig_list:
  15.                 # don't attempt to dig a square that is known to be a given
  16.                 if given[i] == true:
  17.                         # sanity check: this code should never actually be reached
  18.                         continue
  19.                 # store the current value of the digit
  20.                 v = nums[i]
  21.                 for n in range(1, 10): # iterate the numbers 1-9
  22.                         # skip if this is the square's original value
  23.                         if n == v:
  24.                                 continue
  25.                         # dig the value and try finding other solutions
  26.                         nums[i] = 0
  27.                         # get the (x, y) coords for this square
  28.                         coords = index_to_coord(i) # convert the index (0-80) to x,y coordinates as if on a 9x9 grid
  29.                         # change the value at this square to something else, if possible
  30.                         if is_possible(nums, coords.y, coords.x, n):
  31.                                 # N could legally be placed in this square
  32.                                 nums[i] = n
  33.                                 # try to find a solution to the puzzle with this new value in this square
  34.                                 if solve(nums) > 0:
  35.                                         # we found a solutions, which means the puzzle is no longer unique
  36.                                         # so put the value back
  37.                                         nums[i] = v
  38.                                         # mark this square as a given
  39.                                         given[i] = true
  40.                                         # don't attempt further numbers for this square
  41.                                         break


And here's the code for the if_possible() function, which is used to determine if a value n could legally be placed in the square located at (x,y) in a 9x9 grid. Or in other words, it makes sure that no other peer of the square (x,y) has the value n:

Code: Python [Select]
  1. # returns true if no peers have the value n
  2. func is_possible(grid, y : int, x : int, n : int) -> bool:
  3.         for i in 9:
  4.                 # check if n already exists in current row (9x1)
  5.                 if grid[coord_to_index(y, i)] == n:
  6.                         return false
  7.                 # check if n already exists in current column (1x9)
  8.                 if grid[coord_to_index(i, x)] == n:
  9.                         return false
  10.         # integer math here means (5 / 3) * 3 is the same as 1 * 3
  11.         var x0 : int = (x / 3) * 3
  12.         var y0 : int = (y / 3) * 3
  13.         # check if n already exists in current box (3x3)
  14.         for i in 3:
  15.                 for j in 3:
  16.                         if grid[coord_to_index(y0 + i, x0 + j)] == n:
  17.                                 return false
  18.         # as far as we know, n is a legal value for this square
  19.         return true

And finally, the code used to attempt to find a solution to the puzzle. I think this is where my trouble lies. This function basically just brute-forces the grid, attempting every possible value in every unknown square, looking for a solution, with backtracking when it reaches a dead-end. Initially I had it naively starting at element 0 of the array and looping through them in order, but this is obviously very inefficient and leads to a lot of duplicated efforts due to backtracking. It seemed to work, and generated minimal puzzles which usually had about 24-26 givens. But as mentioned previously, it was pretty slow.

So then I attempted to sort the list of squares to solve by the number of possible values that were legal to place there, starting with those which had the least possible solutions. This increases chances of success, but even in the event of a failure (dead-end) it cuts out a huge number of the possible solutions we'd have to check.

Oh, and since it's recursive, I have it keep track of and return how many solutions it has found so that it can immediately jump all the way back down the stack once a solution has been found.

Code: Python [Select]
  1. func solve(grid, solutions := 0) -> int:
  2.         # this is a dictionary whose key will be the (x,y) coordinate
  3.         # and the value will be the number of possibilities for that coordinate
  4.         var possibilities = {}
  5.         # loop through every element in the grid
  6.         for i in grid.size():
  7.                 # get the (x,y) coordinates for this index
  8.                 var coord = index_to_coord(i)
  9.                 # start with the assumption that each square has 0 possibilities (i.e., the square's value is known)
  10.                 possibilities[coord] = 0
  11.                 # if it's not zero, then it's considered "solved" already
  12.                 if not grid[i] == 0:
  13.                         continue
  14.                 # check how many values are possible here
  15.                 for n in range(1, 10): # iterate the numbers 1-9
  16.                         if is_possible(grid, coord.y, coord.x, n):
  17.                                 possibilities[coord] += 1
  18.         # sorted_list is an array whose elements will contain the index of the squares grid,
  19.         # sorted by the number of possibilities
  20.         var sorted_list := []
  21.         for n in range(1, 10): # iterate the numbers 1-9
  22.                 # loop through each element in possibilities
  23.                 for coord in possibilities.keys(): # remember that each key is an (x,y) coordinate
  24.                         if possibilities[coord] == n:
  25.                                 # add this square to the sorted list of squares to solve
  26.                                 sorted_list.push_back(coord_to_index(coord.y, coord.x))
  27.         # now iterate through the sorted list and try solving
  28.         for i in sorted_list:
  29.                 var coord = index_to_coord(i)
  30.                 if grid[i] == 0:
  31.                         for n in range(1, 10): # iterate the numbers 1-9
  32.                                 if is_possible(grid, coord.y, coord.x, n):
  33.                                         grid[i] = n
  34.                                         solutions += solve(grid, solutions)
  35.                                         if solutions > 0:
  36.                                                 return solutions
  37.                                         else:
  38.                                                 grid[i] = 0
  39.                         # none were possible, we've reached a dead end
  40.                         return solutions
  41.         # if we got this far then we've solved it!
  42.         solutions += 1
  43.         return solutions

And again, this seems to work... kind of. I mean, it is (usually) noticeably faster than when I was naively brute-forcing without sorting. But (1) it's still too slow, taking at least a couple of seconds to complete, and (2) the "minimal" puzzles its generating can't actually be minimal. Doing it the naïve way, I was getting minimal puzzles with about 24-26 givens on average, the puzzles this is generating consistently have almost twice as many, usually in the 44-46 range. But it has been proven that the least amount of givens a unique puzzle could have is as low as 17 and the highest amount of givens a minimal unique puzzle can have is 39. So something is definitely wrong. Here's an example:

Code: Text [Select]
  1. . . . | . 5 9 | . 3 4
  2. 2 1 . | . . 6 | . 5 7
  3. 9 . . | 3 . 7 | . 6 .
  4. ------+-------+------
  5. . 9 . | . . 3 | 8 4 2
  6. 5 . 8 | 7 . 2 | 3 . 9
  7. . . 2 | 1 9 . | . . .
  8. ------+-------+------
  9. . 2 . | 8 7 1 | 4 . .
  10. 1 . 7 | 9 . . | 5 . 6
  11. . 3 9 | 6 2 . | 7 8 .

Oh, and for completeness, here is the code for a couple of helper functions that are used in a few places in the code above:

Code: Python [Select]
  1. # returns an index for an 81-element array based on x,y coordinates of a 9x9 grid
  2. func coord_to_index(y : int, x : int) -> int:
  3.         return (y * 9) + x
  4.  
  5.  
  6. # returns the x,y coordinates of a square on a 9x9 grid based on 81-element array index
  7. func index_to_coord(i : int) -> Vector2:
  8.         var coord := Vector2()
  9.         coord.x = i % 9
  10.         # integer division
  11.         coord.y = i / 9
  12.         return coord

I tried adding tons of comments everywhere to make sure it was clear what the code was (supposed to be) doing. If anyone here takes the time to read this, parse/understand the code, and offer helpful suggestions/solutions, I'd appreciate it. Mostly I think I just needed a break from the coding and thought that maybe the process of writing this all out would be a form of Rubber Duck debugging.

I know I'm doing something wrong because I'm not getting minimal puzzles anymore. So help figuring out why that's happening would be much appreciated. But perhaps a more helpful response would explain what else I'm doing wrong that is making this so slow. My understanding is that most minimal puzzles should be solved/generated in milliseconds at most, and that it should be fairly rare to come across a puzzle that takes a few seconds and even rarer to come across one that takes minutes or longer. I've tried benchmarking my code by generating 100 puzzles and averaging the time it takes to do each step, and I can't even do that because while it "only" takes a few seconds at best for each puzzle, it often takes up to a few minutes. But the clincher is that in every attempt at generating 100 puzzles in a row there's (at least) one that takes so long that I've never had the patience to find out exactly how long it would take to complete. I can't remember for sure, but I think I've left it running on a single puzzle for over an hour before aborting the process and trying to adjust the code to speed it up.

What am I doing wrong? And where? Please don't say "everything/everywhere" :D

17
DC Gamer Club / Valve Announces Steam Deck: A Handheld PC
« on: July 17, 2021, 12:48 AM »
steamdeck_photo_skus.png

Valve have announced a new, portable PC with built-in gamepad which looks very much like the Nintendo Switch. It will come with a custom version of SteamOS installed, designed to run games using your Steam library. But it's a full PC, meaning it can also be docked to a TV or monitor and other peripherals (keyboard, mouse, other gamepads) using a USB hub or Bluetooth. And you could even install Windows or other Linux distributions on it.

All the internal hardware of the various models is the same except for the internal storage, which results in a 64 GB (eMMC) model for $400, a 256 GB (NVMe) model for $530, and a 512 GB (even faster NVMe than the 256) model for $650 which also has an anti-glare screen. That said, all three will include a MicroSD slot to allow for even more storage.

Here's a pretty good video overview of all the pertinent details.



Reserving requires a $5 deposit (which will be put toward the final price once you purchase) and will put you on the waiting list, which currently appears to be full until Q2-Q3 of 2022 depending on which model you want.

I decided to reserve one even though I'm still on the fence about it, because at this point I'll have about a year to cancel if I change my mind.

For more details and to reserve one for yourself, see:
https://www.steamdeck.com/

18
On July 6th, 2021, the Linux Foundation announced the public availability of the Open 3D Engine (O3DE) open-source project and the formation of the Open 3D Foundation.

As a founding partner in O3DE, Amazon Web Services and the AWS Lumberyard team has been working on this effort for awhile, preparing the code and tools for a developer-centric initial release.


atom_showcase.png


Amazon is contributing its Lumberyard game engine to open source, and it will be known as the Open 3D Engine.

The Linux Foundation will oversee the project and form the Open 3D Foundation to accelerate collaboration with game developers to enhance the triple-A game engine.

So what’s changed? Is this Lumberyard with an open-source license?

In short, a lot! Yes, it’s open source, under the permissive Apache 2.0 license. And no, O3DE is very different than the artist formerly known as Lumberyard. We leaned heavily on our Lumberyard experiences, iterated, and improved O3DE for eventual collaboration and creative control. We kept the parts that customers loved most about Lumberyard and significantly revamped the rest. We aimed to build an engine that could stand the test of time in an open source world. Because game engines tend to be monolithic, we leaned heavily toward becoming modular with extensibility, embracing open standards tooling from the onset. However, we remained unsatisfied, so we added a new prefab system, a new build system, an extensible UI, many new cloud capabilities, numerous math library optimizations, new networking capabilities, and far too many performance improvements to mention here. Also, for good measure, we even added a whole new PBR renderer capable of forward+ and deferred rendering with ray tracing and GI support!



from o3de.org

19
General Software Discussion / Windows 11 Announced
« on: June 24, 2021, 03:26 PM »
Well, I had heard the rumors and even the "confirmations" of the leaks, but I had assumed that the "Windows 11" moniker was just an internal name since Microsoft has said that Windows 10 would be the last version of Windows ever. But it seems they're actually calling it Windows 11.

They had a big, hour long presentation today, which I didn't watch. But I did watch this trimmed down to about 7-minutes version from the Verge:


20
This may be a little niche, or too specific to my situation, but I'm having some troubles and figured I'd ask if anyone here had experience or insight on how to resolve them. No worries if this is too specific. I don't expect anyone here to go through the trouble of setting up GitLab and SourceTree just to help me out, though I wouldn't put it past some of you people to be that awesomely helpful. ;)

A Brief-ish History:
Ten years ago a thread started here on some relatively new distributed version control systems called Git and Mercurial (Hg). At the time when I researched them, I wanted to use Git but it wasn't easy to get working on Windows, so I ended up using Hg. So for about the past ten years I've become an old fart, set in my ways, thumbing my nose at Git while more or less happily using Hg with BitBucket and TortoiseHg. That is, until last year when BitBucket dropped support for Hg repositories. :(

For a little while I remained stubborn and hosted my own RhodeCode server but it wasn't ideal because I actually collaborate with at least one other person on a somewhat regular basis and my ISP's upload speed isn't that great and my internet connection kept dropping out frequently, so it wasn't very reliable for others to connect to and use.

As a result, this past December I decided to bite the bullet and convert all my repositories to Git and start using Git from then on. And after doing some research I decided I'd rather be using GitLab than GitHub. So I first used a feature of GitHub to import my Mercurial repositories from my personal RhodeCode server and convert them to Git automatically. Then I used a feature of GitLab to import my GitHub repositories to GitLab.

While I was still using Hg, I was alternating between using TortoiseHg and SourceTree to manage my repositories. That is, I primarily used TortoiseHg, but I felt SourceTree had better integrated using development branches more easily. When I made the move to Git, I obviously had to drop TortoiseHg, but I decided to just keep using SourceTree, which supports both Git and Hg (for now... Atlassian owns BitBucket and SourceTree).

I think I got it all configured and had it working in December so that I could connect with GitLab and push/pull to/from my repositories. But then this year I've kind of taken a break from my usual thing and have just been doing little experiments that I never felt were big or important enough for version control. But now I'm starting to get back into wanting version control and I've run into a problem where I can't seem to get SourceTree to work with GitLab anymore.

The Problem:
Somehow I was able to successfully create a new remote repository from SourceTree, but then I immediately got an error when it tried to push my local repo to it. The error looks something like this:

remote: HTTP Basic: Access denied
remote: You must use a personal access token with 'read_repository' or 'write_repository' scope for Git over HTTP.
remote: You can generate one at https://gitlab.com/-/profile/personal_access_tokens
fatal: Authentication failed for 'https://gitlab.com/Deozaan/MyCoolRepository.git/'

GitLab has something called "personal access tokens" (PAT) which can be used in place of your password to authenticate third-party apps to work with your account. The UI doesn't make it very clear (IMO) which scopes are needed, and which scopes include the privileges of other scopes, etc. So, while the PAT I created from December had only the "api" scope, the error message says I need "read_repository" and/or "write_repository" so I created a new PAT with all the scopes.

All the things.png

It didn't help. I still get the same error.

I tried changing the URL to use SSH instead of HTTPS, but then it just asks me to load a ppk file which supposedly contains my SSH key. I don't have one of those. Or at least not the right one. I found one on my PC which I thought might be the right one, but when I load it, it just asks me for the SSH password over and over again. So it seems that the password I have stored in my password manager for GitLab SSH key does not match whatever SSH key is in that ppk file.

I do have my SSH private key stored in my password manager, but the putty agent (pageant) which SourceTree uses seems to require PPK format instead of... whatever format I have it stored as. It starts with "-----BEGIN OPENSSH PRIVATE KEY-----" whereas the random PPK file I found where I thought my GitLab SSH key would be starts with "PuTTY-User-Key-File-2: ssh-rsa"

GitLab PATs.png

So here I am with two PATs that don't seem to work anymore. Somehow I have saved an incompatible SSH key/password in my password manager. I swear I had this all working fine in December and I don't know what changed in the interim. And I don't know if it's a classic case of PEBCAK, or if GitLab's PATs aren't working right, or if SourceTree is not working properly. I just want to be able to push to and pull from my GitLab repos from SourceTree!

That said, I'm mostly just sticking with SourceTree because I'm already familiar with it and because it's free. If there's an alternative (and free) Git GUI client that works well with GitLab and runs on Windows which would solve my problem, I'd be happy to hear your recommendations on that front, as well. :Thmbsup:

Pages: prev1 2 3 [4] 5 6 7 8 9 ... 93next
Go to full version