We want to make a turn-based strategy game, based on a hexagonal grid.
Wait, how do we even lay out a hexagonal grid in kivy?
http://playtechs.blogspot.com/2007/04/hex-grids.html
Also
http://gamedev.stackexchange.com/questions/15881/hexagonal-grid-tiles-tutorials
Ignore device orientation.
Let's lay out a grid. First we need to draw hexagons. Clone gamecamp. change the object names. Can we lay out the basic screen?
In [ ]:
%%file strategygame.kv
#:include debug.kv
<StrategyGame>:
BoxLayout:
orientation: 'horizontal'
DebugLabel:
text: 'Main map'
size_hint: .75, 1
BoxLayout:
orientation: 'vertical'
size_hint: .25, 1
DebugLabel:
text: 'status'
size_hint: 1, .66
DebugLabel:
text: 'mini-map'
size_hint: 1, .33
In [ ]:
%%file main.py
from kivy.app import App
from kivy.uix.floatlayout import FloatLayout
class StrategyGame(FloatLayout):
pass
class StrategyGameApp(App):
def build(self):
return StrategyGame()
if __name__ == '__main__':
StrategyGameApp().run()
In [ ]:
In [ ]:
#!python main.py
Let's make a gridlayout in the game area, and give it a label so we can refer to it from our code. We are going to build a rectangular grid
In [ ]:
%%file strategygame.kv
#:include debug.kv
<StrategyGame>:
main_map: _main_map
BoxLayout:
orientation: 'horizontal'
GridLayout:
id: _main_map
cols: 10
size_hint: .75, 1
BoxLayout:
orientation: 'vertical'
size_hint: .25, 1
DebugLabel:
text: 'status'
size_hint: 1, .66
DebugLabel:
text: 'mini-map'
size_hint: 1, .33
Now let's hook to that from code. We will do this in __init__ in our main StrategyGame class. Let's make a loop to add buttons to the root widget. We will use properties in the kv file to determine how many rows/cols to add. We will also need to refer to various objects in the widget hierarchy in the code.
You can refer to the top-level object (non-indented) as root in the kv file, but it we want to refer to conveniently elsewhere in the code, we need to use an id.
id: _game
and then from any widget we want to refer to it, we do a
game: _game
We'll use this to access the number of regions to layout in the grid via map_cols and map_rows from the main game widget.
Fill in grids: it fills it in by default, left to right, top to bottom, and give them coordinate references.
In [ ]:
%%file strategygame.kv
#:include debug.kv
<StrategyGame>:
id: _game
main_map: _main_map
map_rows: 10
map_cols: 10
BoxLayout:
orientation: 'horizontal'
GridLayout:
id: _main_map
game: _game
cols: root.map_cols
size_hint: .75, 1
BoxLayout:
orientation: 'vertical'
size_hint: .25, 1
DebugLabel:
text: 'status'
size_hint: 1, .66
DebugLabel:
text: 'mini-map'
size_hint: 1, .33
In [ ]:
# %load main.py
from kivy.app import App
from kivy import properties
from kivy.uix import button
from kivy.uix.floatlayout import FloatLayout
class StrategyGame(FloatLayout):
main_map = properties.ObjectProperty(None)
map_rows = properties.NumericProperty(0)
map_cols = properties.NumericProperty(0)
def __init__(self, **kwargs):
super(StrategyGame, self).__init__(**kwargs)
number_of_regions = self.map_rows * self.map_cols
for region in xrange(0, number_of_regions):
row = region / self.map_cols
col = region % self.map_cols
self.main_map.add_widget(button.Button(text='({}, {})'.format(row, col)))
class StrategyGameApp(App):
def build(self):
return StrategyGame()
if __name__ == '__main__':
StrategyGameApp().run()
We draw alternating rows with the patterns:
/ \
| |
| |
and
\ /
|
|
In [ ]:
%%file strategygame.kv
#:include debug.kv
<StrategyGame>:
id: _game
main_map: _main_map
map_rows: 10
map_cols: 10
BoxLayout:
orientation: 'horizontal'
GridLayout:
id: _main_map
game: _game
cols: root.map_cols
size_hint: .75, 1
BoxLayout:
orientation: 'vertical'
size_hint: .25, 1
DebugLabel:
text: 'status'
size_hint: 1, .66
DebugLabel:
text: 'mini-map'
size_hint: 1, .33
In [ ]:
%%file main.py
from kivy.app import App
from kivy import properties
from kivy.uix import button
from kivy.uix.floatlayout import FloatLayout
class StrategyGame(FloatLayout):
main_map = properties.ObjectProperty(None)
map_rows = properties.NumericProperty(0)
map_cols = properties.NumericProperty(0)
def __init__(self, **kwargs):
super(StrategyGame, self).__init__(**kwargs)
number_of_regions = self.map_rows * self.map_cols
for region in xrange(0, number_of_regions):
row = region / self.map_cols
col = region % self.map_cols
self.main_map.add_widget(button.Button(text='({}, {})'.format(row, col)))
class StrategyGameApp(App):
def build(self):
return StrategyGame()
if __name__ == '__main__':
StrategyGameApp().run()
In [ ]:
In [ ]:
Wait, that doesn't draw where we think it should.'
In [ ]:
%%file main.py
import collections
from kivy.app import App
from kivy import properties
from kivy import graphics
from kivy.uix import label
from kivy.uix.floatlayout import FloatLayout
MapCoords = collections.namedtuple('MapCoords', ['row', 'col'])
class StrategyGame(FloatLayout):
main_map = properties.ObjectProperty(None)
map_rows = properties.NumericProperty(0)
map_cols = properties.NumericProperty(0)
def __init__(self, **kwargs):
super(StrategyGame, self).__init__(**kwargs)
number_of_regions = self.map_rows * self.map_cols
for region in xrange(0, number_of_regions):
row = region / self.map_cols
col = region % self.map_cols
self.main_map.add_widget(HexMapCell(row=row, col=col))
class HexMapCell(label.Label):
def __init__(self, row=0, col=0, **kwargs):
self.region_in_map = MapCoords(row, col)
super(HexMapCell, self).__init__(**kwargs)
self.draw_hex_edge()
def draw_hex_edge(self):
edge = ''
if self.region_in_map.col % 2 == 0:
row_mod = self.region_in_map.row % 6
if row_mod == 0:
edge = 'BU'
with self.canvas:
graphics.Color(1, 1, 1, 1)
graphics.Line(points=[self.x, self.y, self.width + self.x, self.height + self.y])
elif row_mod in (1, 2):
edge = 'L '
elif row_mod == 3:
edge = 'TD'
elif row_mod in (4, 5):
edge = ' R'
else:
row_mod = self.region_in_map.row % 6
if row_mod == 0:
edge = 'TD'
elif row_mod in (1, 2):
edge = ' R'
elif row_mod == 3:
edge = 'BU'
elif row_mod in (4, 5):
edge = 'L '
self.text = edge
class StrategyGameApp(App):
def build(self):
return StrategyGame()
if __name__ == '__main__':
StrategyGameApp().run()
In [ ]:
%%file main.py
from kivy.app import App
from kivy.uix.gridlayout import GridLayout
from kivy.lang import Builder
from kivy.properties import NumericProperty
class HelloWorld(GridLayout):
cols = NumericProperty(4)
def __init__(self, **kw):
super(HelloWorld, self).__init__(**kw)
self.add_widget(self.BU())
self.add_widget(self.TD())
self.add_widget(self.BU())
self.add_widget(self.TD())
def BU(self):
return Builder.load_string('''
Label:
canvas:
Color:
rgba: (1,1,1,1)
Line:
points: (self.x, self.y, self.right, self.top)
width: 2''')
def TD(self):
return Builder.load_string('''
Label:
canvas:
Color:
rgba: (1,1,1,1)
Line:
points: (self.x, self.top, self.right, self.y)
width: 2''')
def L(self):
return Builder.load_string('''
Label:
canvas:
Color:
rgba: (1,1,1,1)
Line:
points: (self.x, self.y, self.x, self.top)
width: 2''')
def R(self):
return Builder.load_string('''
Label:
canvas:
Color:
rgba: (1,1,1,1)
Line:
points: (self.right, self.y, self.right, self.top)
width: 2''')
class HelloWorldApp(App):
def build(self):
return HelloWorld()
if __name__ == '__main__':
HelloWorldApp().run()
In [ ]:
# %load strategygame.kv
#:include debug.kv
<StrategyGame>:
id: _game
main_map: _main_map
map_rows: 10
map_cols: 10
BoxLayout:
orientation: 'horizontal'
GridLayout:
id: _main_map
game: _game
cols: root.map_cols
size_hint: .75, 1
BoxLayout:
orientation: 'vertical'
size_hint: .25, 1
DebugLabel:
text: 'status'
size_hint: 1, .66
DebugLabel:
text: 'mini-map'
size_hint: 1, .33
We tried adding the ability to style our widgets for to draw the Hexmap using lines like this
BU: /
TD: \
L: |
R: |
We get hexs now, but we need to figure out how to do the aspect ratio properly now...for a spiky top, we need each grid widget to have aspec ratio $$\sqrt{3}\cdot height = width$$
In [ ]:
%%file strategygame.kv
#:include debug.kv
<StrategyGame>:
id: _game
main_map: _main_map
map_rows: 10
map_cols: 10
BoxLayout:
orientation: 'horizontal'
GridLayout:
id: _main_map
game: _game
cols: root.map_cols
size_hint: .75, 1
BoxLayout:
orientation: 'vertical'
size_hint: .25, 1
DebugLabel:
text: 'status'
size_hint: 1, .66
DebugLabel:
text: 'mini-map'
size_hint: 1, .33
<BU>:
canvas:
Color:
rgba: (1,1,1,1)
Line:
points: (self.x, self.y, self.right, self.top)
width: 2
<TD>:
canvas:
Color:
rgba: (1,1,1,1)
Line:
points: (self.x, self.top, self.right, self.y)
width: 2
<L>:
canvas:
Color:
rgba: (1,1,1,1)
Line:
points: (self.x, self.y, self.x, self.top)
width: 2
<R>:
canvas:
Color:
rgba: (1,1,1,1)
Line:
points: (self.right, self.y, self.right, self.top)
width: 2
In [ ]:
%%file main.py
import collections
from kivy.app import App
from kivy import properties
from kivy import graphics
from kivy.uix import label
from kivy.uix.floatlayout import FloatLayout
import math
MapCoords = collections.namedtuple('MapCoords', ['row', 'col'])
class StrategyGame(FloatLayout):
main_map = properties.ObjectProperty(None)
map_rows = properties.NumericProperty(0)
map_cols = properties.NumericProperty(0)
def __init__(self, **kwargs):
super(StrategyGame, self).__init__(**kwargs)
number_of_regions = self.map_rows * self.map_cols
for region in xrange(0, number_of_regions):
row = region / self.map_cols
col = region % self.map_cols
self.main_map.add_widget(self.pick_hex_cell(row=row, col=col))
def pick_hex_cell(self, row, col):
row_mod = row % 6
if col % 2 == 0:
if row_mod == 0:
return BU()
elif row_mod in (1, 2):
return L()
elif row_mod == 3:
return TD()
elif row_mod in (4, 5):
return R()
else:
if row_mod == 0:
return TD()
elif row_mod in (1, 2):
return R()
elif row_mod == 3:
return BU()
elif row_mod in (4, 5):
return L()
class HexMapCell(label.Label):
def __init__(self, row=0, col=0, **kwargs):
self.region_in_map = MapCoords(row, col)
super(HexMapCell, self).__init__(**kwargs)
class BU(HexMapCell):
pass
class TD(HexMapCell):
pass
class L(HexMapCell):
pass
class R(HexMapCell):
pass
class StrategyGameApp(App):
def build(self):
return StrategyGame()
if __name__ == '__main__':
StrategyGameApp().run()
Replaced the debug labels and coloured the status and minimap two different ways.
In [ ]:
%%file strategygame.kv
#:import math math
<StrategyGame>:
id: _game
main_map: _main_map
map_rows: 30
map_cols: 10
BoxLayout:
orientation: 'horizontal'
GridLayout:
id: _main_map
game: _game
cols: root.map_cols
size_hint: .75, 1
BoxLayout:
orientation: 'vertical'
size_hint: .25, 1
Label:
id: _stats
text: 'status'
size_hint: 1, .66
canvas.before:
Color:
rgba: .49, .49, .81, 1
Rectangle:
pos: _stats.pos
size: _stats.size
Button:
text: 'mini-map'
size_hint: 1, .33
background_color: .75, .71, .99, 1
<BU>:
canvas:
Color:
rgba: (1,1,1,1)
Line:
points: (self.x, self.y, self.right, self.top)
width: 2
<TD>:
canvas:
Color:
rgba: (1,1,1,1)
Line:
points: (self.x, self.top, self.right, self.y)
width: 2
<L>:
canvas:
Color:
rgba: (1,1,1,1)
Line:
points: (self.x, self.y, self.x, self.top)
width: 2
<R>:
canvas:
Color:
rgba: (1,1,1,1)
Line:
points: (self.right, self.y, self.right, self.top)
width: 2
<HexMapCell>:
size_hint: 1, None
height: self.width / math.sqrt(3)
In [ ]:
%%file main.py
import collections
import random
from kivy import app, properties
from kivy.uix import button, label
from kivy.uix.floatlayout import FloatLayout
MapCoords = collections.namedtuple('MapCoords', ['row', 'col'])
class StrategyGame(FloatLayout):
main_map = properties.ObjectProperty(None)
map_rows = properties.NumericProperty(0)
map_cols = properties.NumericProperty(0)
def __init__(self, **kwargs):
super(StrategyGame, self).__init__(**kwargs)
number_of_regions = self.map_rows * self.map_cols
for region in xrange(0, number_of_regions):
row = region / self.map_cols
col = region % self.map_cols
# Add hex cells to make up the map.
hex_cell = self.pick_hex_cell(row=row, col=col)
self.main_map.add_widget(hex_cell)
# Add overlay conditionally.
if (row % 6 == 2 and col % 2 == 0) or (row % 6 == 5 and col % 2 == 1):
print('({}, {})'.format(row, col))
self.add_widget(HexMapControlCell(hex_bind=hex_cell))
@staticmethod
def pick_hex_cell(row, col):
row_mod = row % 6
if col % 2 == 0:
if row_mod == 0:
return BU()
elif row_mod in (1, 2):
return L()
elif row_mod == 3:
return TD()
elif row_mod in (4, 5):
return R()
else:
if row_mod == 0:
return TD()
elif row_mod in (1, 2):
return R()
elif row_mod == 3:
return BU()
elif row_mod in (4, 5):
return L()
class HexMapCell(label.Label):
def __init__(self, row=0, col=0, **kwargs):
super(HexMapCell, self).__init__(**kwargs)
self.coords = MapCoords(row, col)
class BU(HexMapCell):
pass
class TD(HexMapCell):
pass
class L(HexMapCell):
pass
class R(HexMapCell):
pass
class HexMapControlCell(button.Button):
def __init__(self, hex_bind=None, **kwargs):
super(HexMapControlCell, self).__init__(**kwargs)
self.hex_bind = hex_bind
self.background_color = random.random(), random.random(), random.random(), 1
self.bind(pos=self.reposition_control_cell, size=self.resize_control_cell)
self.text = '({}, {})'.format(self.hex_bind.coords.row, self.hex_bind.coords.col)
def reposition_control_cell(self, obj, value):
self.pos = self.hex_bind.pos
def resize_control_cell(self, obj, value):
self.height = self.hex_bind.height * 2
self.width = self.hex_bind.width * 2
class StrategyGameApp(app.App):
def build(self):
return StrategyGame()
if __name__ == '__main__':
StrategyGameApp().run()
Warning if you run this your will get an error!!
We should probably have a viewer and a hexagon map underneath!!
But not for now...let's make an overlay of boxes...and label them with an "offset-r)
Hmmmm....overlay...is...having...issues.
In [ ]:
%%file strategygame.kv
#:import math math
#:include debug.kv
<StrategyGame>:
id: _game
main_map: _main_map
map_rows: 30
map_cols: 10
BoxLayout:
orientation: 'horizontal'
GridLayout:
id: _main_map
game: _game
cols: root.map_cols
size_hint: .75, 1
BoxLayout:
orientation: 'vertical'
size_hint: .25, 1
Label:
id: _stats
text: 'status'
size_hint: 1, .66
canvas.before:
Color:
rgba: .49, .49, .81, 1
Rectangle:
pos: _stats.pos
size: _stats.size
Button:
text: 'mini-map'
size_hint: 1, .33
background_color: .75, .71, .99, 1
<Hex@Label>:
pos_hint: {'center_x':.5, 'center_y':.5}
canvas.after:
Color:
rgba: 1,1,1,1
Ellipse:
segments: 6
pos: self.pos
size: min(self.width, self.height), min(self.width, self.height)
<HexMapCell>:
size_hint: 1, None
height: self.width / math.sqrt(3)
In [ ]:
%%file main.py
import collections
import random
import math
from kivy import app, properties
from kivy.uix import button, label
from kivy.uix.floatlayout import FloatLayout
from kivy.graphics import Color, Ellipse, Line
from kivy.logger import Logger
MapCoords = collections.namedtuple('MapCoords', ['row', 'col'])
class StrategyGame(FloatLayout):
main_map = properties.ObjectProperty(None)
map_rows = properties.NumericProperty(0)
map_cols = properties.NumericProperty(0)
def __init__(self, **kwargs):
super(StrategyGame, self).__init__(**kwargs)
number_of_regions = self.map_rows * self.map_cols
for region in xrange(0, number_of_regions):
row = region / self.map_cols
col = region % self.map_cols
# Add hex cells to make up the map.
hex_cell = self.pick_hex_cell(row=row, col=col)
self.main_map.add_widget(hex_cell)
# Add overlay conditionally.
if (row % 6 == 1 and col % 2 == 1) or (row % 6 == 4 and col % 2 == 0):
print('({}, {})'.format(row, col))
#radius = math.sqrt(hex_cell.width**2 + hex_cell.height**2)
radius = 2*hex_cell.height
with hex_cell.canvas.after:
Color(1,0,1,1)
hex_cell.ell = Line(circle=(hex_cell.x, hex_cell.y,radius, 0, 360, 6), width=2)
hex_cell.bind(pos=hex_cell.update_pos, size=hex_cell.update_pos)
@staticmethod
def pick_hex_cell(row, col):
row_mod = row % 6
if col % 2 == 0:
if row_mod == 0:
return BU()
elif row_mod in (1, 2):
return L()
elif row_mod == 3:
return TD()
elif row_mod in (4, 5):
return R()
else:
if row_mod == 0:
return TD()
elif row_mod in (1, 2):
return R()
elif row_mod == 3:
return BU()
elif row_mod in (4, 5):
return L()
class HexMapCell(label.Label):
def __init__(self, row=0, col=0, **kwargs):
super(HexMapCell, self).__init__(**kwargs)
self.coords = MapCoords(row, col)
def update_pos(self, instance, value):
Logger.info("StratGame: {}".format(instance))
#radius = math.sqrt(self.width**2 + self.height**2)
radius = 2*self.height
self.ell.circle = (self.x, self.y, radius, 0, 360, 6)
class BU(HexMapCell):
pass
class TD(HexMapCell):
pass
class L(HexMapCell):
pass
class R(HexMapCell):
pass
class HexMapControlCell(button.Button):
def __init__(self, hex_bind=None, **kwargs):
super(HexMapControlCell, self).__init__(**kwargs)
self.hex_bind = hex_bind
self.background_color = random.random(), random.random(), random.random(), 1
self.bind(pos=self.reposition_control_cell, size=self.resize_control_cell)
self.text = '({}, {})'.format(self.hex_bind.coords.row, self.hex_bind.coords.col)
def reposition_control_cell(self, obj, value):
self.pos = self.hex_bind.pos
def resize_control_cell(self, obj, value):
self.height = self.hex_bind.height * 2
self.width = self.hex_bind.width * 2
class StrategyGameApp(app.App):
def build(self):
return StrategyGame()
if __name__ == '__main__':
StrategyGameApp().run()
What did we learn?
Cleaned some stuff up and added colours to the hexagons.
In [ ]: