Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Thoughts on Dots #1

Open
nejstastnejsistene opened this issue Jan 27, 2014 · 0 comments
Open

Thoughts on Dots #1

nejstastnejsistene opened this issue Jan 27, 2014 · 0 comments

Comments

@nejstastnejsistene
Copy link
Owner

Thoughts on Dots

Dots Solving Program

It takes a screenshot and returns a path to take.

Optionally take the number of moves, current score, and coordinates of the dots to skip parsing the image. Also optionally take options like whether to use powerups or not.

Parse Image

Turns Remaining and Current Score

This doesn't really need to be there for the hackathon. It adds to much complication for such a time limited competition.

Do OCR on the top gray areas of the image.

tesseract seems to work pretty well but it has to be cropped pretty well to be accurate from my brief messing around with it.

My cropping algorithm:

  • Similar to the edge detection for finding dots below.
  • First crop the gray header (easy, iterate until you hit white pixels to find the height of the header).
  • Do edge detection from the top, and their should be groups of edges. Some of those groups will be significantly taller than the rest (the numbers are taller than their labels).
  • Crop near those boundaries edges to two smaller images which should be just the number.
  • tesseract -psm 8 filename.png output
  • The value is in output.txt

Coordinates of Dots

Use simple edge detection to find the circles.

For each row in the image whose first pixel is white, find the first non-white pixel in that row. If the scan goes all the way through without finding any non-white pixels, no dot was hit. This should give you the left edges of circles. Given one list of coordinates representing an edge, the number of coordinates in that list is the diameter and the minimum x coordinate (denoted (x, y)) in that list is the center of the dot on the y-axis. The center of the dot is (x + radius, y).

We only need to do this for the first two dots to get the left most coordinate and the distance between dots.
Given x0, y0, and d (the distance between dots), the coordinate of the dot at (row, col) is (x0+d*row, y0+d*col).

Colors of the Dots

Needed for the path finding algorithm.

Assuming a 6x6 2d array of coordinates and a PIL image:

colors = [map(image.getcolor, row) for row in coordinates]

Find Optimal Path

Iterate through all paths

  • First break up grid into simple graphs (connected nodes representing dots) of connected chunks of the same color.
    • Starting at each of the 36 dots, perform a "flood fill" search for adjacent matching colors, marking nodes as visited.
    • The flood fill can be a naive recursive solution, and the restricted space will keep it tame.
    • Time complexity: linear to the number of dots.
  • For each subgraph from the previous step, link all of nodes (marking each other as neighbors).
    • itertools.combinations(subgraph, 2) is what I'm thinkin of.
    • A quick way to check adjacency is using the 1-norm (abs(r_1-r_0)+abs(c_1-c_0)==1).
    • Time complexity: not sure, but the worst case (36 choose 2) isn't that bad.
  • For each subgraph, do a depth-first search starting from each node to iterate through all possible paths.
    • DFS is simple. It's basically a brute force on a node, iterating through all of its neighbors, then each of each neighbor's neighbors, and so on.
    • The main restriction on paths is that line segments can not be repeated.
    • This should be efficient except when their are cycles, which happen to have special meaning in Dots! So we can short circuit.
      • I haven't proved this but I think this is true: If you repeatedly remove nodes with less than 2 edges from the subgraph, the remaining graph must either be empty or it has a cycle in it.
      • After removing those 1 edged nodes, choose an arbitrary node and follow neighbors until it forms a cycle.
        • This could miss some cycles which are better than others (for example it might miss a cycle that encircles other dots if a smaller cycle that is available.
          1. Assign weights to each edge so that weight=|v_1|+|v_2| where v_1 and v_2 are the vertices that the edge connects.
          2. For each vertex, add the edge(s) with the maximum weight to a set of edges to be removed. If either of the vertices of one of those paths has a degree of 2, don't remove it.
          3. Remove all of the edges in that set.
          4. DFS until you have the Eularian path.
          5. We still need to think of a decent algorithm to detect dots encircled by a cycle. (Aside: the bounding box of a set/graph is called the "convex hull")
      • This could benefit from an adjacency matrix for checking adjacency and disconnecting nodes without modifying the actual graph.
        • Instead of using an actual matrix, it's probably quicker to use a dictionary which maps nodes to a set of their neighbors.

Recursively try all possible moves

  • Apply a path to the grid.
    • When removing a dot and making the above dots fall down, go from top to bottom so that we don't mess up the coordinates in our path.
    • Fill in unknown dots with None, and have our path finding algorithm ignore them (alternatively, use a probabilistic estimation of value based on what color they might be, or something like that.)
    • For each path apply it to the board, and for each path in that board do the same, until there are no more moves to be made (the board is filled with Nones.) Choose a path from the first level that has the highest estimated number of dots removed.

Cycle Detection

A simple O(1) way to detect cycles in a path is to check if len(dots) != len(set(dots)). In other words, look for repeated dots in a path.

Hashing Collections of Dots

When checking if a segment already exists, we'll want to be able to quickly check if segments are equivalent, i.e.

def __eq__(self, other):
    a, b = self
    c, d = other
    return (a, b) == (c, d) or (a, b) == (d, c)

We also want the hash to reflect the equality so we can using hash sets for constant time operations. I suggest using a bitfield. This will use 36 bits (1 for each position) and has the advantage that it can extend to any sized collection of dots in case we want to compare the equivalence of paths or something.

Plug-in Optimizations with Cython

We can probably easily and significantly speed up our code using cython's augmenting pxd file. By providing this extra file with type annotations cython is able to compile the code to a .so file that is importable by regular python, while leaving the original python file unchanged as well! This could be an easy way to get our crazy exponential complexity algorithm to run in decent time if hashing and stuff isn't good enough.

Example:

foo.py

class Dot(object):
    def __init__(self, row, col, color):
        self.row = row
        self.col = col
        self.color = color

def print_dot(dot):
    print '{} dot at ({}, {})'.format(dot.color, dot.row, dot.col)

foo.pxd

cdef class Dot:
    cdef int row, col, color
cpdef print_dot(Dot dot)

setup.py

from distutils.core import setup
from Cython.Build import cythonize

setup(ext_modules=cythonize('foo.py'))

Now running python setup.py build_ext --inplace will generate a .so and we get speedups at little cost! Woohoo! After messing with it a little bit the only caveat I have seen is that there is no support for the yield keyword which I use a lot.... Idk how slow our algorithm will actually run/if it will be worth it or not.

End Result

The program should output coordinate pairs in terms of pixels.

Twilio MMS Service

An addition to the server that people can text screenshots, and it draws a path on their screenshot and texts it back to them.

The twilio callback should simply queue images and the phone number that they came from. Optionally, the body of the incoming MMS's can specify parameters to our algorithm. Then, a worker pool can process the images as follows:

  • Queue the image and phone number.
  • Wait for the image to be processed by workers running the above program.
  • Generate images of dots with paths drawn on them and save them in a publicly accessible directory.
  • Use twilio to send them the generated image (code sample from here). Note this requires us to get a Canadian phone number ($1/mo) or a short code ($1000/mo). Tough decision.
message = client.messages.create(
    body="Hello Monkey!",  # Message body, if any
    to="+12125551234",
    from_="+15105551234",
    media_url=[  # List of media URLs, if any
        "http://example.com/image1.jpg",
        "http://example.com/image2.jpg",
    ],
)

Emulators

Army of emulators, all using the above webserver to decide their moves in an effort to make high scores, and tweeting about their progress (I have preemptively registered @DotBotDotBot in anticipation).
Justification: Android images have a minimal linux install, without things like python, curl, find, etc that we take for granted.
Result: Move most of the code to the webserver, so that the application running on the emulator is only translating coordinate pairs to touch events and writing them to /dev/input/event0. This will probably be a shell script or written in C.

  • Take screenshot.
    • Problem: Currently no way to take screenshots because we have to use the "Use Host GPU" option for the emulator to even play Dots. https://code.google.com/p/android/issues/detail?id=60359
    • We can use imagemagick: import -window 5554:DotBot ~/Pictures/screenshot.png, where 5554:DotBot is the title of the gui window we're taking the screenshot of.
  • Send screenshot to server and get path to take.
  • Translate path to touch events.

Setup up Remove Instance with X and VNC

https://www.digitalocean.com/community/articles/how-to-setup-vnc-for-ubuntu-12

Setup Emulator with Dots

  • Install bundle to from here.
  • Make sure <bundle>/sdk/tools and <bundle>/sdk/platform-tools are in your $PATH.
  • android sdk # Install everything for version 4.3
  • android avd # Create an avd
    • Name: DotBot
    • Nexus 4
    • 4.3 (API Level 18)
    • ARM
    • Use Host GPU
  • emulator @DotBot -no-boot-anim -no-audio
  • Download Dots from play store here
  • adb install com.nerdyoctopus.gamedots.apk
  • Play Dots!

Powerups

We might be able to edit /data/data/com.nerdyoctopus.gamedots/files/.Defaults.plist to give us free powerups.

I have been successful by killing the app, pulling the file, adding some entries, pushing it back, and then restarting the app. Experimentation has shown that the counts use signed 32 bit integers, and that 2**31-1=2147483647 is the maximum value that can be entered without resulting in negative values.

<key>number_of_shrinkers</key>
<integer>2147483647</integer>
<key>number_of_expanders</key>
<integer>2147483647</integer>
<key>number_of_time_freezes</key>
<integer>2147483647</integer>

sendevent format

The source code for sendevent reveals it's a simple wrapper around writing directly to the device file, so we could either use sendevent or write it ourselves.

usage: sendevent <device> <type> <code> <value>

In the above emulator setup, all events should be written to /dev/input/event0

type code value what it means
3 0 pixel x coordinate
3 1 pixel y coordinate
1 330 1 finger down
1 330 0 finger up
0 0 0 separator

Sample drag event

This should drag in a right triangle shape. This is untested, I just figured it out from getevent output and sendevent.c. The timing between events might need tweaking to make android register them appropriately. Also I don't know how many points (if any) need to be registered in between coordinates to make a proper path but we can experiment with that.

sendevent /dev/input/event0 3 0 100
sendevent /dev/input/event0 3 1 100
sendevent /dev/input/event0 1 330 1
sendevent /dev/input/event0 0 0 0 # Finger down at 100,100
sendevent /dev/input/event0 3 0 125
sendevent /dev/input/event0 0 0 0 # Move to 125,100
sendevent /dev/input/event0 3 0 150
sendevent /dev/input/event0 0 0 0 # Move to 150,100
sendevent /dev/input/event0 3 1 75
sendevent /dev/input/event0 0 0 0 # Move to 150,75
sendevent /dev/input/event0 3 1 50
sendevent /dev/input/event0 0 0 0 # Move to 150,50
sendevent /dev/input/event0 3 0 125
sendevent /dev/input/event0 3 1 75
sendevent /dev/input/event0 0 0 0 # Move to 125,75
sendevent /dev/input/event0 3 0 100
sendevent /dev/input/event0 3 1 100
sendevent /dev/input/event0 0 0 0 # Move to 100,100
sendevent /dev/input/event0 1 330 0
sendevent /dev/input/event0 0 0 0 # Release finger

Website

Domain Names

We ought to make a simple webpage to "sell" what we did. I'm thinking a twitter feed of our exploits, plus a video of the emulator playing itself, plus a big bold phone number with "Text Your Screenshots to This Number!". Plus of course a snazzy "Fork me on GitHub" banner. GitHub pages is as good a place as any to do this.

Domain Names

The way my GitHub is configured, the default URL will be http://peterjohnson.io/DotBot/. Alternatively, we can buy a domain name. If we do, we should probably do this at least a few days in advance so we aren't worrying about the registration getting processed in time like last year or about DNS propagation.

Dependencies

Python

package license py3k what for
Pillow PIL Yes Image manipulations
Twilio MIT Yes MMS integration
Flask BSD Yes Simple web server for twilio callback

So that means we can probably write it in Python 3.3 and we won't be restricted to GPL.

Emulator Stuff

  • Android bundle
  • imagemagick for screenshots.
  • Dots (from google play store)
  • Maybe boto for automating AWS instances.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant