Nonogram Demo in PyGTK

Josh Sherman
9 min read
Software Development

So a while back I had gotten 3-in-1 Sodoku Garden on my cell phone because I like Sodoku even though I’m not that great at it. I figured, 3 games for the price of 1, wicked cool. Turns out one of the 3 games would later steal my heart. It went by the name “Tenpenki” and I couldn’t find anything on the interwebs about it that didn’t include the game on my cell phone. I thought to myself, “self, this would be a great project to learn PyGTK and you’d be rewarded for your efforts with a version of Tenpenki for the computer”. And it was. Funny enough though, once I got into coding my version of the puzzle game, I did some further digging on the game and I came across the Wikipedia page that completely summed up the alleged “Tenpenki”. It was called a Nonogram and it went by many names, none of which included “Tenpenki”. At this point I figured, no reason to stop just my Nonogram project just because Simon Tatham’s Portable Puzzle Collection has one. And so I present to you my Nonogram Demo. It’s extremely hard due to the random puzzle generation with no regard for skill level. I also opted to use check boxes either checked or unchecked, this just adds to the confusion as you can’t mark off any cells are “definitely blank”. Expect a better version in the future.

#!/usr/bin/env python

# Nonogram Demo
#
# This script generates a 10x10 pseudo-randomly generated Nonogram puzzle
# with hints.  This is my first app in GTK since I dabbled with it when
# PHP-GTK was first released, and it's my first application in PyGTK since
# I started dabbling with Python not to long ago.
#
# If you're wondering what a Nanogram is, go here:
# http://en.wikipedia.org/wiki/Nonogram
#
# These types of puzzles are also known as Paint by Numbers, Picross, Pixel
# Puzzles and the name that I discovered the game under, Tenpenki.
#
# @author Josh Sherman
# @link   http://joshtronic.com

import pygtk
pygtk.require('2.0')
import gtk
import random

player_on = 0

class Nonogram:
    # Callback function that checks if the puzzle has been completed successfully
    def callback(self, widget, correct_cell_value, total_on):
        # TODO: I know globals are bad, but when returning from the callback had unexpected results (for another day!)
        global player_on

        player_cell_value = (0, 1)[widget.get_active()]

        if player_cell_value == correct_cell_value:
            # Increment the player total when they check a cell
            if player_cell_value == 1:
                player_on += 1
        else:
            # Decrement the player total when they uncheck a cell
            if correct_cell_value == 1 and player_cell_value == 0:
                player_on -= 1

        # Display a short message upon completion of the puzzle
        if player_on == total_on:
            dialog = gtk.MessageDialog(
                parent         = self.window,
                flags          = gtk.DIALOG_DESTROY_WITH_PARENT,
                type           = gtk.MESSAGE_INFO,
                buttons        = gtk.BUTTONS_OK,
                message_format = "You have successfully completed the Nonogram\n\nThis demo will terminate when you click 'OK'\n\nCheers!\n"
            )

            dialog.set_title('Congratulations')
            dialog.connect('response', lambda dialog, response: gtk.main_quit())

            dialog.show()

        return player_on

    # Quits the program
    def delete_event(self, widget, event, data=None):
        gtk.main_quit()
        return False

    # Initial logic to draw all the GUI widgets
    def __init__(self, grid):

        total_on          = 0
        player_on         = 0
        current_count     = 0
        current_col_count = []
        current_row_count = []
        all_col_counts    = []
        all_row_counts    = []

        # Loops through all the cells in the grid
        for i in range(10):
            current_count = 0

            # Loops through all the cells in the row
            for j in grid[i]:

                # Increment the counts
                if j == 1:
                    current_count += 1
                    total_on      += 1
                # Add the total to the list and reset
                else:
                    if current_count != 0:
                        current_row_count.append(current_count)

                    current_count = 0

            # This catches the count if the last cell isn't 0
            if current_count != 0:
                current_row_count.append(current_count)

            all_row_counts.append(current_row_count)
            current_row_count = []
            current_count     = 0

            # Loops through all the cells in the column
            for j in range(10):
                # Increment the counts
                if grid[j][i] == 1:
                    current_count += 1
                # Add the total to the list and reset
                else:
                    if current_count != 0:
                        current_col_count.append(current_count)

                    current_count = 0

            # This catches the count if the last cell isn't 0
            if current_count != 0:
                current_col_count.append(current_count)

            all_col_counts.append(current_col_count)
            current_col_count = []

        # Creates a new window
        self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)

        # Sets the window title
        self.window.set_title("Nonogram Demo")

        # Sets a handler for delete_event that immediately exits GTK
        self.window.connect("delete_event", self.delete_event)

        # Sets the border width of the window
        self.window.set_border_width(0)

        # Adds our main layout box
        vbox = gtk.VBox(True, 0)
        self.window.add(vbox)

        # Determines how much padding is necessary on the column hints
        max_col_hints = 0
        for i in range(10):
            current_hints = len(all_col_counts[i])

            if current_hints > max_col_hints:
                max_col_hints = current_hints

        # Pads the column hint lists
        for i in range(10):
            padding = max_col_hints - len(all_col_counts[i])

            for j in range(padding):
                all_col_counts[i].insert(0, '')

        # Determines how much padding is necessary on the row hints
        max_row_hints = 0
        for i in range(10):
            current_hints = len(all_row_counts[i])

            if current_hints > max_row_hints:
                max_row_hints = current_hints

        # Pads the row hint lists
        for i in range(10):
            padding = max_row_hints - len(all_row_counts[i])

            for j in range(padding):
                all_row_counts[i].insert(0, '')

        # Generates the column hints
        for i in range(max_col_hints):
            hbox = gtk.HBox(True, 0)
            vbox.add(hbox)

            # Adds padding to offset the row hints
            for j in range(max_row_hints):
                label = gtk.Label('')
                label.show()
                hbox.add(label)

            # Adds the hints
            for j in range(10):
                label = gtk.Label(all_col_counts[j][i])
                label.show()
                hbox.add(label)

            hbox.show()

        # Generates the playing area (with row hints)
        for i in range(10):
            hbox = gtk.HBox(True, 0)
            vbox.add(hbox)

            # Adds the row hints
            for count in all_row_counts[i]:
                label = gtk.Label(count)
                label.show()
                hbox.add(label)

            # Generates the grid of checkboxes
            for j in range(10):
                button = gtk.CheckButton()
                button.connect('toggled', self.callback, grid[i][j], total_on)
                hbox.pack_start(button, True, True, 2)
                button.show()

            hbox.show()

        vbox.show()

        self.window.show()

def main():
    gtk.main()
    return 0

if __name__ == "__main__":

    # Generates the 10x10 grid and populates the values
    Nonogram([[random.randint(0, 1) for column in range(10)] for row in range(10)])
    main()
Join the Conversation

Good stuff? Want more?

Weekly emails about technology, development, and sometimes sauerkraut.

100% Fresh, Grade A Content, Never Spam.

About Josh

Husband. Father. Pug dad. Musician. Founder of Holiday API, Engineering Manager and Emoji Specialist at Mailshake, and author of the best damn Lorem Ipsum Library for PHP.

Currently Reading

Parasie Eve

Previous Reads

Buy Me a Coffee Become a Sponsor

Related Articles