# TOFU
# Copyright (C) 2005 Jean-Baptiste LAMY
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

# This is a small Tk-based demo using Tofu.
# To run it, execute the run_demo.py script.
# Try   python ./run_demo.py --help   for more info.


import sys, random, Tkinter
import tofu


# Tofu provide some base classes that you must extend, and finally you call
# tofu.init() and pass these classes to it.

# The first class is the Level class. A level is a level in the game.

class Level(tofu.Level):
  def __init__(self, filename):
    
    # Don't forget to call Tofu's implmentation when you override a method!!!
    
    tofu.Level.__init__(self, filename)
    
    print "* Demo * creating level %s..." % filename
    
    # The level contains a 10x10 square array. 3 of them are randomly choosen to
    # contain a wall.
    
    self.walls     = [0] * 100
    for i in range(3): self.walls[random.randrange(0, 100)] = 1
    
    # These attribute are used when drawing the level in the Tk Canvas.
    
    self.canvas     = None
    self.level_tags = []
    
    # Add a bot (see the Bot class below)
    
    if filename == "1_0":
      bot = Bot()
      self.add_mobile(bot)
      bot.y = 6

  # When we load a level, if the level is not found, we create a new one.
  # (By default, an error is risen)
  
  def load(clazz, filename):
    try:               return tofu.Level.load(filename)
    except ValueError: return Level(filename)
  load = classmethod(load)
  
  # Returns the level located next to this one (e.g. if delta_x = 1 and delta_y = 0,
  # returns the level at the left of this one).
  
  def neighbour(self, delta_x, delta_y):
    x, y = map(int, self.filename.split("_"))
    x += delta_x
    y += delta_y
    return Level.get("%s_%s" % (x, y))
  
  def __repr__(self):
    s = "level %s : \n" % (self.filename,)
    for y in xrange(10):
      for x in xrange(10): s += str(self.walls[x + y * 10]) + " "
      s += "\n"
    s += "characters :\n"
    for character in self.mobiles: s = s + "  " + `character` + "\n"
    return s

  # Sets the Tk canvas and draws the level in this canvas. If the canvas is None, the
  # level's canva items are destroyed.
  
  def set_canvas(self, canvas):
    if canvas:
      self.canvas = canvas
      self._draw()
    else:
      if self.canvas: self._clear()
      self.canvas = self.tag = None
      
    # Sets the canvas for all mobiles in the level.
    
    for mobile in self.mobiles: mobile.set_canvas(canvas)
    
  # Draw the level in the Tk canvas.
  # Canvas items are stored in self.level_tags for _clear()
  
  def _draw (self):
    self._clear()
    self.level_tags = [self.canvas.create_rectangle(x * 20, y * 20, (x + 1) * 20, (y + 1) * 20, fill = "black")
                       for x in xrange(10)
                       for y in xrange(10)
                       if self.walls[x + y * 10]
                       ]
    
  # Destroy all canvas items used to draw the level.
  
  def _clear(self): self.canvas.delete(*self.level_tags)
  
  # set_active is called when the level is activated or de-activated.
  # An active level is a level that contains at least a player, and thus
  # active levels should be drawn.
  
  def set_active(self, active):

    # tofu.GAME_INTERFACE is the current game interface, i.e. the visible part of the
    # game (see GameInterface below).
    # if tofu.GAME_INTERFACE is None, we are running the server, and so we don't have
    # to draw anything !
    
    if tofu.GAME_INTERFACE:
      
      # If the level is active, we set its canvas to the game interface Tk canvas in
      # order to make the level visible.
      # Else, we set the canvas to None to hide the level.
      
      if active: self.set_canvas(tofu.GAME_INTERFACE.canvas)
      else:      self.set_canvas(None)
      
    # Delegates AFTER, because set_active(0) can call save(), and we DON'T WANT to
    # save the Tk canvas !
    tofu.Level.set_active(self, active)
    
  def discard(self):
    # When the level is discarded, we hide it by setting the canavas to None.
    # Remove the canvas BEFORE discarding, because discard may save the character, and
    # we DON'T WANT to save the Tk canvas !
    self.set_canvas(None)
    
    tofu.Level.discard(self)
    
  # add_mobile is called when a mobile is added into the level.
  # When a mobile is added in the level, we set the mobile's canvas to the level canvas,
  # and thus if the level is displayed in the game interface canvas, the mobile will be
  # displayed too.
  
  def add_mobile(self, mobile):
    tofu.Level.add_mobile(self, mobile)
    mobile.set_canvas(self.canvas)
    
  # remove_mobile is called when a mobile is removed from the level.
  # When a mobile is removed from the level, we set the mobile's canvas to None,
  # in order to hide the mobile if it was displayed.
  
  def remove_mobile(self, mobile):
    mobile.set_canvas(None)
    tofu.Level.remove_mobile(self, mobile)
    
  # add_wall is called when a wall is added in the level.
  # After adding the wall, we redraw the level if needed.
  
  def add_wall(self, x, y):
    self.walls[x + y * 10] = 1
    if self.canvas: self._draw()
    

# A Player represent a human player.

class Player(tofu.Player):

  # Player.__init__ is called when a NEW player is created (NOT when an existent player
  # logon !).
  # filename and password are the player's login and password. An additional string data 
  # can be passed by the client, here we ignore them.
  
  # Player.__init__ must create at least one mobile for the player, then add this mobile
  # in a level and finally add the mobile in the player.
  
  def __init__(self, filename, password, client_side_data = ""):
    tofu.Player.__init__(self, filename, password)
    
    level     = Level.get("0_0")
    character = PlayerCharacter(self.filename)
    level.add_mobile(character)
    self .add_mobile(character)
    

# An Action is an action a mobile can accomplish.
# Here, we have 5 actions: 4 moves, and wall creation. 

ACTION_MOVE_LEFT   = 1
ACTION_MOVE_RIGHT  = 2
ACTION_MOVE_UP     = 3
ACTION_MOVE_DOWN   = 4
ACTION_CREATE_WALL = 5

class Action(tofu.Action):
  def __init__(self, action = 0):
    tofu.Action.__init__(self)
    self.action = action

# A state is the state of a mobile, here its current X,Y position.
# spc is used to indicate special action like wall creation.

class State(tofu.State):
  def __init__(self, x, y, spc = 0):
    tofu.State.__init__(self)
    self.x   = x
    self.y   = y
    self.spc = spc
    
  # is_crucial must returns true if the state is crucial.
  # Non-crucial states can be dropped, either for optimization purpose or because of
  # network protocol (currently we use TCP, but in the future we may use UDP to send
  # non-crucial states).
  
  def is_crucial(self): return self.spc
  

# A mobile is anything that can move and evolve in a level. This include player characters
# but also computer-controlled objects (also named bots).
  
class Mobile(tofu.Mobile):
  def __init__(self, name):
    tofu.Mobile.__init__(self)
    
    # x and y are the position of the mobile. name is the text displayed in the interface.
    # canvas and tag are the tk canvas in which the mobile is drawn, and the
    # corresponding canvas item.
    
    self.x      = 4
    self.y      = 4
    self.name   = name[:4]
    self.canvas = None
    self.tag    = None

  # Sets the Tk canvas and draws the mobile in this canvas. If the canvas is None, the
  # mobile's canva items are destroyed.
  
  def set_canvas(self, canvas):
    if self.canvas:
      self.canvas.delete(self.tag)
      self.tag = None
      
    self.canvas = canvas
    
    if canvas:
      self.tag = canvas.create_text(self.x * 20 + 10, self.y * 20 + 10, text = self.name)
      
  # do_action is called when the mobile executes the given action. It is usually called
  # on the server side.
  # It must return the new State of the Mobile, after the action is executed.
  
  def do_action(self, action):
    if action:
      
      # Computes the new X,Y position.
      
      new_x, new_y = self.x, self.y
      if   action.action == ACTION_MOVE_LEFT : new_x -= 1
      elif action.action == ACTION_MOVE_RIGHT: new_x += 1
      elif action.action == ACTION_MOVE_UP   : new_y -= 1
      elif action.action == ACTION_MOVE_DOWN : new_y += 1
      
      # Checks if we are out of the level, i.e. if the mobile moves to a new level.
      
      new_level = self.level
      if new_x < 0: new_x += 10; new_level = self.level.neighbour(-1,  0)
      if new_x > 9: new_x -= 10; new_level = self.level.neighbour( 1,  0)
      if new_y < 0: new_y += 10; new_level = self.level.neighbour( 0, -1)
      if new_y > 9: new_y -= 10; new_level = self.level.neighbour( 0,  1)
      
      # Checks if the new position is inside a wall.
      
      if not new_level.walls[new_x + new_y * 10]:
        if not new_level is self.level:
          
          # Change to new level.
          
          self.level.remove_mobile(self)
          new_level.add_mobile(self)

        # Return a state with the new position.
        self.doer.action_done(State(new_x, new_y, action.action))
        return
        #return State(new_x, new_y, action.action)
      
    # Else, the mobile doesn't move => we return a state with the previous position.
    self.doer.action_done(State(self.x, self.y))
    #return State(self.x, self.y)
  
  # set_state is called when the mobile's state change, due to the execution of an
  # action. It is called BOTH server-side and client-side.
  
  def set_state(self, state):
    tofu.Mobile.set_state(self, state)
    
    # If the new position is different than the old one, update the mobile and, if
    # needed, move the corresponding canvas item.
    
    if (self.x, self.y) != (state.x, state.y):
      self.x = state.x
      self.y = state.y
      if self.tag: self.canvas.coords(self.tag, self.x * 20 + 10, self.y * 20 + 10)
      
    # If the action was "create wall", create the wall !
    
    if state.spc == ACTION_CREATE_WALL: self.level.add_wall(self.x, self.y)
    
  def discard(self):
    # When the mobile is discarded, we hide it by setting the canavas to None.
    # Remove the canvas BEFORE discarding, because discard may save the character, and
    # we DON'T WANT to save the Tk canvas !
    self.set_canvas(None)
    
    tofu.Mobile.discard(self)
    

# A PlayerCharacter is a Mobile controlled by a human player.

class PlayerCharacter(Mobile):
  def __init__(self, name):
    Mobile.__init__(self, name)
    
    # action is the next action the character will do; it is set by the game interface.
    
    self.action = None
    
  # next_action is called when the character must choose its next action.
  # Here we simply return self.action.
  
  def next_action(self):
    if self.action:
      action, self.action = self.action, None
      return action
    
  # control_owned is called on the client-side when the player gets the control of the
  # mobile (i.e. the mobile is not a bot).
  
  def control_owned(self):
    tofu.Mobile.control_owned(self)
    
    # Set the game interface player character to this mobile.
    # The game interface will set the action for this mobile.
    
    tofu.GAME_INTERFACE.player_character = self
    
    
# A Bot is a Mobile controlled by the computer player.

class Bot(Mobile):
  def __init__(self, name = "bot"):
    Mobile.__init__(self, name = "bot")
    
    # Set the bot attribute to true. Tofu will now consider this Mobile as a bot.
    
    self.bot = 1
    self.direction = -40
    
  # next_action is called when the character must choose its next action.
  # Here, the bot moves horizontally.
    
  def next_action(self):
    dir = cmp(self.direction, 0)
    self.direction += 1
    if self.direction > 40: self.direction = -49
    if self.direction / 10.0 == self.direction // 10 :
      if   self.direction < 0: return Action(ACTION_MOVE_LEFT)
      elif self.direction > 0: return Action(ACTION_MOVE_RIGHT)
      
      
# GameInterface is the interface of the game. Here, we use Tk and thus GameInterface
# inherits from Tk toplevel window.

class GameInterface(tofu.GameInterface, Tkinter.Toplevel):
  def __init__(self):
    
    # Initialises Tk and install Twisted Tk support.
    
    tkroot = Tkinter.Tk(className = 'tofu_demo')
    tkroot.withdraw()
    import twisted.internet.tksupport
    twisted.internet.tksupport.install(tkroot)
    
    tofu.GameInterface.__init__(self)
    Tkinter.Toplevel.__init__(self, tkroot)
    
    # Creates a canvas.
    
    self.canvas = Tkinter.Canvas(self, bg = "white")
    self.canvas.place(x = 0, y = 0, width = 200, height = 200)
    
    # The character we are playing.
    
    self.player_character = None
    
    # Binds cursor keys.
    
    self.bind("<Key-Up>"   , self.on_move_up)
    self.bind("<Key-Down>" , self.on_move_down)
    self.bind("<Key-Left>" , self.on_move_left)
    self.bind("<Key-Right>", self.on_move_right)
    self.bind("<Key- >"    , self.on_create_wall)
    
    # To quite the game, we call GameInterface.end_game().
    
    self.bind("<Key-q>"    , lambda event = None: self.end_game())
    self.bind("<Destroy>"  , lambda event = None: self.end_game())

  # When a key is pressed, we set the current character's action to a new action
  # according to that key.
  
  def on_move_left  (self, event = None): self.player_character.action = Action(ACTION_MOVE_LEFT)
  def on_move_right (self, event = None): self.player_character.action = Action(ACTION_MOVE_RIGHT)
  def on_move_up    (self, event = None): self.player_character.action = Action(ACTION_MOVE_UP)
  def on_move_down  (self, event = None): self.player_character.action = Action(ACTION_MOVE_DOWN)
  def on_create_wall(self, event = None): self.player_character.action = Action(ACTION_CREATE_WALL)
  
  # ready is called when the client has contacted the server and anything is ready.
  # In particular, we can now call self.notifier.login_player to logon the server.
  
  def ready(self, notifier):
    tofu.GameInterface.ready(self, notifier)
    
    login, password = sys.argv[-1], "test"
    self.notifier.login_player(login, password)
    

# Tofu uses serialization to transfert object from server to client and vice-versa, and
# to store level and player data in local file.
# Tofu can use 2 different protocol: cPickle and Cerealizer. You can also use different
# protocols for local file and network.

# Some concerns about these protocols :
#  - cPickle is not safe for network and must not be used for that !!!
#  - Cerealizer require that you register the classes that are safe


# My advice is to use Cerealizer for network (since it's safe) and either Cerealizer or cPickle
# for local file.
# If you are using Soya, you should use cPickle for local file, however i'm planning to use
# Cerealizer as default in Soya soon.
#
# To each protocol corresponds a function like :
#     tofu.enable_<protocol>(enable_for_local, enable_for_network)

# For Cerealizer + cPickle:
#
#tofu.enable_pickle    (1, 0)
#tofu.enable_cerealizer(0, 1)

# For Cerealizer:
#
tofu.enable_cerealizer(1, 1)


# Registers our classes as safe for Cerealizer
# Level and Player class MUST be registred using the tofu.SavedInAPathHandler Cerealizer handler.

import cerealizer
cerealizer.register(Action)
cerealizer.register(State)
cerealizer.register(Mobile)
cerealizer.register(Bot)
cerealizer.register(PlayerCharacter)
cerealizer.register(Level , tofu.SavedInAPathHandler(Level ))
cerealizer.register(Player, tofu.SavedInAPathHandler(Player))


# Inits Tofu with our classes.

tofu.init(GameInterface, Player, Level, Action, State, Mobile)
