著者:飯尾 淳
本連載では「Pythonを昔から使っているものの、それほど使いこなしてはいない」という筆者が、いろいろな日常業務をPythonで処理することで、立派な「蛇使い」に育つことを目指します。その過程を温かく見守ってください。皆さんと共に勉強していきましょう。第21回では、「ハノイの塔」の解法「GDHP(Gniibe Distributed Hanoi Protocol)」の手順をアニメーションで表示するPythonアプリを作成します。
シェルスクリプトマガジン Vol.91は以下のリンク先でご購入できます。
図2 GDHPの手順をアニメーションで表示するJavaScriptコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
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コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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コード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
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) |