So, this thread and a few other factors finally convinced me to throw together a tech demo for an idea that's been floating around in my head a while. Using Python, libtcod, and a decent tutorial I managed to obtain the following (spoilered for length):
You'll need libtcod and the libtcod python bindings (libtcodpy.py) to run it.
Arrow keys, or vi keys to move.
import libtcodpy as libtcod
import math
#actual size of the window
SCREEN_WIDTH = 40
SCREEN_HEIGHT = 40
#size of the map
MAP_WIDTH = 80
MAP_HEIGHT = 45
START_X = 0
START_Y = 0
LIMIT_FPS = 20 #20 frames-per-second maximum
delta_linex, delta_liney = 0.0, 0.0 # globals used for delta line calculations
cur_linex, cur_liney = 0.0, 0.0
color_dark_wall = libtcod.Color(0, 0, 100)
color_dark_ground = libtcod.Color(50, 50, 150)
color_black = libtcod.Color(0,0,0)
# class acting as an enum for the cardinal directions
class Dirs:
N = 8
NE = 9
E = 6
SE = 3
S = 2
SW = 1
W = 4
NW = 7
C = 5
# mapping from difference in coords to directions
ctodir = { (0,-1) : Dirs.N,
(1,-1) : Dirs.NE,
(1, 0) : Dirs.E,
(1, 1) : Dirs.SE,
(0, 1) : Dirs.S,
(-1,1) : Dirs.SW,
(-1,0) : Dirs.W,
(-1,-1): Dirs.NW,
(0, 0) : Dirs.C}
# mapping from coords to directions
dirtoc = { Dirs.N : (0,-1),
Dirs.NE : (1,-1),
Dirs.E : (1, 0),
Dirs.SE : (1, 1),
Dirs.S : (0, 1),
Dirs.SW : (-1,1),
Dirs.W : (-1,0),
Dirs.NW : (-1,-1),
Dirs.C : (0, 0) }
class Tile:
#a tile of the map and its properties
def __init__(self, x, y, char, color, blocked, block_sight = None):
self.blocked = blocked
self.char = char
self.color = color
self.linked = False
self.x = x
self.y = y
self.links = { Dirs.N : [-1,-1],
Dirs.NE : [-1,-1],
Dirs.E : [-1,-1],
Dirs.SE : [-1,-1],
Dirs.S : [-1,-1],
Dirs.SW : [-1,-1],
Dirs.W : [-1,-1],
Dirs.NW : [-1,-1],
Dirs.C : [0, 0] }
self.contents = list()
#by default, if a tile is blocked, it also blocks sight
if block_sight is None: block_sight = blocked
self.block_sight = block_sight
def neighbor(self, dir):
x = self.x
y = self.y
#if linked, find the appropriate neighbor
if self.linked:
if not self.links[dir] == [-1,-1]:
#print("Link dir:", dir, self.links[dir])
return self.links[dir]
if dir == Dirs.N:
return (x,y-1)
elif dir == Dirs.NE:
return (x+1,y-1)
elif dir == Dirs.E:
return (x+1,y)
elif dir == Dirs.SE:
return (x+1,y+1)
elif dir == Dirs.S:
return (x,y+1)
elif dir == Dirs.SW:
return (x-1,y+1)
elif dir == Dirs.W:
return (x-1,y)
elif dir == Dirs.NW:
return (x-1,y-1)
else:
return (x,y)
def unlink(self):
self.linked = False
self.links = { Dirs.N : [-1,-1],
Dirs.NE : [-1,-1],
Dirs.E : [-1,-1],
Dirs.SE : [-1,-1],
Dirs.S : [-1,-1],
Dirs.SW : [-1,-1],
Dirs.W : [-1,-1],
Dirs.NW : [-1,-1],
Dirs.C : [0, 0] }
def linkdir(self, dir, x, y):
self.linked = True
self.links[dir] = [x,y]
print self.x, self.y, " is now linked (", dir, ") to ", x, y
def link(self, x, y):
self.linked = True
print("Linking to ", x , y)
for d in range(1,10): # the eight cardinal directions
tx, ty = self.neighbor(d)
tile = map[tx][ty]
#print d, tile
if( not tile == None and not tile == self):
if d == Dirs.N:
tile.linkdir(Dirs.S, x, y)
elif d == Dirs.NE:
tile.linkdir(Dirs.SW, x, y)
elif d == Dirs.E:
tile.linkdir(Dirs.W, x, y)
elif d == Dirs.SE:
tile.linkdir(Dirs.NW, x, y)
elif d == Dirs.S:
tile.linkdir(Dirs.N, x, y)
elif d == Dirs.SW:
tile.linkdir(Dirs.NE, x, y)
elif d == Dirs.W:
tile.linkdir(Dirs.E, x, y)
elif d == Dirs.NW:
tile.linkdir(Dirs.SE, x, y)
def empty(self):
return not self.contents
def draw_neighbors(self, dirs, x, y, rlist):
self.draw(x, y)
sx, sy = x,y
for d in dirs: #draw neighbors in the four cardinal dirs
nx, ny = self.neighbor(d)
dx, dy = dirtoc[d]
sx = x+dx
sy = y+dy
if( in_map_bounds(nx,ny) and in_screen_bounds(sx,sy) and not rlist[sx][sy] \
and map[nx][ny].blocked):
tile = map[nx][ny]
#tile.draw(sx, sy)
rlist[sx][sy] = True
libtcod.console_put_char_ex(con, sx, sy, tile.char, tile.color, libtcod.black)
def draw(self, x, y):
libtcod.console_put_char_ex(con, x, y, self.char, self.color, libtcod.black)
for o in self.contents:
o.draw(x, y)
class Object:
#this is a generic object: the player, a monster, an item, the stairs...
#it's always represented by a character on screen.
def __init__(self, x, y, char, color):
self.x = x
self.y = y
self.char = char
self.color = color
self.viewdist = 15
def move(self, dx, dy):
#move by the given amount, if the destination is not blocked
nx = self.x + dx
ny = self.y + dy
if nx < MAP_WIDTH and nx >= 0 \
and ny < MAP_HEIGHT and ny >= 0 \
and not map[nx][ny].blocked:
if(self in map[self.x][self.y].contents):
map[self.x][self.y].contents.remove(self)
self.x = nx
self.y = ny
map[self.x][self.y].contents.append(self)
def movedir(self, dir):
nx, ny = map[self.x][self.y].neighbor(dir);
if in_map_bounds(nx, ny):
tile = map[nx][ny]
if not tile.blocked:
if(self in map[self.x][self.y].contents):
map[self.x][self.y].contents.remove(self)
self.x = nx
self.y = ny
tile.contents.append(self)
#print "Now at ", self.x, self.y, (dir)
def draw(self, x, y):
#set the color and then draw the character that represents this object at its position
libtcod.console_set_foreground_color(con, self.color)
libtcod.console_put_char(con, x, y, self.char, libtcod.BKGND_NONE)
def clear(self):
#erase the character that represents this object
libtcod.console_put_char(con, self.x, self.y, ' ', libtcod.BKGND_NONE)
def make_map_from_file(file_string):
global map
global START_X, START_Y
file = open(file_string, 'r')
print(file)
line = file.readline()
mw, mh = line.rsplit('x')
MAP_WIDTH = int(mw)
MAP_HEIGHT = int(mh)
print(MAP_WIDTH,MAP_HEIGHT)
map = []
#fill map with "blocked" tiles
for x in range(0,MAP_WIDTH):
map.append([])
for y in range(0,MAP_HEIGHT):
map[x].append(Tile(x,y,'#',libtcod.light_gray,True,True))
for y in range(MAP_HEIGHT):
line = file.readline()
for x in range(MAP_WIDTH):
char = line[x]
if char == '.':
map[x][y].char = '.'
map[x][y].blocked = False
map[x][y].block_sight = False
map[x][y].color = libtcod.dark_gray
#Now the map is loaded, process start location and links
while not line == '':
keys = line.rsplit(':')
if line.startswith('l'): #link
if(not keys == ['']):
print keys
dir = int(keys[1])
st = keys[2].split(',')
dt = keys[3].split(',')
map[int(st[0])][int(st[1])].linkdir(dir, int(dt[0]),int(dt[1]))
elif line.startswith('@'):
sx, sy = keys[1].split(',')
START_X, START_Y = int(sx), int(sy)
print "Player Start@" +sx+"," + sy
line = file.readline()
def make_map():
make_map_from_file('map.txt')
def in_map_bounds(x, y):
try:
map[x][y]
return True
except:
return False
# if x >= 0 and x <= MAP_WIDTH-1 \
# and y >= 0 and y <= MAP_HEIGHT-1:
# return True
# else:
# return False
def in_screen_bounds(x, y):
if x >= 0 and x <= SCREEN_WIDTH-1 \
and y >= 0 and y <= SCREEN_HEIGHT-1:
return True
else:
return False
def dline_init(dx, dy):
global delta_linex, delta_liney, cur_linex, cur_liney
delta_linex, delta_liney = dx, dy
cur_linex, cur_liney = 0.0, 0.0
def dline_step():
global delta_linex, delta_liney, cur_linex, cur_liney
ox, oy = cur_linex, cur_liney
dx, dy = cur_linex-ox, cur_liney-oy
while int(dx) == 0 and int(dy) == 0:
cur_linex += delta_linex
cur_liney += delta_liney
dx, dy = cur_linex-ox, cur_liney-oy
ox, oy = cur_linex, cur_liney
return int(dx), int(dy)
def safe_remove(l, e1, e2, e3):
#safely remove three elements from list l
if e1 in l:
l.remove(e1)
if e2 in l:
l.remove(e2)
if e3 in l:
l.remove(e3)
def render_line(dx, dy, range, rlist):
# Renders a line from the center of the screen out, each cell
# being dx and dy spaces away from the last
rsx, rsy = float((SCREEN_WIDTH/2)+0.5), float((SCREEN_HEIGHT/2)+0.5)
rmx, rmy = float(player.x)+0.5, float(player.y)+0.5
hsx, hsy = int(rsx), int(rsy)
osx, osy = hsx, hsy
cx, cy = hsx-osx, hsy-osy
count = 0
while count < range \
and in_map_bounds(int(rmx), int(rmy)) \
and in_screen_bounds(hsx, hsy):
if (cx == 0 and cy == 0):
pass
else:
if( not rlist[int(rsx)][int(rsy)] ):
#draw cell
map[int(rmx)][int(rmy)].draw(int(rsx), int(rsy))
#draw neighboring walls
dirs = [1,3,7,9]
# should change this to use dx, dy for more accurate values
if not (cx == 0): #moving horizontally
dirs.append(8)
dirs.append(2)
if not (cy == 0): #moving vertically
dirs.append(4)
dirs.append(6)
map[int(rmx)][int(rmy)].draw_neighbors( dirs, int(rsx), int(rsy), rlist)
rlist[int(rsx)][int(rsy)] = True
rsx, rsy = rsx+dx, rsy+dy
osx, osy = hsx, hsy
hsx, hsy = int(rsx), int(rsy)
cx, cy = hsx-osx, hsy-osy
rmx, rmy = map[int(rmx)][int(rmy)].neighbor( ctodir[(cx, cy)] )
rmx, rmy = rmx+0.5, rmy+0.5
if( map[int(rmx)][int(rmy)].blocked ):
map[int(rmx)][int(rmy)].draw(int(rsx),int(rsy))
return
#rmx, rmy = rmx+dx, rmy+dy
#print(dx,dy)
count+=1
def render_all():
global color_light_wall
global color_light_ground
libtcod.console_clear(con)
rlist = [[False for col in range(SCREEN_HEIGHT)] for row in range(SCREEN_WIDTH)]
# rlist used to track which cells have already been rendered
# for x in range(0,SCREEN_WIDTH):
# render_line(x, 0, rlist)
# render_line(x, SCREEN_HEIGHT, rlist)
# for y in range(0, SCREEN_HEIGHT):
# render_line(0, y, rlist)
# render_line(SCREEN_WIDTH-1, y, rlist)
map[player.x][player.y].draw(int(SCREEN_WIDTH/2), int(SCREEN_HEIGHT/2))
rlist[int(SCREEN_WIDTH/2)][int(SCREEN_HEIGHT/2)] = True
map[player.x][player.y].draw_neighbors( [1,2,3,4,5,6,7,8,9], int(SCREEN_WIDTH/2), int(SCREEN_HEIGHT/2), rlist)
i = 0
while i < 360:
r = math.radians(i)
dx, dy = math.cos(r), math.sin(r)
render_line(dx,dy, 15, rlist)
i+=5
#blit the contents of "con" to the root console
libtcod.console_blit(con, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0)
def handle_keys():
#key = libtcod.console_check_for_keypress() #real-time
key = libtcod.console_wait_for_keypress(True) #turn-based
char = chr(key.c)
if key.vk == libtcod.KEY_ENTER and key.lalt:
#Alt+Enter: toggle fullscreen
libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen())
elif key.vk == libtcod.KEY_ESCAPE:
return True #exit game
elif key.vk == libtcod.KEY_F12:
libtcod.sys_save_screenshot()
#movement keys (arrows)
if libtcod.console_is_key_pressed(libtcod.KEY_UP):
player.movedir(Dirs.N)
elif libtcod.console_is_key_pressed(libtcod.KEY_DOWN):
player.movedir(Dirs.S)
elif libtcod.console_is_key_pressed(libtcod.KEY_LEFT):
player.movedir(Dirs.W)
elif libtcod.console_is_key_pressed(libtcod.KEY_RIGHT):
player.movedir(Dirs.E)
# movement keys (vi)
elif char == 'k':
player.movedir(Dirs.N)
elif char == 'j':
player.movedir(Dirs.S)
elif char == 'l':
player.movedir(Dirs.E)
elif char == 'h':
player.movedir(Dirs.W)
elif char == 'b':
player.movedir(Dirs.SW)
elif char == 'n':
player.movedir(Dirs.SE)
elif char == 'y':
player.movedir(Dirs.NW)
elif char == 'u':
player.movedir(Dirs.NE)
#############################################
# Main Loop
#############################################
libtcod.console_set_custom_font('font.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD)
libtcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, 'M&M Tech Demo', False)
libtcod.sys_set_fps(LIMIT_FPS)
con = libtcod.console_new(SCREEN_WIDTH, SCREEN_HEIGHT)
#generate map (at this point it's not drawn to the screen)
make_map()
#create a staircase (non functional)
stair = Object(START_X,START_Y, '<', libtcod.gold)
stair.move(0,0)
#create object representing the player
#player = Object(SCREEN_WIDTH/2, SCREEN_HEIGHT/2, '@', libtcod.white)
player = Object(START_X,START_Y, '@', libtcod.white)
player.move(0, 0)
#the list of objects with those two
objects = [stair, player]
first_time = True #for turn-based games
while not libtcod.console_is_window_closed():
#erase all objects at their old locations, before they move
#for object in objects:
#object.clear()
#handle keys and exit game if needed
if not first_time: #for turn-based games
exit = handle_keys()
if exit:
break
first_time = False #for turn-based games
#render the screen
render_all()
libtcod.console_flush()
I may or may not take this farther, I don't want to do so without committing properly to it, and I'm not sure I've got the time to do so.
Whoops, left out two rather important parts, sorry.
You'll need to put a libtcod font png file in the same directory. Libtcod comes with a 'data/fonts' folder that has a bunch, I recommend one of the 10x10s. Just put it next to the main file and rename it 'font.png'