シェルスクリプトマガジン

Pythonあれこれ(Vol.91掲載)

著者:飯尾 淳

本連載では「Pythonを昔から使っているものの、それほど使いこなしてはいない」という筆者が、いろいろな日常業務をPythonで処理することで、立派な「蛇使い」に育つことを目指します。その過程を温かく見守ってください。皆さんと共に勉強していきましょう。第21回では、「ハノイの塔」の解法「GDHP(Gniibe Distributed Hanoi Protocol)」の手順をアニメーションで表示するPythonアプリを作成します。

シェルスクリプトマガジン Vol.91は以下のリンク先でご購入できます。

図2 GDHPの手順をアニメーションで表示するJavaScriptコード

const COLORS = [ 'crimson', 'forestgreen', 'yellow', 'royalblue',
                 'saddlebrown', 'hotpink', 'darkorange', 'darkmagenta' ];
const NUM_OF_DISKS = COLORS.length;
const BASE_LENGTH = 200;
const C_WIDTH  = 3.732 * BASE_LENGTH;
const C_HEIGHT = 3.500 * BASE_LENGTH;
const DISK_R = 0.9 * BASE_LENGTH;
const POLE_R = 15;
const POSITIONS = { 'Source'      : [0.268, 0.714],
                    'Auxiliary'   : [0.500, 0.286],
                    'Destination' : [0.732, 0.714] };
const FLASHING_COUNTER = 20;
const STEPS = 30;

class Vector {
  constructor(x,y) {
    this.x = x;
    this.y = y;
  }
}
class Position extends Vector {
  constructor(x,y) { super(x,y); }
  move(vec) {
    this.x += vec.x;
    this.y += vec.y;
  }
}
class Disk {
  constructor(level) {
    this.level = level;
    this.color = COLORS[level];
    this.r = (DISK_R-POLE_R)*(NUM_OF_DISKS-level)/NUM_OF_DISKS + POLE_R;
  }
}
class MovingDisk extends Disk {
  constructor(level,from,to) {
    super(level); 
    [sx,sy] = [from.pos.x,from.pos.y];
    [dx,dy] = [to.pos.x,  to.pos.y];
    this.pos = new Position(sx,sy);
    this.mvec = new Vector((dx-sx)/STEPS,(dy-sy)/STEPS);
    this.move_ctr = 0;
    this.from = from;
    this.to = to;
  }
  step_forward() {
    this.pos.move(this.mvec);
    this.move_ctr++;
  }
  finish_p() {
    var ret_flag = false;
    if (ret_flag = (this.move_ctr == STEPS)) {
      this.to.disks.push(new Disk(this.level));
    }
    return ret_flag;
  }
}
class Tower {
  constructor(name, disks, direction=null) {
    this.name = name;
    this.disks = [];
    for (var i = 0; i < disks; i++) {
      this.disks.push(new Disk(i));
    }
    this.direction = direction;
    this.moving = false;
    this.flash_ctr = 0;
  }
  get toplevel() {
    var l = this.disks.length;
    // '-1' means there is no disk.
    return (l > 0) ? this.disks[l-1].level : -1;
  }
}
var src = new Tower('Source', NUM_OF_DISKS);
var aux = new Tower('Auxiliary',   0, src);
var dst = new Tower('Destination', 0, src);
// In the case of NUM_OF_DISKS is odd, 
// the src must face the src.
// Otherwise, the src faces the aux.
src.direction = (COLORS.length % 2 == 1) ? dst : aux;
// the reference to moving disk is stored to this variable.
var moving_disk = null;

function setup() {
  createCanvas(C_WIDTH, C_HEIGHT);
  frameRate(30);
  [src,aux,dst].forEach(function(t) {
    [rx,ry] = POSITIONS[t.name];
    t.pos = new Position(rx * C_WIDTH, ry * C_HEIGHT);
  })
}
function base_drawing() {
  background('beige');
  [src,aux,dst].forEach(function(t) {
    // draw disks
    t.disks.forEach(function(d) {
      stroke('black');
      fill(d.color);
      ellipse(t.pos.x,t.pos.y,2*d.r);
    })
    // draw a pole
    stroke('brown');
    fill(t.moving & (t.flash_ctr < FLASHING_COUNTER/2) ? 'gold' : 'white');
    ellipse(t.pos.x,t.pos.y,2*POLE_R);
    // draw a direction
    stroke('navy');
    [sx, sy] = [t.pos.x, t.pos.y];
    [dx, dy] = [t.direction.pos.x, t.direction.pos.y];
    r = POLE_R / Math.sqrt((dx-sx)*(dx-sx)+(dy-sy)*(dy-sy));
    [dx, dy] = [(dx-sx)*r+sx, (dy-sy)*r+sy];
    line(sx,sy,dx,dy);
  })
}
function flash_poles() {
  [src,aux,dst].forEach(function(t) {
    t.moving = (t.direction.direction === t);
    t.flash_ctr += 1;
    t.flash_ctr %= FLASHING_COUNTER;
  })
}
function pop_disk(src,aux,dst) {
  var towers = [src,aux,dst].filter(t => t.moving);
  var idx,from,to;
  idx = (towers[0].toplevel > towers[1].toplevel) ? 0 : 1;
  [from, to] = [towers[idx], towers[1-idx]];
  return new MovingDisk(from.disks.pop().level,from,to);
}
function draw_moving_disk() {
  var d = moving_disk;
  d.step_forward();
  stroke('black');
  fill(d.color);
  ellipse(d.pos.x,d.pos.y,2*d.r);
  return d.finish_p();
}
function turn() {
  [moving_disk.from,moving_disk.to].forEach(function(t) {
    t.direction = ([src,aux,dst]
      .filter(x => (x !== t) && (x !== t.direction)))[0];
    t.moving = false;
  })
}
function draw() {
  // base drawing
  base_drawing();
  // find two exchange-towers out of three
  flash_poles();
  // start moving
  if (moving_disk == null) {
    moving_disk = pop_disk(src,aux,dst);
  } else {
    if (draw_moving_disk()) {
      turn();
      moving_disk = null;
    }
  }
}

図3 Pygameを用いて描画ウィンドウを表示するPythonコード

import pygame

pygame.init()
screen = pygame.display.set_mode((640,480))
pygame.display.set_caption('Pygame Window')

while True:
  for event in pygame.event.get():
    if event.type == pygame.TEXTINPUT and event.text == 'q':
      pygame.quit()
      exit()

  screen.fill('lavender')

  pygame.display.flip()
  pygame.time.delay(30)

図5 GDHPの手順をアニメーションで表示するPythonコード

import pygame
import math

COLORS = [ 'crimson', 'forestgreen', 'yellow', 'royalblue',
           'saddlebrown', 'hotpink', 'darkorange', 'darkmagenta' ]
NUM_OF_DISKS = len(COLORS)
BASE_LENGTH = 200
C_WIDTH  = 3.732 * BASE_LENGTH
C_HEIGHT = 3.500 * BASE_LENGTH
DISK_R = 0.9 * BASE_LENGTH
POLE_R = 15
POSITIONS = { 'Source'      : [0.268, 0.714],
              'Auxiliary'   : [0.500, 0.286],
              'Destination' : [0.732, 0.714] }
FLASHING_COUNTER = 20
STEPS = 30

class Vector:
  def __init__(self, x, y):
    self.x = x
    self.y = y
class Position(Vector):
  def __init__(self, x, y):
    super().__init__(x, y)
  def move(self, vec):
    self.x += vec.x
    self.y += vec.y
class Disk:
  def __init__(self, level):
    self.level = level
    self.color = COLORS[level]
    self.r = (DISK_R-POLE_R)*(NUM_OF_DISKS-level)/NUM_OF_DISKS + POLE_R
class MovingDisk(Disk):
  def __init__(self, level, frm, to):
    super().__init__(level) 
    [sx, sy] = [frm.pos.x, frm.pos.y]
    [dx, dy] = [to.pos.x,  to.pos.y]
    self.pos = Position(sx,sy)
    self.mvec = Vector((dx-sx)/STEPS,(dy-sy)/STEPS)
    self.move_ctr = 0
    self.frm = frm
    self.to = to
  def step_forward(self):
    self.pos.move(self.mvec)
    self.move_ctr += 1
  def finish_p(self):
    ret_flag = (self.move_ctr == STEPS)
    if ret_flag:
      self.to.disks.append(Disk(self.level))
    return ret_flag
class Tower:
  def __init__(self, name, disks, direction=None):
    self.name = name
    self.disks = []
    for i in range(disks):
      self.disks.append(Disk(i))
    self.direction = direction
    self.moving = False
    self.flash_ctr = 0
  def toplevel(self):
    l = len(self.disks)
    # '-1' means there is no disk.
    return self.disks[l-1].level if l > 0 else -1
def setup():
  pygame.init()
  screen = pygame.display.set_mode((C_WIDTH, C_HEIGHT))
  pygame.display.set_caption('GDHP')
  for t in [src,aux,dst]:
    [rx, ry] = POSITIONS[t.name]
    t.pos = Position(rx * C_WIDTH, ry * C_HEIGHT)
  return screen
def base_drawing():
  screen.fill('beige')
  for t in [src,aux,dst]:
    # draw disks
    for d in t.disks:
      pygame.draw.circle(screen, d.color, (t.pos.x,t.pos.y), d.r)
      pygame.draw.circle(screen, 'black', (t.pos.x,t.pos.y), d.r, 1)
    # draw a pole
    fillcolor = 'gold' \
      if t.moving and t.flash_ctr < FLASHING_COUNTER/2 else 'white' 
    pygame.draw.circle(screen, fillcolor, (t.pos.x,t.pos.y), POLE_R)
    pygame.draw.circle(screen, 'brown', (t.pos.x,t.pos.y), POLE_R, 1)
    # draw a direction
    [sx, sy] = [t.pos.x, t.pos.y]
    [dx, dy] = [t.direction.pos.x, t.direction.pos.y]
    r = POLE_R / math.sqrt((dx-sx)*(dx-sx)+(dy-sy)*(dy-sy))
    [dx, dy] = [(dx-sx)*r+sx, (dy-sy)*r+sy]
    pygame.draw.line(screen, (0,0,128), (sx,sy), (dx,dy), 3)
def flash_poles():
  for t in [src,aux,dst]:
    t.moving = (t.direction.direction == t)
    t.flash_ctr += 1
    t.flash_ctr %= FLASHING_COUNTER
def pop_disk(src,aux,dst):
  towers = list(filter(lambda x: x.moving, [src,aux,dst]))
  idx = 0 if towers[0].toplevel() > towers[1].toplevel() else 1
  [frm, to] = [towers[idx], towers[1-idx]]
  return MovingDisk(frm.disks.pop().level,frm,to) \
           if len(frm.disks) > 0 else None
def draw_moving_disk():
  d = moving_disk
  d.step_forward()
  pygame.draw.circle(screen, d.color, (d.pos.x,d.pos.y), d.r)
  pygame.draw.circle(screen, 'black', (d.pos.x,d.pos.y), d.r, 1)
  return d.finish_p()
def turn():
  for t in [moving_disk.frm,moving_disk.to]:
    t.direction = list(filter(
      lambda x: (x != t) and (x != t.direction), [src,aux,dst]))[0]
    t.moving = False
def draw():
  # base drawing
  base_drawing()
  # find two exchange-towers out of three
  flash_poles()
  # start moving
  finish_p = False
  mdisk = moving_disk
  if mdisk == None:
    mdisk = pop_disk(src,aux,dst)
    if mdisk == None:
      finish_p = True
  else:
    if draw_moving_disk():
      turn()
      mdisk = None
  return mdisk, finish_p
# main routine
if __name__ == '__main__':
  src = Tower('Source', NUM_OF_DISKS)
  aux = Tower('Auxiliary',   0, src)
  dst = Tower('Destination', 0, src)
  # In the case of NUM_OF_DISKS is odd, 
  # the src must face the src.
  # Otherwise, the src faces the aux.
  src.direction = dst if len(COLORS) % 2 == 1 else aux
  # the reference to moving disk is stored to this variable.
  moving_disk = None
  screen = setup()
  while True:
    for event in pygame.event.get():
      if event.type == pygame.TEXTINPUT and event.text == 'q':
        pygame.quit()
        exit()
    moving_disk, finish_p = draw()
    if finish_p:
      pygame.quit()
      exit()
    pygame.display.flip()
    pygame.time.delay(30)