Romain Beaudon

Random 2D dungeon generation in Unity using BSP (Binary Space Partitioning) trees

Intro

The approach and algorithm is based on this Stackexchange answer, and these related resources: eskerda.com/bsp-dungeon-generation and gamedevelopment.tutsplus.com/tutorials/how-to-use-bsp-trees-to-generate-game-maps

Unity intro

The sprites used in this post come from existing games and are used here only for learning purpose.

Build the structure

Let’s start with a new 2D project in Unity 5. Create a new Game Object called Board for holding the whole dungeon. On this game object create a new Script component called BoardManager. We’ll code the dungeon generation logic into it.

Unity new project

I recommend you refer to the stackexchange answer above for the illustrations of the following steps.

The idea of the algorithm is to start with a rectangular cell representing the whole dungeon. Then we’ll split this dungeon in two sub-dungeons, with a randomly chosen splitting position. The process will be repeated again for each sub-dungeons recursively, until the sub-dungeons are approximately the desired size of a room.

Our BoardManager script should look like this:

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
public class BoardManager : MonoBehaviour {
  public int boardRows, boardColumns;
  public int minRoomSize, maxRoomSize;

  public class SubDungeon {
    public SubDungeon left, right;
    public Rect rect;
    public Rect room = new Rect(-1,-1, 0, 0); // i.e null
    public int debugId;

    private static int debugCounter = 0;

    public SubDungeon(Rect mrect) {
      rect = mrect;
      debugId = debugCounter;
      debugCounter++;
    }

    public bool IAmLeaf() {
      return left == null && right == null;
    }

    public bool Split(int minRoomSize, int maxRoomSize) {
      if (!IAmLeaf()) {
        return false;
      }

      // choose a vertical or horizontal split depending on the proportions
      // i.e. if too wide split vertically, or too long horizontally,
      // or if nearly square choose vertical or horizontal at random
      bool splitH;
      if (rect.width / rect.height >= 1.25) {
        splitH = false;
      } else if (rect.height / rect.width >= 1.25) {
        splitH = true;
      } else {
        splitH = Random.Range (0.0f, 1.0f) > 0.5;
      }

      if (Mathf.Min(rect.height, rect.width) / 2 < minRoomSize) {
        Debug.Log ("Sub-dungeon " + debugId + " will be a leaf");
        return false;
      }

      if (splitH) {
        // split so that the resulting sub-dungeons widths are not too small
        // (since we are splitting horizontally)
        int split = Random.Range (minRoomSize, (int)(rect.width - minRoomSize));

        left = new SubDungeon (new Rect (rect.x, rect.y, rect.width, split));
        right = new SubDungeon (
          new Rect (rect.x, rect.y + split, rect.width, rect.height - split));
      }
      else {
        int split = Random.Range (minRoomSize, (int)(rect.height - minRoomSize));

        left = new SubDungeon (new Rect (rect.x, rect.y, split, rect.height));
        right = new SubDungeon (
          new Rect (rect.x + split, rect.y, rect.width - split, rect.height));
      }

      return true;
    }
  }

  public void CreateBSP(SubDungeon subDungeon) {
    Debug.Log ("Splitting sub-dungeon " + subDungeon.debugId + ": " + subDungeon.rect);
    if (subDungeon.IAmLeaf()) {
      // if the sub-dungeon is too large
      if (subDungeon.rect.width > maxRoomSize
        || subDungeon.rect.height > maxRoomSize
        || Random.Range(0.0f,1.0f) > 0.25) {

        if (subDungeon.Split (minRoomSize, maxRoomSize)) {
          Debug.Log ("Splitted sub-dungeon " + subDungeon.debugId + " in "
            + subDungeon.left.debugId + ": " + subDungeon.left.rect + ", "
            + subDungeon.right.debugId + ": " + subDungeon.right.rect);

          CreateBSP(subDungeon.left);
          CreateBSP(subDungeon.right);
        }
      }
    }
  }

  void Start () {
    SubDungeon rootSubDungeon = new SubDungeon (new Rect (0, 0, boardRows, boardColumns));
    CreateBSP (rootSubDungeon);
  }
}

Then set the public variables for board and rooms size in the Unity editor.

Variables in editor

When launching the game you should have something like this in the console :

Unity split

Create the rooms

We now have a tree structure with the leaves corresponding to the smallest sub-dungeons. In each of these leaves we’ll now create a room with a random size.

Add the following variable and method to the SubDungeon class :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  public class SubDungeon {
    //...
    public Rect room = new Rect(-1,-1, 0, 0); // i.e null
    //...
    public void CreateRoom() {
      if (left != null) {
        left.CreateRoom ();
      }
      if (right != null) {
        right.CreateRoom ();
      }
      if (IAmLeaf()) {
        int roomWidth = (int)Random.Range (rect.width / 2, rect.width - 2);
        int roomHeight = (int)Random.Range (rect.height / 2, rect.height - 2);
        int roomX = (int)Random.Range (1, rect.width - roomWidth - 1);
        int roomY = (int)Random.Range (1, rect.height - roomHeight - 1);

        // room position will be absolute in the board, not relative to the sub-dungeon
        room = new Rect (rect.x + roomX, rect.y + roomY, roomWidth, roomHeight);
        Debug.Log ("Created room " + room + " in sub-dungeon " + debugId + " " + rect);
      }
    }
    //...

And call it in the Start() method :

1
2
3
4
5
  void Start () {
    SubDungeon rootSubDungeon = new SubDungeon (new Rect (0, 0, boardRows, boardColumns));
    CreateBSP (rootSubDungeon);
    rootSubDungeon.CreateRoom ();
  }

The output console should look like this:

Unity rooms created console

Drawing the rooms ½: add a sprite for the floor

Instead of relying on console output, it would be more intuitive to start having a visual representation. First thing is to add a sprite for the floor of the rooms. You can use this one Room sprite and add it as an asset in your project.

Also be careful to set the Pixels Per Unit to exactly the size of the sprite (16 for this sprite), so that the sprite will cover exactly a square in the board.

Room sprite in Unity

Next create a Prefab with this sprite (drag and drop it on the scene screen, then drag and drop back the object in the assets):

Floor prefab

In the script we’ll need to add a public GameObject variable to hold this prefab:

1
2
3
public class BoardManager : MonoBehaviour {
  //...
  public GameObject floorTile;

Then in the editor drag and drop the prefab to set it.

Floor prefab variable editor

Drawing the rooms 2/2: write the code

Back in the script we add a method to draw the rooms. Also as we draw the rooms, we’ll register the drawed squares in a variable representing the board called boardPositionsFloor. This variable is not really useful at first but will be essential when starting to construct a game above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class BoardManager : MonoBehaviour {
  //...
  private GameObject[,] boardPositionsFloor;
  //...

  public void DrawRooms(SubDungeon subDungeon) {
    if (subDungeon == null) {
      return;
    }
    if (subDungeon.IAmLeaf()) {
      for (int i = (int)subDungeon.room.x; i < subDungeon.room.xMax; i++) {
        for (int j = (int)subDungeon.room.y; j < subDungeon.room.yMax; j++) {
          GameObject instance = Instantiate (floorTile, new Vector3 (i, j, 0f), Quaternion.identity) as GameObject;
          instance.transform.SetParent (transform);
          boardPositionsFloor [i, j] = instance;
        }
      }
    } else {
      DrawRooms (subDungeon.left);
      DrawRooms (subDungeon.right);
    }
  }

And in the Start() method initialize boardPositionsFloor and call DrawRooms on the root:

1
2
3
4
5
6
  void Start () {
    //...

    boardPositionsFloor = new GameObject[boardRows, boardColumns];
    DrawRooms (rootSubDungeon);
  }

You should get something like this in the scene view :

Rooms drawed

Now that we have a visual feedback, it’s also easier to play with the parameters :

Rooms drawed big

Connect the rooms: create corridors between rooms

Isolated rooms are not very useful, so we’ll add corridors between them. To do so, we’ll connect each leaf to its sibling. Then going up one level in the tree, we’ll repeat the process to connect parents sub-dungeons, until finally we connect the two initial sub-dungeons (see the stackexchange answer for illustrations).

Add using System.Collections.Generic; at the beginning of the script, then in the SubDungeon class add:

1
2
3
  public class SubDungeon {
    // ...
    public List<Rect> corridors = new List<Rect>();

We’ll also need a method to get the room of a sub-dungeon. If the sub-dungeon is not a leaf (i.e. doesn’t contain a room), the method will return the room of a child.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  public class SubDungeon {
    //...
    public Rect GetRoom() {
      if (IAmLeaf()) {
        return room;
      }
      if (left != null) {
        Rect lroom = left.GetRoom ();
        if (lroom.x != -1) {
          return lroom;
        }
      }
      if (right != null) {
        Rect rroom = right.GetRoom ();
        if (rroom.x != -1) {
          return rroom;
        }
      }

      // workaround non nullable structs
      return new Rect (-1, -1, 0, 0);
    }

And now the method to create the corridor between rooms:

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
public void CreateCorridorBetween(SubDungeon left, SubDungeon right) {
  Rect lroom = left.GetRoom ();
  Rect rroom = right.GetRoom ();

  Debug.Log("Creating corridor(s) between " + left.debugId + "(" + lroom + ") and " + right.debugId + " (" + rroom + ")");

  // attach the corridor to a random point in each room
  Vector2 lpoint = new Vector2 ((int)Random.Range (lroom.x + 1, lroom.xMax - 1), (int)Random.Range (lroom.y + 1, lroom.yMax - 1));
  Vector2 rpoint = new Vector2 ((int)Random.Range (rroom.x + 1, rroom.xMax - 1), (int)Random.Range (rroom.y + 1, rroom.yMax - 1));

  // always be sure that left point is on the left to simplify the code
  if (lpoint.x > rpoint.x) {
    Vector2 temp = lpoint;
    lpoint = rpoint;
    rpoint = temp;
  }

  int w = (int)(lpoint.x - rpoint.x);
  int h = (int)(lpoint.y - rpoint.y);

  Debug.Log ("lpoint: " + lpoint + ", rpoint: " + rpoint + ", w: " + w + ", h: " + h);

  // if the points are not aligned horizontally
  if (w != 0) {
    // choose at random to go horizontal then vertical or the opposite
    if (Random.Range (0, 1) > 2) {
      // add a corridor to the right
      corridors.Add (new Rect (lpoint.x, lpoint.y, Mathf.Abs (w) + 1, 1));

      // if left point is below right point go up
      // otherwise go down
      if (h < 0) {
        corridors.Add (new Rect (rpoint.x, lpoint.y, 1, Mathf.Abs (h)));
      } else {
        corridors.Add (new Rect (rpoint.x, lpoint.y, 1, -Mathf.Abs (h)));
      }
    } else {
      // go up or down
      if (h < 0) {
        corridors.Add (new Rect (lpoint.x, lpoint.y, 1, Mathf.Abs (h)));
      } else {
        corridors.Add (new Rect (lpoint.x, rpoint.y, 1, Mathf.Abs (h)));
      }

      // then go right
      corridors.Add (new Rect (lpoint.x, rpoint.y, Mathf.Abs (w) + 1, 1));
    }
  } else {
    // if the points are aligned horizontally
    // go up or down depending on the positions
    if (h < 0) {
      corridors.Add (new Rect ((int)lpoint.x, (int)lpoint.y, 1, Mathf.Abs (h)));
    } else {
      corridors.Add (new Rect ((int)rpoint.x, (int)rpoint.y, 1, Mathf.Abs (h)));
    }
  }

  Debug.Log ("Corridors: ");
  foreach (Rect corridor in corridors) {
    Debug.Log ("corridor: " + corridor);
  }
}

You should see the corridors being created in the console:

Corridors console

It’s hard to see visualize without the corridors being drawed but for this example it would look like this:

Corridors drawed

Draw the corridors ½: add the corridor sprite

Same as before for the rooms we’ll need a sprite Corridor sprite, a prefab in Unity and variable to hold the prefab:

Remember to correctly set Pixels Per Unit setting:

Corridor sprite

Next create a prefab:

Corridor prefab

And add a variable in the code:

1
2
3
public class BoardManager : MonoBehaviour {
  //...
  public GameObject corridorTile;

And finally set the variable in the editor:

Corridor variable editor

Draw the corridors 2/2: write the code

The drawing function will be quite similar to the one for the rooms:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class BoardManager : MonoBehaviour {
  //...

  void DrawCorridors(SubDungeon subDungeon) {
    if (subDungeon == null) {
      return;
    }

    DrawCorridors (subDungeon.left);
    DrawCorridors (subDungeon.right);

    foreach (Rect corridor in subDungeon.corridors) {
      for (int i = (int)corridor.x; i < corridor.xMax; i++) {
        for (int j = (int)corridor.y; j < corridor.yMax; j++) {
          if (boardPositionsFloor[i,j] == null) {
            GameObject instance = Instantiate (corridorTile, new Vector3 (i, j, 0f), Quaternion.identity) as GameObject;
            instance.transform.SetParent (transform);
            boardPositionsFloor [i, j] = instance;
          }
        }
      }
    }
  }

In the Start() method, you can call DrawCorridors before DrawRooms to better see what is going on:

1
2
3
4
5
6
7
8
9
  void Start () {
    SubDungeon rootSubDungeon = new SubDungeon (new Rect (0, 0, boardRows, boardColumns));
    CreateBSP (rootSubDungeon);
    rootSubDungeon.CreateRoom ();

    boardPositionsFloor = new GameObject[boardRows, boardColumns];
    DrawCorridors (rootSubDungeon);
    DrawRooms (rootSubDungeon);
  }

Corridor drawed 2

Corridor drawed above

If DrawRooms is called before :

Corridor drawed below

Now that the random generation is working future improvements could be adding walls and of course a player sprite and controls and enemies.

Dungeon with enemies

And with a camera on the player and some further improvements it can become the basis of a simple rogue-like or dungeon crawler:

Dungeon with enemies

Backbone, Leaflet, Bootstrap, Node.js and MongoDB using Responsive Design

Little webapp hacked with Backbone, Leaflet, Bootstrap, Node.js and MongoDB using Responsive Design.

The landing page is a map with places around you fetched from the database (new places are eventually added through a request to OpenStreetMap). When you click on a place you can see comments from other people and leave yours.

Live Demo (currently down)

Github repo

Mobile

Desktop

Technically the app features these concepts/technologies :

Paris Public Transports route planning app

TL;DR : RATP Open Data (GTFS) –> JSON graph –> Dijkstra with Priority Queue in JS –> Firefox OS webapp

This is a Paris public transports route planning POC webapp.

DATA come from RATP (Paris Public Transports operator).

You can try it here : http://www.rombdn.com/fxos-metrobusparis/demo2/

Source : https://github.com/rombdn/fxos-metrobusparis

Source for the DATA transformation : https://github.com/rombdn/ratp-gtfs-to-json

External libs : Leaflet (map), Firefox OS Building Blocks, JQuery and Bootstrap (for autocompletion, didn’t have time to implement mine)

Background

When the RATP opened their data earlier this year I thought it was a good opportunity to create an offline route planning app acrosse the whole public transportation network.

The technical challenges were : – The full subway (metro) and bus graph, including edges with informations like durations, had to fit in less than a MB to minimize loading time – The shortest path search had to be fast.

Conception and implementation

DATA transformation (GTFS –> JSON Graph)

The DATA provided by RATP are 1GB of flat text files with informations on every route, trips, stop times for each day of the year…

Some simplifications I made :

  • Collapse stations : in the original DATA, stations are duplicated for every direction (route) and every line. This would have leaded to an overly complicated graph
  • Average durations and wait times

The final graph contains approximatly 6000 stations (stops) and around 3 edges per stations instead of 26000 stations and up to 20 edges per station.

Link to data transformation Github project : ratp-gtfs-to-json

Original DATA

Full description : http://data.ratp.fr/?eID=ics_od_datastoredownload&file=88

routes.txt : route_id, trip_id, type, line (directory)

trips.txt : route_id, trip_id

stop_times.txt : stop_id, trip_id, arrival_time

stops.txt : stop_id, stop_name

Graph output :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
    <stop_id>: {
        name: <name>,
        loc: {
            lat: <latitude>,
            lon: <longitude>
        }
        zip: <zipcode>,
        edges: [
            {
                "dest": <dest_stop_id>,
                "dur": <edge_duration>,
                "type": <edge_type>,
                "open": <hour_of_opening>,
                "close": <hour_of_closing>,
                "line": <line_number>,
                "dir": <line_direction>,
                "freq": <average_frequency>
            }
        ]
    }
}

This is achieved with 2 scripts : the first in C to parse the file stop_times.txt (650MB, millions of entry) to create the raw edges, and the second in ruby to create the graph (JSON format).

Javascript Shortest Path with Priority Queue

The first version was a straight forward implementation of Dijkstra’s algorithm. It was too slow so I decided to use a priority queue.

I started from a binary heap implementation from the book Eloquent Javascript by Marijn Haverbeke link.

Binary Heap with Keys source (idFunction is the method called to get the index).

Identify Backbone Models in Chrome Heap Profiler

Note: this post is based on the idea of Named Constructors from the Coccyx library

In this post I showed an example of a memory leak in Backbone, and how to troobleshoot it using the Chrome Heap Profiler :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var ZombieView = Backbone.View.extend({
    initialize: function() {
        Backbone.on('greetingEvent', this.sayFoo, this);
    },

    sayFoo: function() {
      console.log('foo');
    }
});

for(var i = 0; i<1000; ++i) {
    var view = new ZombieView();
    view.remove();
}

Backbone.trigger('greetingEvent');

But identifying these views is not easy because models extended from Backbone Model/View have their constructor named child in the Heap Profiler.

A solution to this problem would be the possibility to name the constructors. Here is a simple hack to do that :

In backbone.js, around line 1541 :

1
2
3
4
5
6
7
8
9
// The constructor function for the new subclass is either defined by you
// (the "constructor" property in your `extend` definition), or defaulted
// by us to simply call the parent's constructor.
if (protoProps && _.has(protoProps, 'constructor')) {
  child = protoProps.constructor;
}
else {
  child = function(){ return parent.apply(this, arguments); };
}

Simply add a case for an optional constructor name :

1
2
3
4
5
6
7
8
9
if (protoProps && _.has(protoProps, 'constructor')) {
  child = protoProps.constructor;
}
else if (_.has(protoProps, 'constructorName')) {
  eval("child = function " + protoProps.constructorName + "() { return parent.apply(this, arguments); };");
}
else {
  child = function(){ return parent.apply(this, arguments); };
}

Note: due to the use of ‘eval’ it is not safe in production…

Now we can add a ‘constructorName’ in our models/views.

Example with the leaking view from part 1

Adding a constructor name :

1
2
   var ZombieView = Backbone.View.extend({
      constructorName: 'Zombie',

Now the Views Constructors are correctly named in the Heap Profiler :

Hope this will help!

Backbone Memory leaks analysis with Chrome Heap Profiler

Intro

The Heap Profiler in Chrome is a great tool to analyze memory usage in web applications. Here are some useful links :

Why do we have to worry about memory management in JS when there is a Garbage Collector ? Because memory leaks can still happen : as long as an object is referenced by another it cannot be disposed by the GC. These references are “Retaining Paths” in the Heap Profiler.

A common source of these problems are event binding : events are references, which prevent object to be destroyed after they are removed from the DOM. This can happen with Backbone Views.

Example

Here is a simple example (using the Backbone global object as an event mediator) :

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
<html>
<body>
    <script src="jquery.js"></script>
    <script src="underscore.js"></script>
    <script src="backbone.js"></script>
    <script src="zombieViews.js"></script>

    <script>

        var ZombieView = Backbone.View.extend({
            initialize: function() {
                Backbone.on('greetingEvent', this.sayFoo, this);
            },

            sayFoo: function() {
                console.log('foo');
            }
        });

        for(var i = 0; i<1000; ++i) {
            var view = new ZombieView();
            view.remove();
        }

        Backbone.trigger('greetingEvent');

    </script>
</body>
</html>

Console Ouptput :

Nothing should appear in the console because every view is removed after its creation. As we can see this is not the case : the views are still catching greetingEvent and displaying a message.

A heap snapshot confirm that all the ZombieViews are still here (extended Backbone models have their constructor named child, see this post for a solution).

Why the views aren’t removed ?

Troobleshoot

If we take a heap snapshot we can see that the greetingEvent is in the retaining path of the views. This path prevent the GC to dispose the object.

Let’s modify the code with a call to listenTo instead of Backbone.on. With listenTo models can keep track of events binded so they can be unbinded by the remove method and thus allow the GC to dispose the attached views.

1
2
//Backbone.on('greetingEvent', this.sayFoo, this);
this.listenTo(Backbone, 'greetingEvent', this.sayFoo);

This time nothing shows up in the console, which is the expected output : all the views where removed, none catched the event.

Note: You may have noted that the views have their constructor named ‘child’ which complicate identification, a solution is provided in this post.

Linerider clone with HTML5 Canvas and Box2D JS

Dog Sled is a little weekend project game hacked with HTML5 Canvas, Javascript and Box2D :

Dog Sledsource

Code details

Initialisations

1
2
3
4
5
6
7
8
9
//...
dog_initcanvas();
dog_initsound();
dog_events(); //mouse and keyboard
dog_initworld();
dog_setupcollisions();
dog_initdraw();
dog_update();
//...

Collisions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//...
function dog_setupcollisions()
{
   var listener = new Box2D.Dynamics.b2ContactListener;
   listener.BeginContact = function(contact) {
      //if the dog touch the line we add a flag
      //...
   listener.PreSolve = function(contact) {
      //if the dog eats a bone there's no collision
      //...
   listener.PostSolve = function(contact, impulse) {
      //if the dog touch his house
      //and sounds...
      //...

Main loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//...
function dog_update()
{
  window.requestAnimationFrame(dog_update);

  world.Step(
      1 / 60,   //frame-rate
      10,      //velocity iterations
      10      //position iterations
  );

  world.ClearForces();
  canvas_draw(); //graphics
}
//...
function canvas_draw()
//...
//we iterate through all the bodies
for(body = world.GetBodyList(); body; body = body.GetNext()) {
   //...
   //we use the User Data field to know what to draw
  switch(body.GetUserData()) {
      case 'line':
//...

I used Box2DWeb, a javascript port of box2d 2.1. It lacks features like rope joint but I realized it too late… More up to date ports i didn’t try : 

Have fun!

Good tutorials : Seth Ladd’s blog

JMeter HTTP tips and examples

Load all resources attached to a html file

alt text

Useful when working with frames to avoid one http request per frame (I use this a lot when scripting for PeopleSoft ERP).

Extract a value to a variable with a regular expression

alt text

You can use a Debug Listener to adjust the regexp. In this example (PeopleSoft) I checked “Main sample and sub-samples” because the value to be extracted is in a frame.

Test a variable

alt text

In this example I verify that the Regular Expression hereabove worked.

Javascript in parameters (e.g. to randomize a choice)

First we get the number of values :

alt text

Then we replace the parameter with our javascript :

1
Math.floor( Math.random() * ${VALUES_COUNT} ) )

In JMeter it becomes :

1
${__javaScript( Math.floor( Math.random() * ${NB_Lignes } ) ) }

alt text

Note : The double $$ is due to variable names in PeopleSoft (AF_PCC_WRK_SELECT_PB$1, AF_PCC_WRK_SELECT_PB$2…)

Selenium Webdriver 2 with Python example

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
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
import time, re

#browser init
browser = webdriver.Firefox()

#wait template : 3 seconds
wait = WebDriverWait(browser, 3)

#goto google
browser.get('http://www.google.com')

#verify title using python internal assert
assert 'Google' in browser.title

#type cheese in the search area then submit
elem = browser.find_element_by_name('q')
elem.send_keys('cheese')
elem.submit()

#wait until we get the search results
#see http://seleniumhq.org/docs/04_webdriver_advanced.html for details
wait.until(lambda d : d.title.lower().startswith('cheese'))

assert 'cheese' in browser.title

#goto to translate tools
browser.find_element_by_link_text('Plus').click()

#wait until the menu is displayed (french)
#ExpectedConditions.elementsToBeClickable is not available in python
#instead we can use is_displayed()
#see http://selenium-python.readthedocs.org/en/latest/api.html
wait.until(lambda d : d.find_element_by_link_text('Traduction').is_displayed())

#translate from english to french
browser.find_element_by_link_text('Traduction').click()
assert 'fromage' in browser.find_element_by_id('result_box').text