Minesweeper in Minecraft!

Davie Wales has created a Minesweeper game that runs in Minecraft! He’s posted the source code with a readme
here.

The instructions for the game are as follows:

  • A minesweeper board will be created at the coordinates (0,0,0)
  • Right-clicking with a sword will clear a block (It doesn’t work if you left click, or if you right click with anything else…)
  • Placing a torch will flag a block
  • When all the mines are flagged, you win.
  • If you hit a mine, you lose.
  • Different blocks represent different numbers:
    • Glass blocks have 0 mines touching them.
    • Wooden planks have 1 mine touching them.
    • Cobblestone has 2 mines touching it.
    • Coal has 3 mines touching it.
    • Iron Ore has 4 mines touching it.
    • Gold Ore has 5 mines touching it.
    • Diamond Ore has 6 mines touching it.
    • Gold Blocks have 7 mines touching them.
    • Diamond Blocks have 8 mines touching them.

Check out the pics, video and code below:

Code:

#!/usr/bin/env python

import sys
import random
import threading
import minecraft.minecraft as minecraft

defaultDifficulty = 0.1
setDifficulty = defaultDifficulty

class board:
    """ The cartesian grid can be done as follows:
    board = [["?", "*", "?", "?", "*", "?", "?", "?", "?", "*",], 
            ["?", "?", "*", "?", "?", "?", "?", "?", "?", "?",], 
            ["?", "*", "*", "?", "*", "?", "?", "*", "*", "?",], 
            ["?", "?", "?", "?", "?", "?", "*", "?", "*", "?",],
            ["*", "?", "?", "?", "?", "*", "?", "?", "?", "?",], 
            ["?", "?", "?", "?", "?", "?", "?", "?", "?", "?",], 
            ["?", "*", "?", "*", "?", "?", "?", "*", "?", "*",], 
            ["?", "?", "?", "?", "?", "?", "?", "?", "?", "?",],
            ["?", "*", "?", "?", "*", "*", "?", "?", "*", "?",], 
            ["?", "?", "*", "?", "?", "?", "?", "*", "?", "?",]]

     Obviously the grid will be randomly generated on run to fit 
     within the bounds of the terminal window.
     Notice that you can access the state of any tile on the grid with
     the simple command "board[x][y]"
     i.e. a = board[0][0] makes a == "?", a = board[0][9] makes a == "*"
     NOTE: I subsequently changed the code to use nested dictionaries,
     rather than lists to represent the board. The general idea is
     still the same...
     If we use a dictionary rather than a list in the following
     function, we will get a KeyError if we try to access a 
     negative index, assuming we construct the dictionary such
     that it has identical indexes to the list equivalent.
     i.e. dictionary = {0:{0:" ", 1:" ", 2"*"}, 1:{0:"*", 1:" ", 2:" "}}
     This will be helpful, as it will negate the need to explicitly
     check whether a particular coordinate is legitimate. i.e. a 
     dictionary won't match negative values for x, y, but a list will..."""

    """ At the moment we are getting the window size before curses is
     initialised, which means that we have to use "stty size". If 
     we can move this code into the curses section, we can use the
     built in window.getmaxyx(). This will make it easier to use
     windows smaller than the size of the terminal for the game, 
     which will in turn allow us to add timers and minecounts."""

    def __init__(self):
        global width, height
        width = 10
        height = 10

    def options(self):
        totalTiles = width * height

        #possible choices of tile: either "*" or " "
        self.mineNumber = int(setDifficulty * totalTiles)
        choices = list(self.mineNumber * "*")
        choices.extend(list((totalTiles-len(choices))*" "))
        return choices

    # For every x and y, check all the squares around to see if there is a mine,
    # add together the number of mines touching the original square and replace
    # the original square with the final count.
    def numberise(self, board):
        for x in xrange(width):
            for y in xrange(height):
                count = 0
                if board[x][y] != "*":
                        for i in xrange(-1, 2):
                            for n in xrange(-1, 2):
                                try:
                                    if board[x+i][y+n] == "*":
                                        count += 1
                                except KeyError:
                                    pass
                        if count != 0:
                            board[x][y] = str(count)

    def create(self):
        self.mineCoords = []
        choices = self.options()
        board = {}
        for i in xrange(0, width):
            board[i] = {}
            for n in xrange(0, height):
                board[i][n] = choices.pop(choices.index(random.choice(choices)))
                if board[i][n] == "*":
                    self.mineCoords.append([i,n])

        self.numberise(board)
        for i in xrange(width):
            for n in xrange(height):
                minecraftAddBlock(i, n, 1, board[i][n])

        return board

    def visibleScreen(self):
        board = {}
        for i in xrange(0, width):
            board[i] = {}
            for n in xrange(0, height):
                board[i][n] = " "
        return board

def minecraftAddBlock(X, Y, Z, mineAttribute):
    # This equates values passed through mineAttribute with the actual
    # block IDs from Minecraft.
    # 0 is Air, 5 is Wood Planks, 4 is cobblestone, coal is 16
    # Iron is 15, Gold is 14, Diamond is 56, Gold Block is 41, 
    # Diamond Block is 57
    mineDict = {"dirt":3, "*":46, " ":20, 0:0, "1":5, "2":4, "3":16, "4":15, "5":14, "6":56, "7":41, "8":57}
    mc.setBlock(X, Y, Z, mineDict[mineAttribute])

def explore(x, y, Z): # Z is capitalised because it doesn't
                              # need to be changed by the function.
    """ This is the bit that happens when you click on a blank square
     First it checks the squares directly around the clicked square
     If the square it checks is a number, it will display it, and
     if the square it checks is blank, it will add the blank square's
     coordinates to a list or dictionary, then it will keep doing the
     same process to all the coordinates in the list, deleting squares
     that have been checked, and adding new squares, until the list is
     empty. At that point, the area around the original square will be
     revealed, as you would expect to happen in minesweeper."""

    checked = [[x,y]]       # Has been checked and contains either a number or ' '
    toBeCentre = [[x, y]]   # Each point in this list will be checked on all sides for the above conditions
    centred = []            # These points have already been checked on all sides
    global cleared
    cleared = []

    while len(toBeCentre) > 0:
        X, Y = toBeCentre.pop(0)
        centred.append([X,Y])
        minecraftAddBlock(X, Y, Z, 0)
        if [X,Y] not in cleared:
            cleared.append([X,Y])
        for i in xrange(-1, 2):
            for n in xrange(-1, 2):

        # When I was writing this section, it wouldn't work, and wouldn't work
        # and then after changing it around a million times, suddenly it started working...
        # The only problem is that I don't actually know what I did to make it work... =P
                try:
                    if ((newBoard[X+i][Y+n] in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'])and ([X+i, Y+n] not in checked)):
                        minecraftAddBlock(X+i, Y+n, Z, 0) #newBoard[X+i][Y+n])
                        checked.append([X+i, Y+n])
                        if [X+i,Y+n] not in cleared:
                            cleared.append([X+i,Y+n])

                    elif newBoard[X+i][Y+n] == " ":
                        if (([X+i, Y+n] not in checked) and ([X+i, Y+n] not in toBeCentre)):
                            toBeCentre.append([X+i, Y+n])
                            checked.append([X+i, Y+n])
                except KeyError:
                    pass

class WinningCheckThread (threading.Thread):

    def __init__(self,  mineCoords, mineNumber, z):
        self.mineCoords = mineCoords
        self.mineNumber = mineNumber
        self.z = z
        threading.Thread.__init__(self)

    def run(self):
        global running
        running = True
        mc = minecraft.Minecraft.create()
        while running:
            ### This is the winning condition... ###
            flagCount = 0
            correctFlagCount = 0

            for x in xrange(width):
                for y in xrange(height):
                    if mc.getBlock(x, y, 0-1) == 50:
                        flagCount += 1
                        if [x,y] in self.mineCoords:
                            correctFlagCount += 1

            if  (self.mineNumber == correctFlagCount) and (self.mineNumber == flagCount):
                for x in xrange(width):
                     for y in xrange(height):
                         mc.setBlock(x, y, self.z, 20)

                mc.postToChat("You Win!!!")
                running = False
                sys.exit()

class BlockCheckThread(threading.Thread):

    def __init__(self):

        threading.Thread.__init__(self)
    def run(self):
        global running
        running = True
        mc = minecraft.Minecraft.create()
        while running:

            event = mc.events.pollBlockHits()

            if event:
                eventSplit = str(event[0]).split()
                eventSplit = [eventSplit[1][0], eventSplit[2][0], eventSplit[3][0]]
                cursorX, cursorY, cursorZ = eventSplit
                cursorX = int(cursorX)
                cursorY = int(cursorY)
                cursorZ = int(cursorZ)
                if newBoard[cursorX][cursorY] == "*":
                    for y in xrange(height):
                        for x in xrange(width):
                            # This bit of code's dodgy, because it relies on the 
                            # creation of "newBoard" external to the function...
                            mc.setBlock(x, y, z, 0) # (If you hit a mine it clears the board.)
                    mc.postToChat("You Lose!")
                    running = False
                    sys.exit()

                if newBoard[cursorX][cursorY] in ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']:
                    #visibleScreen[x][y] = newBoard[x][y]
                    mc.setBlock(cursorX, cursorY, cursorZ, 0) # We just remove the top layer.

                if newBoard[cursorX][cursorY] == " ":
                    explore(cursorX, cursorY, cursorZ)

#def main():
global running
running = True
mc = minecraft.Minecraft.create()
board = board()
newBoard = board.create()
visibleScreen = board.visibleScreen()

for x in xrange(width):
    for y in xrange(height):
        mc.setBlock(x,y,-1,0)

z = 0 # For now... We can make this dynamic later.
for y in xrange(height):
   for x in xrange(width):
       # This bit of code's dodgy, because it relies on the 
       # creation of "visibleScreen" external to the function...
       minecraftAddBlock(x, y, z, "dirt")

WinningCheck = WinningCheckThread(board.mineCoords, board.mineNumber, z)
BlockCheck = BlockCheckThread()
BlockCheck.daemon
WinningCheck.start()
BlockCheck.start()

#main()
Advertisements

3 thoughts on “Minesweeper in Minecraft!

  1. Seeing it like that makes me realise how many rubbish comments I’ve left in the code… =P I’m still working on it, so I’ll let you know when I release the new improved version.

    • Heh, my code is usually littered with abandoned statements and too many commented print statements. If you have an update though, send it to me and I’ll update the post. I’m capturing all the source here to make it easy for others to find them — and just in case they disappear.. 🙂

      • I’ll let you know when I release the updated version. I’m adding a few new features, but I’m at the stage where the new features are breaking the code.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s