├── gfx
├── hero.png
├── gooball.png
├── sprites.png
├── arrow_up.png
├── statusbox.png
├── arrow_up_dbg.png
├── empty_heart.png
├── focus_empty.png
├── focus_full.png
├── full_heart.png
├── sprites_bak.png
├── splash
│ └── frank_face.png
├── color_tileset_16x16.png
└── color_tileset_64x64.png
├── audio
└── fx
│ ├── heal.ogg
│ ├── grunt1.ogg
│ ├── grunt2.ogg
│ ├── grunt3.ogg
│ ├── herodie.ogg
│ ├── sword.ogg
│ ├── gooballdie.ogg
│ ├── heroinjure.ogg
│ └── gooballinjure.ogg
├── fonts
└── manaspace.regular.ttf
├── monsters.lua
├── conf.lua
├── cursor.lua
├── renderer.lua
├── word_queue.lua
├── credits.md
├── inheritance.lua
├── mode_switcher.lua
├── test
├── lovetest.lua
├── test_collider.lua
└── lunatest.lua
├── splash_renderer.lua
├── collider.lua
├── status.lua
├── dungeon_master.lua
├── main.lua
├── character.lua
├── keyboard_handler.lua
├── tiledmap.lua
├── maps
└── basic.tmx
├── ProFi.lua
└── dictionaries
└── basic_english.txt
/gfx/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/hero.png
--------------------------------------------------------------------------------
/gfx/gooball.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/gooball.png
--------------------------------------------------------------------------------
/gfx/sprites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/sprites.png
--------------------------------------------------------------------------------
/audio/fx/heal.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/audio/fx/heal.ogg
--------------------------------------------------------------------------------
/gfx/arrow_up.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/arrow_up.png
--------------------------------------------------------------------------------
/gfx/statusbox.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/statusbox.png
--------------------------------------------------------------------------------
/audio/fx/grunt1.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/audio/fx/grunt1.ogg
--------------------------------------------------------------------------------
/audio/fx/grunt2.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/audio/fx/grunt2.ogg
--------------------------------------------------------------------------------
/audio/fx/grunt3.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/audio/fx/grunt3.ogg
--------------------------------------------------------------------------------
/audio/fx/herodie.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/audio/fx/herodie.ogg
--------------------------------------------------------------------------------
/audio/fx/sword.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/audio/fx/sword.ogg
--------------------------------------------------------------------------------
/gfx/arrow_up_dbg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/arrow_up_dbg.png
--------------------------------------------------------------------------------
/gfx/empty_heart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/empty_heart.png
--------------------------------------------------------------------------------
/gfx/focus_empty.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/focus_empty.png
--------------------------------------------------------------------------------
/gfx/focus_full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/focus_full.png
--------------------------------------------------------------------------------
/gfx/full_heart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/full_heart.png
--------------------------------------------------------------------------------
/gfx/sprites_bak.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/sprites_bak.png
--------------------------------------------------------------------------------
/audio/fx/gooballdie.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/audio/fx/gooballdie.ogg
--------------------------------------------------------------------------------
/audio/fx/heroinjure.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/audio/fx/heroinjure.ogg
--------------------------------------------------------------------------------
/gfx/splash/frank_face.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/splash/frank_face.png
--------------------------------------------------------------------------------
/audio/fx/gooballinjure.ogg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/audio/fx/gooballinjure.ogg
--------------------------------------------------------------------------------
/fonts/manaspace.regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/fonts/manaspace.regular.ttf
--------------------------------------------------------------------------------
/gfx/color_tileset_16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/color_tileset_16x16.png
--------------------------------------------------------------------------------
/gfx/color_tileset_64x64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/csaunders/spellsword/master/gfx/color_tileset_64x64.png
--------------------------------------------------------------------------------
/monsters.lua:
--------------------------------------------------------------------------------
1 | require('character')
2 | Monsters = {}
3 |
4 | function Monsters.gooBall(x, y)
5 | goo = Character.NewCharacter(x, y, 'gooball')
6 | goo:setHp(1, 1)
7 | goo:setFocus(0, 0)
8 | return goo
9 | end
10 |
--------------------------------------------------------------------------------
/conf.lua:
--------------------------------------------------------------------------------
1 |
2 | function love.conf(t)
3 | t.window = t.window or t.screen
4 |
5 | t.title = "SpellSword" -- The title of the window the game is in (string)
6 | t.author = "csaunders" -- The author of the game (string)
7 | t.window.width = 1024 -- The window width (number)
8 | t.window.height = 768 -- The window height (number)
9 | t.screen.fullscreen = false -- Enable fullscreen (boolean)
10 | t.screen.vsync = false -- Enable vertical sync (boolean)
11 |
12 | t.screen = t.screen or t.window
13 | end
14 |
--------------------------------------------------------------------------------
/cursor.lua:
--------------------------------------------------------------------------------
1 | Cursor = {}
2 | Cursor.__index = Cursor
3 |
4 | function Cursor.NewCursor(x, y)
5 | local self = setmetatable({}, Cursor)
6 |
7 | self.startX = x
8 | self.startY = y
9 | self.x = x
10 | self.y = y
11 |
12 | return self
13 | end
14 |
15 | function Cursor:reset()
16 | self.x = self.startX
17 | self.y = self.startY
18 | end
19 |
20 | function Cursor:moveBy(x, y)
21 | self.x = self.x + x
22 | self.y = self.y + y
23 | end
24 |
25 | function Cursor:draw(color)
26 | love.graphics.setColor(color or {0xFF, 0xFF, 0})
27 | love.graphics.circle('fill', self.x, self.y, 5)
28 | love.graphics.reset()
29 | end
30 |
--------------------------------------------------------------------------------
/renderer.lua:
--------------------------------------------------------------------------------
1 | require('inheritance')
2 | Renderer = {}
3 | Renderer.__index = Renderer
4 |
5 | RenderItem = {}
6 | RenderItem.__index = RenderItem
7 |
8 | function RenderItem.NewRenderItem(klass, image, x, y)
9 | local self = setmetatable({}, RenderItem)
10 | self.d = image
11 | self.x = x
12 | self.y = y
13 |
14 | return self
15 | end
16 |
17 | function Renderer:create()
18 | local self = setmetatable({}, Renderer)
19 | self.renderItems = {}
20 |
21 | return self
22 | end
23 |
24 | function Renderer:draw()
25 | for i, renderItem in ipairs(self.renderItems) do
26 | love.graphics.draw(renderItem.d, renderItem.x, renderItem.y)
27 | end
28 | end
29 |
30 |
31 |
--------------------------------------------------------------------------------
/word_queue.lua:
--------------------------------------------------------------------------------
1 | WordQueue = {DEFAULT_GRAB = 4}
2 | WordQueue.__index = WordQueue
3 |
4 | function WordQueue.NewQueue(dictionary_file)
5 | local self = setmetatable({}, WordQueue)
6 |
7 | self.words = self:extractWords(love.filesystem.newFile("dictionaries/" .. dictionary_file .. ".txt"))
8 | self.size = table.getn(self.words)
9 |
10 | return self
11 | end
12 |
13 | function WordQueue:grab(number)
14 | accumulator = {}
15 | for i = 1,(number or WordQueue.DEFAULT_GRAB) do
16 | index = math.random(self.size)
17 | table.insert(accumulator, self.words[index])
18 | end
19 | return accumulator
20 | end
21 |
22 | function WordQueue:extractWords(io)
23 | words = {}
24 | for line in io:lines() do
25 | table.insert(words, line)
26 | end
27 | return words
28 | end
29 |
--------------------------------------------------------------------------------
/credits.md:
--------------------------------------------------------------------------------
1 | tiledmap.lua -- ghoulsblade -- http://love2d.org/forums/viewtopic.php?f=5&t=2411&p=25929#p25929
2 | basic_english.txt -- Wikipedia - http://simple.wikipedia.org/wiki/Wikipedia:Basic_English_combined_wordlist
3 | manaspace-regular.ttf -- codeman38 -- http://www.1001fonts.com/manaspace-font.html#styles
4 | SleekBars.png -- Jannax -- http://opengameart.org/content/sleek-bars
5 | gruntsound.wav -- n3b -- http://opengameart.org/content/grunt
6 | yelling_sounds.zip -- HaelDB -- http://opengameart.org/content/male-gruntyelling-sounds
7 | Spell Sounds Starter Pack -- p0ss -- http://opengameart.org/content/spell-sounds-starter-pack
8 | 3 Heal Spells -- DoKashiteru -- http://opengameart.org/content/3-heal-spells
9 | RPG sound pack -- artisticdude -- http://opengameart.org/content/rpg-sound-pack
10 | Audacity -- The Audacity Devs -- http://audacity.sourceforge.net/
11 |
--------------------------------------------------------------------------------
/inheritance.lua:
--------------------------------------------------------------------------------
1 | -- Create a new class that inherits from a base class
2 | --
3 | function inheritsFrom( baseClass )
4 |
5 | -- The following lines are equivalent to the SimpleClass example:
6 |
7 | -- Create the table and metatable representing the class.
8 | local new_class = {}
9 | local class_mt = { __index = new_class }
10 |
11 | -- Note that this function uses class_mt as an upvalue, so every instance
12 | -- of the class will share the same metatable.
13 | --
14 | function new_class:create()
15 | local newinst = {}
16 | setmetatable( newinst, class_mt )
17 | return newinst
18 | end
19 |
20 | -- The following is the key to implementing inheritance:
21 |
22 | -- The __index member of the new class's metatable references the
23 | -- base class. This implies that all methods of the base class will
24 | -- be exposed to the sub-class, and that the sub-class can override
25 | -- any of these methods.
26 | --
27 | if baseClass then
28 | setmetatable( new_class, { __index = baseClass } )
29 | end
30 |
31 | return new_class
32 | end
33 |
--------------------------------------------------------------------------------
/mode_switcher.lua:
--------------------------------------------------------------------------------
1 | ModeSwitcher = {}
2 | ModeSwitcher.__index = ModeSwitcher
3 |
4 | function ModeSwitcher.NewModeSwitcher(handlers)
5 | local self = setmetatable({}, ModeSwitcher)
6 | self.modes = {}
7 | for i, handler in ipairs(handlers) do
8 | self:insertHandler(handler)
9 | end
10 |
11 | return self
12 | end
13 |
14 | function ModeSwitcher:insertHandler(handler)
15 | table.insert(self.modes, handler)
16 | end
17 |
18 | function ModeSwitcher:handle(key)
19 | if key == "escape" then
20 | return self:nextMode()
21 | else
22 | return self:handler():handle(key)
23 | end
24 | end
25 |
26 | function ModeSwitcher:inflictsInjuries()
27 | if self:handler().injureOnFailure == nil then return true end
28 | return self.handler().injureOnFailure
29 | end
30 |
31 | function ModeSwitcher:getMode()
32 | return self:handler():name()
33 | end
34 |
35 | function ModeSwitcher:handler()
36 | return self.modes[1]
37 | end
38 |
39 | function ModeSwitcher:tick(response)
40 | end
41 |
42 | function ModeSwitcher:reset()
43 | self:handler():reset()
44 | end
45 |
46 | function ModeSwitcher:nextMode()
47 | previousMode = table.remove(self.modes, 1)
48 | table.insert(self.modes, previousMode)
49 | end
50 |
--------------------------------------------------------------------------------
/test/lovetest.lua:
--------------------------------------------------------------------------------
1 | local lovetest = {}
2 |
3 | lovetest._version = "0.1.0"
4 |
5 | -- Search the passed in arguments for either -t or --test
6 | function lovetest.detect(args)
7 | for _, flag in ipairs(args) do
8 | if flag == "-t" or flag == "--test" then
9 | return true
10 | end
11 | end
12 | return false
13 | end
14 |
15 | local function enumerate(dir)
16 | if love.filesystem.enumerate then
17 | return love.filesystem.enumerate(dir)
18 | else
19 | return love.filesystem.getDirectoryItems(dir)
20 | end
21 | end
22 |
23 | -- Run the unit tests, On windows, don't quit. This allows the user to see the
24 | -- test results in the console
25 | function lovetest.run()
26 | require "test/lunatest"
27 |
28 | for _, filename in ipairs(enumerate('test')) do
29 | if filename:match("^test_.*%.lua$") then
30 | local testname = (filename:gsub(".lua", ""))
31 | lunatest.suite("test/" .. testname)
32 | end
33 | end
34 |
35 | local opts = {verbose=true}
36 | local failures = lunatest.run(nil, opts)
37 |
38 | if love._os ~= "Windows" then
39 | if failures > 0 then
40 | os.exit(failures)
41 | else
42 | love.event.push("quit")
43 | end
44 | end
45 | end
46 |
47 | return lovetest
48 |
--------------------------------------------------------------------------------
/test/test_collider.lua:
--------------------------------------------------------------------------------
1 | require "../collider"
2 |
3 | local function setup(x, y)
4 | local testColliderObjects = {}
5 | local singleRect = {['x'] = 10, ['y'] = 10, ['width'] = 5, ['height'] = 5}
6 | table.insert(testColliderObjects, singleRect)
7 | local testColliderCharacter = {
8 | ['x'] = x, ['y'] = y
9 | }
10 | return Collider.NewCollider(testColliderObjects), testColliderCharacter
11 | end
12 |
13 | function test_collision_inside_box()
14 | collider, character = setup(13, 13)
15 | assert_equal(true, collider:withinBounds(character))
16 | end
17 |
18 | function test_collision_outside_box()
19 | collider, character = setup(13, 17)
20 | assert_equal(false, collider:withinBounds(character))
21 | end
22 |
23 | function test_collision_on_line_of_box()
24 | collider, character = setup(13, 15)
25 | assert_equal(true, collider:withinBounds(character))
26 | end
27 |
28 | function test_point_caching()
29 | collider, character = setup(17, 13)
30 | assert_equal(false, collider:withinBounds(character))
31 | assert_equal(true, collider:knownPointWithinBounds(character))
32 | end
33 |
34 | function test_collisions_with_multiple_binding_boxes()
35 | collider, character = setup(17, 13)
36 | otherRect = {
37 | ['x'] = 15, ['y'] = 13,
38 | ['width'] = 5, ['height'] = 5
39 | }
40 | collider:addObject(otherRect)
41 | assert_equal(true, collider:withinBounds(character))
42 | end
43 |
--------------------------------------------------------------------------------
/splash_renderer.lua:
--------------------------------------------------------------------------------
1 | require('renderer')
2 |
3 | SplashRenderer = inheritsFrom( Renderer )
4 | SplashRenderer.__index = SplashRenderer
5 |
6 | function SplashRenderer.NewSplash(klass, title)
7 | local self = SplashRenderer:create()
8 |
9 | self.renderItems = {}
10 | self:addBackground()
11 | self:addTitle(title)
12 |
13 | return self
14 | end
15 |
16 | function SplashRenderer:renderItems()
17 | return self.drawables
18 | end
19 |
20 | function SplashRenderer:addRenderItem(d)
21 | table.insert(self.renderItems, d)
22 | end
23 |
24 | function SplashRenderer:addBackground()
25 | local image = love.graphics.newImage('gfx/splash/frank_face.png')
26 | local draw = RenderItem:NewRenderItem(image, 0, 0)
27 | self:addRenderItem(draw)
28 | end
29 |
30 | function SplashRenderer:addTitle(title)
31 | local width, height = love.graphics.getWidth(), love.graphics.getHeight() / 4
32 | local canvas = love.graphics.newCanvas(width, height)
33 | local currentFont = love.graphics.getFont()
34 | love.graphics.setCanvas(canvas)
35 |
36 | -- Draw to local canvas code
37 | local font = love.graphics.newFont('fonts/manaspace.regular.ttf', 50)
38 | love.graphics.setFont(font)
39 | love.graphics.setColor(0x5C,0x5C,0x5C)
40 | local fontWidth, fontHeight = font:getWidth(title), font:getHeight(title)
41 | love.graphics.print(title, (width - fontWidth)/2, (height - fontHeight)/2)
42 | -- /Draw to local canvas code
43 |
44 | if currentFont then love.graphics.setFont(currentFont) end
45 | love.graphics.reset()
46 | love.graphics.setCanvas()
47 | local draw = RenderItem:NewRenderItem(canvas, 0, love.graphics.getHeight() - canvas:getHeight())
48 | self:addRenderItem(draw)
49 | end
50 |
--------------------------------------------------------------------------------
/collider.lua:
--------------------------------------------------------------------------------
1 | Collider = {}
2 | Collider.__index = Collider
3 |
4 | function Collider.NewCollider(objects)
5 | local self = setmetatable({}, Collider)
6 | self.objects = {}
7 | self.colors = {}
8 | self.outsideBounds = {}
9 |
10 | for i, obj in ipairs(objects) do self:addObject(obj) end
11 |
12 | return self
13 | end
14 |
15 | function Collider:cacheBoundCheck(character)
16 | self.outsideBounds[self:cacheKey(character)] = true
17 | end
18 |
19 | function Collider:addObject(object)
20 | table.insert(self.colors, {math.random(255), math.random(255), math.random(255)})
21 | table.insert(self.objects, object)
22 | end
23 |
24 | function Collider:cacheKey(character)
25 | return character.x .. 'x' .. character.y
26 | end
27 |
28 | function Collider:knownPointWithinBounds(character)
29 | return self.outsideBounds[self:cacheKey(character)]
30 | end
31 |
32 | function Collider:performBoundsCheck(character)
33 | local within = false
34 | for i, bound in ipairs(self.objects) do
35 | local normalizedX = character.x - (bound.x + character.adjustmentX)
36 | local normalizedY = character.y - (bound.y + character.adjustmentY)
37 |
38 | if normalizedX >= 0 and normalizedY >= 0 and normalizedX <= bound.width and normalizedY <= bound.height then
39 | self:cacheBoundCheck(character)
40 | within = true
41 | break
42 | end
43 | end
44 | return within
45 | end
46 |
47 | function Collider:withinBounds(character)
48 | --if self:knownPointWithinBounds(character) then
49 | -- return true
50 | --else
51 | local withinBounds = self:performBoundsCheck(character)
52 | if withinBounds then self:cacheBoundCheck(character) end
53 | return withinBounds
54 | --end
55 | end
56 |
57 | function Collider:draw(x, y)
58 | for i, bound in ipairs(self.objects) do
59 | love.graphics.setColor(self.colors[i])
60 | love.graphics.rectangle('line', bound.x + adjustmentX, bound.y + adjustmentY, bound.width, bound.height)
61 | love.graphics.setColor(255, 255, 255)
62 | love.graphics.print('('..adjustmentX..','..adjustmentY..')', 50, 50)
63 | end
64 | love.graphics.reset()
65 | end
66 |
--------------------------------------------------------------------------------
/status.lua:
--------------------------------------------------------------------------------
1 | require("cursor")
2 |
3 | Status = {
4 | PADDING_X = 46,
5 | PADDING_Y = 46,
6 | FULLHP = love.graphics.newImage('gfx/full_heart.png'),
7 | MISSHP = love.graphics.newImage('gfx/empty_heart.png'),
8 | FULLFOC = love.graphics.newImage('gfx/focus_full.png'),
9 | MISSFOC = love.graphics.newImage('gfx/focus_empty.png'),
10 | BACKGROUND = love.graphics.newImage("gfx/statusbox.png")
11 | }
12 | Status.__index = Status
13 |
14 | function Status.NewStatus(x, y, character)
15 | local self = setmetatable({}, Status)
16 | self.x = x
17 | self.y = y - Status.BACKGROUND:getHeight()
18 | self.cursor = Cursor.NewCursor(self.x + Status.PADDING_X, self.y + Status.PADDING_Y)
19 | self.character = character
20 |
21 | return self
22 | end
23 |
24 | function Status:center()
25 | self.center_x = self.center_x or (self.x + (Status.BACKGROUND:getWidth() / 2))
26 | self.center_y = self.center_y or (self.y + (Status.BACKGROUND:getHeight() / 2))
27 | self.center_cursor = self.center_cursor or Cursor.NewCursor(self.center_x, self.center_y)
28 | self.center_cursor:reset()
29 | return self.center_cursor
30 | end
31 |
32 | function Status:draw()
33 | love.graphics.draw(Status.BACKGROUND, self.x, self.y)
34 | self:drawHealth(self.cursor)
35 | self:drawFocus(self.cursor)
36 | love.graphics.reset()
37 | end
38 |
39 | function Status:drawHealth(cursor)
40 | cursor:reset()
41 | width, height = Status.FULLHP:getWidth(), Status.FULLHP:getHeight()
42 | for i=1,self.character.max_hp do
43 | if i <= self.character.current_hp then
44 | image = Status.FULLHP
45 | else
46 | image = Status.MISSHP
47 | end
48 | love.graphics.draw(image, cursor.x, cursor.y)
49 | cursor:moveBy(width, 0)
50 | end
51 | end
52 |
53 | function Status:drawFocus(cursor)
54 | cursor:reset()
55 | width, height = Status.FULLFOC:getWidth(), Status.FULLFOC:getHeight()
56 | cursor:moveBy(Status.BACKGROUND:getWidth() - Status.PADDING_X*3, 0)
57 | for i=1,self.character.max_focus do
58 | if i <= self.character.current_focus then
59 | image = Status.FULLFOC
60 | else
61 | image = Status.MISSFOC
62 | end
63 | love.graphics.draw(image, cursor.x, cursor.y)
64 | cursor:moveBy(-width, 0)
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/dungeon_master.lua:
--------------------------------------------------------------------------------
1 | require('monsters')
2 | require('keyboard_handler')
3 | DungeonMaster = {SPRITE_SIZE = 64, DIRECTIONS = {'up', 'down', 'left', 'right'}}
4 | DungeonMaster.__index = DungeonMaster
5 |
6 | function DungeonMaster.NewMaster(character, collider, x, y)
7 | local self = setmetatable({}, DungeonMaster)
8 |
9 | self.monsters = {}
10 | self.character = character
11 | self.collider = collider
12 | self.x = x
13 | self.y = y
14 |
15 | return self
16 | end
17 |
18 | function DungeonMaster:spawnNewMonster()
19 | if table.getn(self.monsters) < 3 then
20 | while true do
21 | local xMult = self:genMult()
22 | local yMult = self:genMult()
23 | local deltaX = math.random(5) * 64
24 | local deltaY = math.random(5) * 64
25 | local startX = self.x + xMult*deltaX
26 | local startY = self.y + yMult*deltaY
27 | local monster = Monsters.gooBall(startX, startY)
28 | monster.adjustmentX = adjustmentX
29 | monster.adjustmentY = adjustmentY
30 | if self.collider:withinBounds(monster) then
31 | self:addMonster(monster)
32 | return
33 | end
34 | end
35 | end
36 | end
37 |
38 | function DungeonMaster:addMonster(monster)
39 | table.insert(self.monsters, monster)
40 | end
41 |
42 | function DungeonMaster:updateMonsterPositions(dx, dy)
43 | local success = true
44 | updater = function(monster)
45 | if character:isBeside(monster) then
46 | character:attack(monster)
47 | success = false
48 | else
49 | monster:applyDelta(dx, dy)
50 | end
51 | end
52 | self:monsterProcess(updater)
53 | return success
54 | end
55 |
56 | function DungeonMaster:genMult()
57 | if math.random(2) == 2 then return 1 else return -1 end
58 | end
59 |
60 | function DungeonMaster:tick(response, direction)
61 | local deadMonsters = {}
62 | if Response == KeyboardHandler.PROCESSING then return end
63 | mover = function(monster)
64 | if monster:dead() then
65 | table.insert(deadMonsters, monster)
66 | elseif monster:isBeside(self.character) then
67 | monster:attack(character)
68 | else
69 | local x, y = monster:move(direction or self:determineDirection(), monster.x, monster.y, self.collider)
70 | monster:setPosition(x, y)
71 | end
72 | end
73 | if response == KeyboardHandler.RESTING and self.character:injured() then self:rollForHeals(self.character) end
74 | self:rollForMonsters()
75 | self:monsterProcess(mover)
76 | self:cullDeadMonsters(deadMonsters)
77 | end
78 |
79 | function DungeonMaster:draw()
80 | drawer = function(monster)
81 | monster:draw()
82 | end
83 | self:monsterProcess(drawer)
84 | end
85 |
86 | function DungeonMaster:cullDeadMonsters(monsters)
87 | for i, monster in ipairs(monsters) do
88 | table.remove(self.monsters, i)
89 | end
90 | end
91 |
92 | function DungeonMaster:monsterProcess(f)
93 | for i, monster in ipairs(self.monsters) do
94 | f(monster)
95 | end
96 | end
97 |
98 | function DungeonMaster:rollForMonsters()
99 | if math.random(15) <= 2 then
100 | self:spawnNewMonster()
101 | end
102 | end
103 |
104 | function DungeonMaster:rollForHeals(char)
105 | if math.random(10) == 1 then
106 | self.heal = self.heal or love.audio.newSource('audio/fx/heal.ogg')
107 | if char == self.character then self.heal:play() end
108 | char:healRandom()
109 | end
110 | end
111 |
112 | function DungeonMaster:determineDirection()
113 | local index = math.random(table.getn(DungeonMaster.DIRECTIONS))
114 | local direction = DungeonMaster.DIRECTIONS[index]
115 | return direction
116 | end
117 |
--------------------------------------------------------------------------------
/main.lua:
--------------------------------------------------------------------------------
1 | love.filesystem.load("tiledmap.lua")()
2 | local lovetest = require("test/lovetest")
3 | --local ProFi = require('ProFi')
4 | require("character")
5 | require("word_queue")
6 | require("keyboard_handler")
7 | require("mode_switcher")
8 | require("status")
9 | require("cursor")
10 | require("collider")
11 | require("dungeon_master")
12 | require('splash_renderer')
13 |
14 | gKeyPressed = {}
15 | gCamX,gCamY = 100,100
16 | character = nil
17 | words = nil
18 | status = nil
19 | modeSwitcher = nil
20 | gameDebug = false
21 | dungeonMaster = nil
22 | collider = nil
23 | -- otherwise the collisions for monsters get all fucked up
24 | -- TODO: need to figure out WHY this is the way it is.
25 | adjustmentX, adjustmentY = 0, 0
26 | timer = nil
27 |
28 | function love.load()
29 | --ProFi:start()
30 | --if arg and arg[#arg] == "-debug" then require("mobdebug").start() end
31 | if lovetest.detect(arg) then lovetest.run() end
32 |
33 | splash = SplashRenderer:NewSplash("Frank Face Games")
34 |
35 | local mapObjects = TiledMap_Load("maps/level0.tmx", 64)
36 | collider = Collider.NewCollider(mapObjects['Floor'])
37 | love.graphics.setNewFont('fonts/manaspace.regular.ttf', 14)
38 | startingPoint = mapObjects['Setup']['StartingPoint']
39 | gCamX = startingPoint['x']
40 | gCamY = startingPoint['y']
41 | adjustmentX = tonumber(startingPoint.properties.adjustX)
42 | adjustmentY = tonumber(startingPoint.properties.adjustY)
43 | character = Character.NewCharacter(love.graphics.getWidth() / 2, love.graphics.getHeight() / 2, 'hero')
44 | status = Status.NewStatus(0, love.graphics.getHeight(), character)
45 |
46 | prepareHandlers()
47 | prepareDungeon(character, collider, gCamX, gCamY)
48 | timer = 0
49 | end
50 |
51 | function love.keyreleased( key )
52 | gKeyPressed[key] = nil
53 | end
54 |
55 | function love.keypressed(key, unicode)
56 | if timer < 5.0 then return end
57 | gKeyPressed[key] = true
58 | if (key == "escape") then
59 | --ProFi:stop()
60 | --ProFi:writeReport('ProfileResults.txt')
61 | end
62 | if (key == "=") then modeSwitcher:reset() end
63 | if (key == "1" or key == "2" or key == "3" or key == "4") then return end
64 | if (key == "up" or key == "down" or key == "left" or key == "right") then
65 | dungeonMaster:tick(KeyboardHandler.SUCCESS, key)
66 | return
67 | end
68 | response = modeSwitcher:handle(key)
69 | if response == KeyboardHandler.PROCESSING then
70 | return
71 | else
72 | tick(response)
73 | end
74 | end
75 |
76 | function drawImage(cursor, image, rads)
77 | width, height = image:getWidth() / 2, image:getHeight() / 2
78 | multX, multY = 1, 1
79 | if rads == 3.14159 then -- rotated 180deg
80 | -- nothing to
81 | elseif rads > 3.14159 and rads < 2*3.14159 then -- rotated 270deg
82 | multX = -1
83 | elseif rads > 0 and rads < 3.14159 then -- rotated 90deg
84 | multX = 1
85 | multY = -1
86 | else -- rotated 0deg
87 | multX = -1
88 | multY = -1
89 | end
90 |
91 | love.graphics.draw(image, cursor.x + (multX*width), cursor.y + (multY*height), rads)
92 | if gameDebug then cursor:draw() end
93 | end
94 |
95 | function setFontSize(size)
96 | love.graphics.setNewFont('fonts/manaspace.regular.ttf', size or 14)
97 | end
98 |
99 | function handler()
100 | return modeSwitcher:handler()
101 | end
102 |
103 | function tick(response)
104 | if extraTick then
105 | extraTick = false
106 | dungeonMaster:tick(KeyboardHandler.RESTING)
107 | end
108 |
109 | if response == KeyboardHandler.PROCESSING then return end
110 |
111 | if response == KeyboardHandler.SUCCESS then
112 | local updatedX, updatedY, success = character:move(handler():direction(), gCamX, gCamY, collider)
113 | success = success and dungeonMaster:updateMonsterPositions(gCamX-updatedX, gCamY-updatedY)
114 | if success then
115 | gCamX, gCamY = updatedX, updatedY
116 | else
117 | print("could not move that way!")
118 | end
119 | elseif response == KeyboardHandler.FAILURE and modeSwitcher:inflictsInjuries() then
120 | character:injure('focus')
121 | end
122 | modeSwitcher:reset()
123 | dungeonMaster:tick(response)
124 | if character:unfocused() then extraTick = true end
125 | end
126 |
127 | function prepareHandlers()
128 | words = WordQueue.NewQueue('basic_english')
129 | movementHandler = KeyboardHandler.NewHandler(words)
130 | statusHandler = KeyboardHandler.NewHandler(words)
131 | modeSwitcher = ModeSwitcher.NewModeSwitcher({movementHandler, statusHandler})
132 | end
133 |
134 | function prepareDungeon(character, collider, x, y)
135 | dungeonMaster = DungeonMaster.NewMaster(character, collider, x, y)
136 | dungeonMaster:spawnNewMonster()
137 | end
138 |
139 | function love.update(dt)
140 | timer = timer + dt
141 | local s = 100*dt
142 | if(gKeyPressed['1']) then adjustmentX = adjustmentX - s end
143 | if(gKeyPressed['2']) then adjustmentX = adjustmentX + s end
144 | if(gKeyPressed['3']) then adjustmentY = adjustmentY - s end
145 | if(gKeyPressed['4']) then adjustmentY = adjustmentY + s end
146 | -- if(gKeyPressed.up) then gCamY = gCamY - s end
147 | -- if(gKeyPressed.down) then gCamY = gCamY + s end
148 | -- if(gKeyPressed.left) then gCamX = gCamX - s end
149 | -- if(gKeyPressed.right) then gCamX = gCamX + s end
150 | collectgarbage('collect') -- "Fixes" memory leak
151 | end
152 |
153 | function love.draw()
154 | love.graphics.setBackgroundColor(0x80, 0x80, 0x80)
155 | TiledMap_DrawNearCam(gCamX, gCamY)
156 | --collider:draw(gCamX, gCamY)
157 | dungeonMaster:draw()
158 | character:draw()
159 | status:draw()
160 | handler():draw(status:center())
161 | if(timer < 3.0) then
162 | splash:draw()
163 | elseif timer > 3.0 and splash ~= nil then
164 | splash = nil
165 | end
166 | end
167 |
--------------------------------------------------------------------------------
/character.lua:
--------------------------------------------------------------------------------
1 | Character = {MAX_HP = 5, MAX_FOCUS = 5, SCALE=1, COUNTER=0}
2 | Character.Sprites = {
3 | ['hero'] = love.graphics.newImage('gfx/hero.png'),
4 | ['gooball'] = love.graphics.newImage('gfx/gooball.png')
5 | }
6 | Character.Fx = {
7 | ['hero'] = {
8 | ['injure'] = love.audio.newSource('audio/fx/heroinjure.ogg', 'static'),
9 | ['die'] = love.audio.newSource('audio/fx/herodie.ogg', 'static')
10 | },
11 | ['gooball'] = {
12 | ['injure'] = love.audio.newSource('audio/fx/gooballinjure.ogg', 'static'),
13 | ['die'] = love.audio.newSource('audio/fx/gooballdie.ogg', 'static')
14 | }
15 | }
16 | Character.__index = Character
17 |
18 | function Character.NewCharacter(x, y, name, scale)
19 | local self = setmetatable({}, Character)
20 |
21 | self.current_hp = Character.MAX_HP
22 | self.max_hp = Character.MAX_HP
23 | self.current_focus = Character.MAX_FOCUS
24 | self.max_focus = Character.MAX_FOCUS
25 | self.sprite = Character.Sprites[name]
26 | self.injurySound = Character.Fx[name]['injure']
27 | self.deathSound = Character.Fx[name]['die']
28 | self:setPosition(x, y)
29 | self:setDrawPosition(x, y)
30 | self.adjustmentX = 0
31 | self.adjustmentY = 0
32 | self.scale = scale or Character.SCALE
33 | self.width = self.scale * self.sprite:getWidth()
34 | self.height = self.scale * self.sprite:getHeight()
35 | self.name = name .. Character.COUNTER
36 | Character.COUNTER = Character.COUNTER + 1
37 |
38 | return self
39 | end
40 |
41 | function Character:setHp(current, max)
42 | self.current_hp = current or self.current_hp
43 | self.max_hp = max or self.max_hp
44 | self:correctDiscrepancies()
45 | end
46 |
47 | function Character:setFocus(current, max)
48 | self.current_focus = current or self.current_focus
49 | self.max_focus = max or self.max_focus
50 | self:correctDiscrepancies()
51 | end
52 |
53 | function Character:correctDiscrepancies()
54 | if self.current_hp > self.max_hp then
55 | self.current_hp = self.max_hp
56 | end
57 | if self.current_focus > self.max_focus then
58 | self.current_focus = self.max_focus
59 | end
60 | end
61 |
62 | function Character:injuredHp()
63 | return self.current_hp < self.max_hp
64 | end
65 |
66 | function Character:injuredFocus()
67 | return self.current_focus < self.max_focus
68 | end
69 |
70 | function Character:injured()
71 | return self:injuredHp() or self:injuredFocus()
72 | end
73 |
74 | function Character:unfocused()
75 | return self.current_focus <= 0
76 | end
77 |
78 | function Character:dead()
79 | return self.current_hp <= 0
80 | end
81 |
82 | function Character:move(direction, x, y, collider)
83 | local dx, dy = x or self.x, y or self.y
84 | if direction == "up" then
85 | dy = dy - self.height
86 | elseif direction == "down" then
87 | dy = dy + self.height
88 | elseif direction == "left" then
89 | dx = dx - self.width
90 | elseif direction == "right" then
91 | dx = dx + self.width
92 | end
93 | if collider then
94 | local newPosition = {
95 | ['x'] = dx, ['y'] = dy,
96 | ['name'] = self.name,
97 | ['adjustmentX'] = self.adjustmentX or 0,
98 | ['adjustmentY'] = self.adjustmentY or 0
99 | }
100 | if not collider:withinBounds(newPosition) then return x, y, false end
101 | end
102 | return dx, dy, true
103 | end
104 |
105 | function Character:setPosition(x, y)
106 | local oldX, oldY = (self.x or x), (self.y or y)
107 | self:applyDelta(x-oldX, y - oldY)
108 | self.x, self.y = x, y
109 | end
110 |
111 | function Character:setDrawPosition(x, y)
112 | self.drawX, self.drawY = x, y
113 | end
114 |
115 | function Character:applyDelta(dx, dy)
116 | self.drawX = (self.drawX or 0) + (dx or 0)
117 | self.drawY = (self.drawY or 0) + (dy or 0)
118 | end
119 |
120 | function Character:injure(attribute)
121 | if attribute == "health" then
122 | self.current_hp = self.current_hp - 1
123 | if self:dead() then
124 | self.deathSound:play()
125 | else
126 | self.injurySound:play()
127 | end
128 | elseif attribute == "focus" then
129 | self.current_focus = self.current_focus - 1
130 | end
131 | end
132 |
133 | function Character:heal(attribute, amount)
134 | amount = amount or 1
135 | if attribute == "health" then
136 | self.current_hp = self.current_hp + amount
137 | elseif attribute == "focus" then
138 | self.current_focus = self.current_focus + amount
139 | end
140 | self:correctDiscrepancies()
141 | end
142 |
143 | function Character:healRandom(amount)
144 | attrs = {}
145 | if self:injuredHp() then table.insert(attrs, 'health') end
146 | if self:injuredFocus() then table.insert(attrs, 'focus') end
147 | attribute = attrs[math.random(table.getn(attrs))]
148 | self:heal(attribute, amount)
149 | end
150 |
151 | function Character:center()
152 | return (self.drawX or self.x) + self.height/2, (self.drawY or self.y) + self.width/2
153 | end
154 |
155 | function Character:radius()
156 | if self.height > self.width then
157 | return self.height * self.height
158 | else
159 | return self.width * self.height
160 | end
161 | end
162 |
163 | function Character:attack(other)
164 | other:injure('health')
165 | end
166 |
167 | function Character:isBeside(other)
168 | local cx, cy = self:center()
169 | local ocx, ocy = other:center()
170 | local a = math.abs(ocy - cy)
171 | local b = math.abs(ocx - cx)
172 | local radius = (a * a) + (b * b)
173 | local delta = (3.14159*2*self:radius()) - (3.14159*2*radius)
174 | return delta > 0
175 | end
176 |
177 | function Character:draw()
178 | love.graphics.draw(self.sprite, self.drawX, self.drawY, 0, self.scale, self.scale)
179 | -- love.graphics.circle('fill', self.drawX, self.drawY, 2)
180 | -- local cx, cy = self:center()
181 | -- love.graphics.circle('fill', cx, cy, 2)
182 | end
183 |
--------------------------------------------------------------------------------
/keyboard_handler.lua:
--------------------------------------------------------------------------------
1 | KeyboardHandler = {
2 | PUP = 1,
3 | PDOWN = 2,
4 | PLEFT = 3,
5 | PRIGHT = 4,
6 | SUCCESS = 5,
7 | FAILURE = 6,
8 | PROCESSING = 7,
9 | RESTING = 8,
10 | ARROW = love.graphics.newImage('gfx/arrow_up.png'),
11 | ONLYDRAW = 4,
12 | GRUNTS = {
13 | love.audio.newSource('audio/fx/grunt1.ogg', 'static'),
14 | love.audio.newSource('audio/fx/grunt2.ogg', 'static'),
15 | love.audio.newSource('audio/fx/grunt3.ogg', 'static')
16 | },
17 | MAX_ERRORS = 3
18 | }
19 |
20 | KeyboardHandler.__index = KeyboardHandler
21 |
22 | function KeyboardHandler.NewHandler(dictionary, words, injureOnFailure)
23 | if gameDebug then
24 | KeyboardHandler.ARROW = love.graphics.newImage('gfx/arrow_up_dbg.png')
25 | end
26 |
27 | local self = setmetatable({}, KeyboardHandler)
28 | self.errorCount = 1
29 | self.status = KeyboardHandler.PROCESSING
30 | self.dictionary = dictionary
31 | self.injureOnFailure = injureOnFailure
32 | self:setWords(words or dictionary:grab())
33 |
34 | return self
35 | end
36 |
37 | function KeyboardHandler:reset()
38 | self:setWords(self.dictionary:grab())
39 | end
40 |
41 | function KeyboardHandler:setWords(words)
42 | self.wordStrings = words
43 | self.staticWords = {}
44 | self.words = {}
45 | self.chosenWord = nil
46 | for i = 1,table.getn(words) do
47 | word = {}
48 | staticWord = {}
49 | for c in words[i]:gmatch(".") do
50 | table.insert(word, c)
51 | table.insert(staticWord, c)
52 | end
53 | table.insert(self.words, word)
54 | table.insert(self.staticWords, staticWord)
55 | end
56 | end
57 |
58 | function KeyboardHandler:chosenDirection()
59 | return self.chosenWord
60 | end
61 |
62 | function KeyboardHandler:getWord(position)
63 | return self.staticWords[postion]
64 | end
65 |
66 | function KeyboardHandler:handle(key)
67 | if key == " " then return KeyboardHandler.RESTING end
68 | if self.chosenWord == nil then
69 | for i=1,table.getn(self.words) do
70 | if self.words[i][1] == key then
71 | self.chosenWord = i
72 | end
73 | end
74 | end
75 | self.status = self:handleChosenWord(key)
76 | return self.status
77 | end
78 |
79 | function KeyboardHandler:handleChosenWord(key)
80 | if self.chosenWord == nil and not self:withinErrorThreshold() then
81 | self.errorCount = 0
82 | return KeyboardHandler.FAILURE
83 | end
84 |
85 | word = self.words[self.chosenWord]
86 |
87 | if word and word[1] == key then
88 | self.errorCount = 0
89 | table.remove(word, 1)
90 | elseif self:withinErrorThreshold() then
91 | self:playGrunt()
92 | else
93 | self.errorCount = 0
94 | return KeyboardHandler.FAILURE
95 | end
96 |
97 | if word and table.getn(word) == 0 then
98 | return KeyboardHandler.SUCCESS
99 | else
100 | return KeyboardHandler.PROCESSING
101 | end
102 | end
103 |
104 | function KeyboardHandler:direction()
105 | d = self.chosenWord
106 | dw = nil
107 | if d == KeyboardHandler.PUP then
108 | dw = "up"
109 | elseif d == KeyboardHandler.PDOWN then
110 | dw = "down"
111 | elseif d == KeyboardHandler.PLEFT then
112 | dw = "left"
113 | elseif d == KeyboardHandler.PRIGHT then
114 | dw = "right"
115 | end
116 | return dw
117 | end
118 |
119 | function KeyboardHandler:draw(cursor)
120 | image = KeyboardHandler.ARROW
121 | setFontSize(22)
122 |
123 | local width, height = image:getWidth(), image:getHeight()
124 | for i=1,table.getn(self.words) do
125 | if gameDebug and i ~= KeyboardHandler.ONLYDRAW then
126 | -- do nothing
127 | else
128 | cursor:reset()
129 | if i == KeyboardHandler.PUP then
130 | cursor:moveBy(0, -height/2)
131 | drawImage(cursor, image, 0)
132 | cursor:moveBy(0, -height/2)
133 | elseif i == KeyboardHandler.PDOWN then
134 | cursor:moveBy(0, height/2)
135 | drawImage(cursor, image, 3.14159)
136 | cursor:moveBy(0, height/2)
137 | elseif i == KeyboardHandler.PLEFT then
138 | cursor:moveBy(-width/2, 0)
139 | drawImage(cursor, image, 3.14149 * 1.5)
140 | cursor:moveBy(-width/2, 0)
141 | else
142 | cursor:moveBy(width/2, 0)
143 | drawImage(cursor, image, 3.14159 / 2)
144 | cursor:moveBy(width/2, 0)
145 | end
146 | self:drawWord(cursor, i)
147 | -- cursor:draw()
148 | end
149 | end
150 | cursor:reset()
151 | -- cursor:draw({0xFF, 0, 0})
152 | end
153 |
154 | function KeyboardHandler:drawWord(cursor, i)
155 | local word = self.wordStrings[i]
156 | local font = love.graphics.getFont()
157 | local static = self.staticWords[i]
158 | local actual = self.words[i]
159 | local width = font:getWidth(word)
160 | local height = font:getHeight(word)
161 | local sidx, idx = 1, 1
162 | while static[sidx] ~= actual[idx] do
163 | sidx = sidx + 1
164 | end
165 | self:compensateForLocation(word, cursor, i, font)
166 | cursor:moveBy(-width/2, -height/2)
167 | if sidx ~= idx then
168 | love.graphics.setColor(0xFF, 0, 0)
169 | message = string.sub(word, 1, sidx - 1)
170 | love.graphics.print(message, cursor.x, cursor.y)
171 | cursor:moveBy(font:getWidth(message), 0)
172 | end
173 | love.graphics.setColor(0xFF, 0xFF, 0xFF)
174 | message = string.sub(word, sidx)
175 | love.graphics.print(message, cursor.x, cursor.y)
176 | love.graphics.reset()
177 | end
178 |
179 | function KeyboardHandler:compensateForLocation(word, cursor, i, font)
180 | if i == KeyboardHandler.PUP then
181 | cursor:moveBy(0, -5)
182 | elseif i == KeyboardHandler.PDOWN then
183 | cursor:moveBy(0, 5)
184 | elseif i == KeyboardHandler.PLEFT then
185 | cursor:moveBy(-font:getWidth(word)/2,0)
186 | else
187 | cursor:moveBy(font:getWidth(word)/2, 0)
188 | end
189 | end
190 |
191 | function KeyboardHandler:withinErrorThreshold()
192 | return self.errorCount < KeyboardHandler.MAX_ERRORS
193 | end
194 |
195 | function KeyboardHandler:playGrunt()
196 | self.errorCount = self.errorCount + 1
197 | print(self.errorCount)
198 | local index = math.random(table.getn(KeyboardHandler.GRUNTS))
199 | KeyboardHandler.GRUNTS[index]:play()
200 | end
201 |
--------------------------------------------------------------------------------
/tiledmap.lua:
--------------------------------------------------------------------------------
1 | -- loader for "tiled" map editor maps (.tmx,xml-based) http://www.mapeditor.org/
2 | -- supports multiple layers
3 | -- NOTE : function ReplaceMapTileClass (tx,ty,oldTileType,newTileType,fun_callback) end
4 | -- NOTE : function TransmuteMap (from_to_table) end -- from_to_table[old]=new
5 | -- NOTE : function GetMousePosOnMap () return gMouseX+gCamX-gScreenW/2,gMouseY+gCamY-gScreenH/2 end
6 |
7 | kTileSize = 32
8 | kMapTileTypeEmpty = 0
9 | scale = 1
10 | local floor = math.floor
11 | local ceil = math.ceil
12 |
13 | function TiledMap_Load (filepath,tilesize,spritepath_removeold,spritepath_prefix)
14 | spritepath_removeold = spritepath_removeold or "../"
15 | spritepath_prefix = spritepath_prefix or ""
16 | kTileSize = tilesize or 32
17 | gTileGfx = {}
18 |
19 | local tiletype,layers, objects = TiledMap_Parse(filepath)
20 | gMapLayers = layers
21 | for first_gid,path in pairs(tiletype) do
22 | path = spritepath_prefix .. string.gsub(path,"^"..string.gsub(spritepath_removeold,"%.","%%."),"")
23 | local raw = love.image.newImageData(path)
24 | local w,h = raw:getWidth(),raw:getHeight()
25 | local gid = first_gid
26 | local e = kTileSize
27 | for y=0,floor(h/kTileSize)-1 do
28 | for x=0,floor(w/kTileSize)-1 do
29 | local sprite = love.image.newImageData(kTileSize,kTileSize)
30 | sprite:paste(raw,0,0,x*e,y*e,e,e)
31 | gTileGfx[gid] = love.graphics.newImage(sprite)
32 | gid = gid + 1
33 | end
34 | end
35 | end
36 | return objects
37 | end
38 |
39 | function TiledMap_GetMapTile (tx,ty,layerid) -- coords in tiles
40 | local row = gMapLayers[layerid][ty]
41 | return row and row[tx] or kMapTileTypeEmpty
42 | end
43 |
44 | function TiledMap_Project(camx, camy) -- convert camera coordinates to tile coordinates
45 | return camx * kTileSize, camy * kTileSize
46 | end
47 |
48 | function TiledMap_DrawNearCam (camx,camy)
49 | camx,camy = floor(camx),floor(camy)
50 | local screen_w = love.graphics.getWidth()
51 | local screen_h = love.graphics.getHeight()
52 | local minx,maxx = floor((camx-screen_w/2)/kTileSize),ceil((camx+screen_w/2)/kTileSize)
53 | local miny,maxy = floor((camy-screen_h/2)/kTileSize),ceil((camy+screen_h/2)/kTileSize)
54 | for z = 1,#gMapLayers do
55 | for x = minx,maxx do
56 | for y = miny,maxy do
57 | local gfx = gTileGfx[TiledMap_GetMapTile(x,y,z)]
58 | if (gfx) then
59 | local sx = (x*kTileSize - camx + screen_w/2) * scale
60 | local sy = (y*kTileSize - camy + screen_h/2) * scale
61 | love.graphics.draw(gfx,sx,sy,0,scale,scale) -- x, y, r, sx, sy, ox, oy
62 | end
63 | end
64 | end
65 | end
66 | end
67 |
68 |
69 | -- ***** ***** ***** ***** ***** xml parser
70 |
71 |
72 | -- LoadXML from http://lua-users.org/wiki/LuaXml
73 | function LoadXML(s)
74 | local function LoadXML_parseargs(s)
75 | local arg = {}
76 | string.gsub(s, "(%w+)=([\"'])(.-)%2", function (w, _, a)
77 | arg[w] = a
78 | end)
79 | return arg
80 | end
81 | local stack = {}
82 | local top = {}
83 | table.insert(stack, top)
84 | local ni,c,label,xarg, empty
85 | local i, j = 1, 1
86 | while true do
87 | ni,j,c,label,xarg, empty = string.find(s, "<(%/?)([%w:]+)(.-)(%/?)>", i)
88 | if not ni then break end
89 | local text = string.sub(s, i, ni-1)
90 | if not string.find(text, "^%s*$") then
91 | table.insert(top, text)
92 | end
93 | if empty == "/" then -- empty element tag
94 | table.insert(top, {label=label, xarg=LoadXML_parseargs(xarg), empty=1})
95 | elseif c == "" then -- start tag
96 | top = {label=label, xarg=LoadXML_parseargs(xarg)}
97 | table.insert(stack, top) -- new level
98 | else -- end tag
99 | local toclose = table.remove(stack) -- remove top
100 | top = stack[#stack]
101 | if #stack < 1 then
102 | error("nothing to close with "..label)
103 | end
104 | if toclose.label ~= label then
105 | error("trying to close "..toclose.label.." with "..label)
106 | end
107 | table.insert(top, toclose)
108 | end
109 | i = j+1
110 | end
111 | local text = string.sub(s, i)
112 | if not string.find(text, "^%s*$") then
113 | table.insert(stack[#stack], text)
114 | end
115 | if #stack > 1 then
116 | error("unclosed "..stack[stack.n].label)
117 | end
118 | return stack[1]
119 | end
120 |
121 |
122 | -- ***** ***** ***** ***** ***** parsing the tilemap xml file
123 |
124 | local function getTilesets(node)
125 | local tiles = {}
126 | for k, sub in ipairs(node) do
127 | if (sub.label == "tileset") then
128 | tiles[tonumber(sub.xarg.firstgid)] = sub[1].xarg.source
129 | end
130 | end
131 | return tiles
132 | end
133 |
134 | local function getLayers(node)
135 | local layers = {}
136 | for k, sub in ipairs(node) do
137 | if (sub.label == "layer") then -- and sub.xarg.name == layer_name
138 | local layer = {}
139 | table.insert(layers,layer)
140 | width = tonumber(sub.xarg.width)
141 | i = 1
142 | j = 1
143 | for l, child in ipairs(sub[1]) do
144 | if (j == 1) then
145 | layer[i] = {}
146 | end
147 | layer[i][j] = tonumber(child.xarg.gid)
148 | j = j + 1
149 | if j > width then
150 | j = 1
151 | i = i + 1
152 | end
153 | end
154 | end
155 | end
156 | return layers
157 | end
158 |
159 | local function getProperties(node)
160 | local properties = {}
161 | for k, sub in ipairs(node) do
162 | if sub.label == 'properties' then
163 | for l, prop in ipairs(sub) do
164 | properties[prop.xarg.name] = prop.xarg.value
165 | end
166 | end
167 | end
168 | return properties
169 | end
170 |
171 | local function getObjects(node)
172 | local objects = {}
173 | for k, sub in ipairs(node) do
174 | if sub.label == 'objectgroup' then
175 | local objgroup = {}
176 | for l, obj in ipairs(sub) do
177 | if obj.label == "object" and obj.xarg.name then
178 | local object = {}
179 | object['x'] = tonumber(obj.xarg.x)
180 | object['y'] = tonumber(obj.xarg.y)
181 | object['width'] = tonumber(obj.xarg.width)
182 | object['height'] = tonumber(obj.xarg.height)
183 | object['properties'] = getProperties(obj)
184 | table.insert(objgroup, object)
185 | objgroup[obj.xarg.name] = object
186 | end
187 | objects[sub.xarg.name] = objgroup
188 | end
189 | end
190 | end
191 | return objects
192 | end
193 |
194 | function TiledMap_Parse(filename)
195 | local xml = LoadXML(love.filesystem.read(filename))
196 | local tiles = getTilesets(xml[2])
197 | local layers = getLayers(xml[2])
198 | local objects = getObjects(xml[2])
199 | return tiles, layers, objects
200 | end
201 |
202 |
--------------------------------------------------------------------------------
/maps/basic.tmx:
--------------------------------------------------------------------------------
1 |
2 |
411 |
--------------------------------------------------------------------------------
/ProFi.lua:
--------------------------------------------------------------------------------
1 | --[[
2 | ProFi v1.3, by Luke Perkin 2012. MIT Licence http://www.opensource.org/licenses/mit-license.php.
3 |
4 | Example:
5 | ProFi = require 'ProFi'
6 | ProFi:start()
7 | some_function()
8 | another_function()
9 | coroutine.resume( some_coroutine )
10 | ProFi:stop()
11 | ProFi:writeReport( 'MyProfilingReport.txt' )
12 |
13 | API:
14 | *Arguments are specified as: type/name/default.
15 | ProFi:start( string/once/nil )
16 | ProFi:stop()
17 | ProFi:checkMemory( number/interval/0, string/note/'' )
18 | ProFi:writeReport( string/filename/'ProFi.txt' )
19 | ProFi:reset()
20 | ProFi:setHookCount( number/hookCount/0 )
21 | ProFi:setGetTimeMethod( function/getTimeMethod/os.clock )
22 | ProFi:setInspect( string/methodName, number/levels/1 )
23 | ]]
24 |
25 | -----------------------
26 | -- Locals:
27 | -----------------------
28 |
29 | local ProFi = {}
30 | local onDebugHook, sortByDurationDesc, sortByCallCount, getTime
31 | local DEFAULT_DEBUG_HOOK_COUNT = 0
32 | local FORMAT_HEADER_LINE = "| %-50s: %-40s: %-20s: %-12s: %-12s: %-12s|\n"
33 | local FORMAT_OUTPUT_LINE = "| %s: %-12s: %-12s: %-12s|\n"
34 | local FORMAT_INSPECTION_LINE = "> %s: %-12s\n"
35 | local FORMAT_TOTALTIME_LINE = "| TOTAL TIME = %f\n"
36 | local FORMAT_MEMORY_LINE = "| %-20s: %-16s: %-16s| %s\n"
37 | local FORMAT_HIGH_MEMORY_LINE = "H %-20s: %-16s: %-16sH %s\n"
38 | local FORMAT_LOW_MEMORY_LINE = "L %-20s: %-16s: %-16sL %s\n"
39 | local FORMAT_TITLE = "%-50.50s: %-40.40s: %-20s"
40 | local FORMAT_LINENUM = "%4i"
41 | local FORMAT_TIME = "%04.3f"
42 | local FORMAT_RELATIVE = "%03.2f%%"
43 | local FORMAT_COUNT = "%7i"
44 | local FORMAT_KBYTES = "%7i Kbytes"
45 | local FORMAT_MBYTES = "%7.1f Mbytes"
46 | local FORMAT_MEMORY_HEADER1 = "\n=== HIGH & LOW MEMORY USAGE ===============================\n"
47 | local FORMAT_MEMORY_HEADER2 = "=== MEMORY USAGE ==========================================\n"
48 | local FORMAT_BANNER = [[
49 | ###############################################################################################################
50 | ##### ProFi, a lua profiler. This profile was generated on: %s
51 | ##### ProFi is created by Luke Perkin 2012 under the MIT Licence, www.locofilm.co.uk
52 | ##### Version 1.3. Get the most recent version at this gist: https://gist.github.com/2838755
53 | ###############################################################################################################
54 |
55 | ]]
56 |
57 | -----------------------
58 | -- Public Methods:
59 | -----------------------
60 |
61 | --[[
62 | Starts profiling any method that is called between this and ProFi:stop().
63 | Pass the parameter 'once' to so that this methodis only run once.
64 | Example:
65 | ProFi:start( 'once' )
66 | ]]
67 | function ProFi:start( param )
68 | if param == 'once' then
69 | if self:shouldReturn() then
70 | return
71 | else
72 | self.should_run_once = true
73 | end
74 | end
75 | self.has_started = true
76 | self.has_finished = false
77 | self:resetReports( self.reports )
78 | self:startHooks()
79 | self.startTime = getTime()
80 | end
81 |
82 | --[[
83 | Stops profiling.
84 | ]]
85 | function ProFi:stop()
86 | if self:shouldReturn() then
87 | return
88 | end
89 | self.stopTime = getTime()
90 | self:stopHooks()
91 | self.has_finished = true
92 | end
93 |
94 | function ProFi:checkMemory( interval, note )
95 | local time = getTime()
96 | local interval = interval or 0
97 | if self.lastCheckMemoryTime and time < self.lastCheckMemoryTime + interval then
98 | return
99 | end
100 | self.lastCheckMemoryTime = time
101 | local memoryReport = {
102 | ['time'] = time;
103 | ['memory'] = collectgarbage('count');
104 | ['note'] = note or '';
105 | }
106 | table.insert( self.memoryReports, memoryReport )
107 | self:setHighestMemoryReport( memoryReport )
108 | self:setLowestMemoryReport( memoryReport )
109 | end
110 |
111 | --[[
112 | Writes the profile report to a file.
113 | Param: [filename:string:optional] defaults to 'ProFi.txt' if not specified.
114 | ]]
115 | function ProFi:writeReport( filename )
116 | if #self.reports > 0 or #self.memoryReports > 0 then
117 | filename = filename or 'ProFi.txt'
118 | self:sortReportsWithSortMethod( self.reports, self.sortMethod )
119 | self:writeReportsToFilename( filename )
120 | print( string.format("[ProFi]\t Report written to %s", filename) )
121 | end
122 | end
123 |
124 | --[[
125 | Resets any profile information stored.
126 | ]]
127 | function ProFi:reset()
128 | self.reports = {}
129 | self.reportsByTitle = {}
130 | self.memoryReports = {}
131 | self.highestMemoryReport = nil
132 | self.lowestMemoryReport = nil
133 | self.has_started = false
134 | self.has_finished = false
135 | self.should_run_once = false
136 | self.lastCheckMemoryTime = nil
137 | self.hookCount = self.hookCount or DEFAULT_DEBUG_HOOK_COUNT
138 | self.sortMethod = self.sortMethod or sortByDurationDesc
139 | self.inspect = nil
140 | end
141 |
142 | --[[
143 | Set how often a hook is called.
144 | See http://pgl.yoyo.org/luai/i/debug.sethook for information.
145 | Param: [hookCount:number] if 0 ProFi counts every time a function is called.
146 | if 2 ProFi counts every other 2 function calls.
147 | ]]
148 | function ProFi:setHookCount( hookCount )
149 | self.hookCount = hookCount
150 | end
151 |
152 | --[[
153 | Set how the report is sorted when written to file.
154 | Param: [sortType:string] either 'duration' or 'count'.
155 | 'duration' sorts by the time a method took to run.
156 | 'count' sorts by the number of times a method was called.
157 | ]]
158 | function ProFi:setSortMethod( sortType )
159 | if sortType == 'duration' then
160 | self.sortMethod = sortByDurationDesc
161 | elseif sortType == 'count' then
162 | self.sortMethod = sortByCallCount
163 | end
164 | end
165 |
166 | --[[
167 | By default the getTime method is os.clock (CPU time),
168 | If you wish to use other time methods pass it to this function.
169 | Param: [getTimeMethod:function]
170 | ]]
171 | function ProFi:setGetTimeMethod( getTimeMethod )
172 | getTime = getTimeMethod
173 | end
174 |
175 | --[[
176 | Allows you to inspect a specific method.
177 | Will write to the report a list of methods that
178 | call this method you're inspecting, you can optionally
179 | provide a levels parameter to traceback a number of levels.
180 | Params: [methodName:string] the name of the method you wish to inspect.
181 | [levels:number:optional] the amount of levels you wish to traceback, defaults to 1.
182 | ]]
183 | function ProFi:setInspect( methodName, levels )
184 | if self.inspect then
185 | self.inspect.methodName = methodName
186 | self.inspect.levels = levels or 1
187 | else
188 | self.inspect = {
189 | ['methodName'] = methodName;
190 | ['levels'] = levels or 1;
191 | }
192 | end
193 | end
194 |
195 | -----------------------
196 | -- Implementations methods:
197 | -----------------------
198 |
199 | function ProFi:shouldReturn( )
200 | return self.should_run_once and self.has_finished
201 | end
202 |
203 | function ProFi:getFuncReport( funcInfo )
204 | local title = self:getTitleFromFuncInfo( funcInfo )
205 | local funcReport = self.reportsByTitle[ title ]
206 | if not funcReport then
207 | funcReport = self:createFuncReport( funcInfo )
208 | self.reportsByTitle[ title ] = funcReport
209 | table.insert( self.reports, funcReport )
210 | end
211 | return funcReport
212 | end
213 |
214 | function ProFi:getTitleFromFuncInfo( funcInfo )
215 | local name = funcInfo.name or 'anonymous'
216 | local source = funcInfo.short_src or 'C_FUNC'
217 | local linedefined = funcInfo.linedefined or 0
218 | linedefined = string.format( FORMAT_LINENUM, linedefined )
219 | return string.format(FORMAT_TITLE, source, name, linedefined)
220 | end
221 |
222 | function ProFi:createFuncReport( funcInfo )
223 | local name = funcInfo.name or 'anonymous'
224 | local source = funcInfo.source or 'C Func'
225 | local linedefined = funcInfo.linedefined or 0
226 | local funcReport = {
227 | ['title'] = self:getTitleFromFuncInfo( funcInfo );
228 | ['count'] = 0;
229 | ['timer'] = 0;
230 | }
231 | return funcReport
232 | end
233 |
234 | function ProFi:startHooks()
235 | debug.sethook( onDebugHook, 'cr', self.hookCount )
236 | end
237 |
238 | function ProFi:stopHooks()
239 | debug.sethook()
240 | end
241 |
242 | function ProFi:sortReportsWithSortMethod( reports, sortMethod )
243 | if reports then
244 | table.sort( reports, sortMethod )
245 | end
246 | end
247 |
248 | function ProFi:writeReportsToFilename( filename )
249 | local file, err = io.open( filename, 'w' )
250 | assert( file, err )
251 | self:writeBannerToFile( file )
252 | if #self.reports > 0 then
253 | self:writeProfilingReportsToFile( self.reports, file )
254 | end
255 | if #self.memoryReports > 0 then
256 | self:writeMemoryReportsToFile( self.memoryReports, file )
257 | end
258 | file:close()
259 | end
260 |
261 | function ProFi:writeProfilingReportsToFile( reports, file )
262 | local totalTime = self.stopTime - self.startTime
263 | local totalTimeOutput = string.format(FORMAT_TOTALTIME_LINE, totalTime)
264 | file:write( totalTimeOutput )
265 | local header = string.format( FORMAT_HEADER_LINE, "FILE", "FUNCTION", "LINE", "TIME", "RELATIVE", "CALLED" )
266 | file:write( header )
267 | for i, funcReport in ipairs( reports ) do
268 | local timer = string.format(FORMAT_TIME, funcReport.timer)
269 | local count = string.format(FORMAT_COUNT, funcReport.count)
270 | local relTime = string.format(FORMAT_RELATIVE, (funcReport.timer / totalTime) * 100 )
271 | local outputLine = string.format(FORMAT_OUTPUT_LINE, funcReport.title, timer, relTime, count )
272 | file:write( outputLine )
273 | if funcReport.inspections then
274 | self:writeInpsectionsToFile( funcReport.inspections, file )
275 | end
276 | end
277 | end
278 |
279 | function ProFi:writeMemoryReportsToFile( reports, file )
280 | file:write( FORMAT_MEMORY_HEADER1 )
281 | self:writeHighestMemoryReportToFile( file )
282 | self:writeLowestMemoryReportToFile( file )
283 | file:write( FORMAT_MEMORY_HEADER2 )
284 | for i, memoryReport in ipairs( reports ) do
285 | local outputLine = self:formatMemoryReportWithFormatter( memoryReport, FORMAT_MEMORY_LINE )
286 | file:write( outputLine )
287 | end
288 | end
289 |
290 | function ProFi:writeHighestMemoryReportToFile( file )
291 | local memoryReport = self.highestMemoryReport
292 | local outputLine = self:formatMemoryReportWithFormatter( memoryReport, FORMAT_HIGH_MEMORY_LINE )
293 | file:write( outputLine )
294 | end
295 |
296 | function ProFi:writeLowestMemoryReportToFile( file )
297 | local memoryReport = self.lowestMemoryReport
298 | local outputLine = self:formatMemoryReportWithFormatter( memoryReport, FORMAT_LOW_MEMORY_LINE )
299 | file:write( outputLine )
300 | end
301 |
302 | function ProFi:formatMemoryReportWithFormatter( memoryReport, formatter )
303 | local time = string.format(FORMAT_TIME, memoryReport.time)
304 | local kbytes = string.format(FORMAT_KBYTES, memoryReport.memory)
305 | local mbytes = string.format(FORMAT_MBYTES, memoryReport.memory/1024)
306 | local outputLine = string.format(formatter, time, kbytes, mbytes, memoryReport.note)
307 | return outputLine
308 | end
309 |
310 | function ProFi:writeBannerToFile( file )
311 | local banner = string.format(FORMAT_BANNER, os.date())
312 | file:write( banner )
313 | end
314 |
315 | function ProFi:writeInpsectionsToFile( inspections, file )
316 | local inspectionsList = self:sortInspectionsIntoList( inspections )
317 | file:write('\n==^ INSPECT ^======================================================================================================== COUNT ===\n')
318 | for i, inspection in ipairs( inspectionsList ) do
319 | local line = string.format(FORMAT_LINENUM, inspection.line)
320 | local title = string.format(FORMAT_TITLE, inspection.source, inspection.name, line)
321 | local count = string.format(FORMAT_COUNT, inspection.count)
322 | local outputLine = string.format(FORMAT_INSPECTION_LINE, title, count )
323 | file:write( outputLine )
324 | end
325 | file:write('===============================================================================================================================\n\n')
326 | end
327 |
328 | function ProFi:sortInspectionsIntoList( inspections )
329 | local inspectionsList = {}
330 | for k, inspection in pairs(inspections) do
331 | inspectionsList[#inspectionsList+1] = inspection
332 | end
333 | table.sort( inspectionsList, sortByCallCount )
334 | return inspectionsList
335 | end
336 |
337 | function ProFi:resetReports( reports )
338 | for i, report in ipairs( reports ) do
339 | report.timer = 0
340 | report.count = 0
341 | report.inspections = nil
342 | end
343 | end
344 |
345 | function ProFi:shouldInspect( funcInfo )
346 | return self.inspect and self.inspect.methodName == funcInfo.name
347 | end
348 |
349 | function ProFi:getInspectionsFromReport( funcReport )
350 | local inspections = funcReport.inspections
351 | if not inspections then
352 | inspections = {}
353 | funcReport.inspections = inspections
354 | end
355 | return inspections
356 | end
357 |
358 | function ProFi:getInspectionWithKeyFromInspections( key, inspections )
359 | local inspection = inspections[key]
360 | if not inspection then
361 | inspection = {
362 | ['count'] = 0;
363 | }
364 | inspections[key] = inspection
365 | end
366 | return inspection
367 | end
368 |
369 | function ProFi:doInspection( inspect, funcReport )
370 | local inspections = self:getInspectionsFromReport( funcReport )
371 | local levels = 5 + inspect.levels
372 | local currentLevel = 5
373 | while currentLevel < levels do
374 | local funcInfo = debug.getinfo( currentLevel, 'nS' )
375 | if funcInfo then
376 | local source = funcInfo.short_src or '[C]'
377 | local name = funcInfo.name or 'anonymous'
378 | local line = funcInfo.linedefined
379 | local key = source..name..line
380 | local inspection = self:getInspectionWithKeyFromInspections( key, inspections )
381 | inspection.source = source
382 | inspection.name = name
383 | inspection.line = line
384 | inspection.count = inspection.count + 1
385 | currentLevel = currentLevel + 1
386 | else
387 | break
388 | end
389 | end
390 | end
391 |
392 | function ProFi:onFunctionCall( funcInfo )
393 | local funcReport = ProFi:getFuncReport( funcInfo )
394 | funcReport.callTime = getTime()
395 | funcReport.count = funcReport.count + 1
396 | if self:shouldInspect( funcInfo ) then
397 | self:doInspection( self.inspect, funcReport )
398 | end
399 | end
400 |
401 | function ProFi:onFunctionReturn( funcInfo )
402 | local funcReport = ProFi:getFuncReport( funcInfo )
403 | if funcReport.callTime then
404 | funcReport.timer = funcReport.timer + (getTime() - funcReport.callTime)
405 | end
406 | end
407 |
408 | function ProFi:setHighestMemoryReport( memoryReport )
409 | if not self.highestMemoryReport then
410 | self.highestMemoryReport = memoryReport
411 | else
412 | if memoryReport.memory > self.highestMemoryReport.memory then
413 | self.highestMemoryReport = memoryReport
414 | end
415 | end
416 | end
417 |
418 | function ProFi:setLowestMemoryReport( memoryReport )
419 | if not self.lowestMemoryReport then
420 | self.lowestMemoryReport = memoryReport
421 | else
422 | if memoryReport.memory < self.lowestMemoryReport.memory then
423 | self.lowestMemoryReport = memoryReport
424 | end
425 | end
426 | end
427 |
428 | -----------------------
429 | -- Local Functions:
430 | -----------------------
431 |
432 | getTime = os.clock
433 |
434 | onDebugHook = function( hookType )
435 | local funcInfo = debug.getinfo( 2, 'nS' )
436 | if hookType == "call" then
437 | ProFi:onFunctionCall( funcInfo )
438 | elseif hookType == "return" then
439 | ProFi:onFunctionReturn( funcInfo )
440 | end
441 | end
442 |
443 | sortByDurationDesc = function( a, b )
444 | return a.timer > b.timer
445 | end
446 |
447 | sortByCallCount = function( a, b )
448 | return a.count > b.count
449 | end
450 |
451 | -----------------------
452 | -- Return Module:
453 | -----------------------
454 |
455 | ProFi:reset()
456 | return ProFi
--------------------------------------------------------------------------------
/dictionaries/basic_english.txt:
--------------------------------------------------------------------------------
1 | able
2 | about
3 | absence
4 | absorption
5 | acceleration
6 | acceptance
7 | accessory
8 | accident
9 | account
10 | acid
11 | across
12 | act
13 | acting
14 | active
15 | actor
16 | addition
17 | address
18 | adjacent
19 | adjustment
20 | adventure
21 | advertisement
22 | advice
23 | after
24 | afterthought
25 | again
26 | against
27 | age
28 | agency
29 | agent
30 | ago
31 | agreement
32 | air
33 | airplane
34 | alcohol
35 | algebra
36 | all
37 | allowance
38 | almost
39 | along
40 | also
41 | alternative
42 | aluminium
43 | always
44 | ambition
45 | ammonia
46 | among
47 | amount
48 | amplitude
49 | amusement
50 | anchor
51 | and
52 | anesthetic
53 | angle
54 | angry
55 | animal
56 | ankle
57 | another
58 | answer
59 | ant
60 | any
61 | anybody
62 | anyhow
63 | anyone
64 | anything
65 | anywhere
66 | apparatus
67 | appendage
68 | apple
69 | application
70 | approval
71 | approximation
72 | april
73 | arbitrary
74 | arbitration
75 | arc
76 | arch
77 | area
78 | argument
79 | arithmetic
80 | arm
81 | army
82 | arrangement
83 | art
84 | as
85 | asbestos
86 | ash
87 | asset
88 | assistant
89 | at
90 | attack
91 | attempt
92 | attention
93 | attraction
94 | august
95 | authority
96 | autobus
97 | automatic
98 | automobile
99 | average
100 | awake
101 | awkward
102 | axis
103 | baby
104 | back
105 | backbone
106 | backwoods
107 | bad
108 | bag
109 | balance
110 | balcony
111 | bale
112 | ball
113 | ballet
114 | band
115 | bang
116 | bank
117 | bankrupt
118 | bar
119 | bark
120 | barrel
121 | base
122 | based
123 | basin
124 | basing
125 | basket
126 | bath
127 | be
128 | beak
129 | beaker
130 | beard
131 | beat
132 | beautiful
133 | because
134 | become
135 | bed
136 | bedroom
137 | bee
138 | beef
139 | beer
140 | beeswax
141 | before
142 | behavior
143 | behind
144 | belief
145 | bell
146 | belt
147 | bent
148 | berry
149 | bet
150 | between
151 | bill
152 | biology
153 | bird
154 | birefringence
155 | birth
156 | birthday
157 | birthright
158 | bit
159 | bite
160 | bitter
161 | black
162 | blackberry
163 | blackbird
164 | blackboard
165 | blade
166 | blame
167 | blanket
168 | blood
169 | bloodvessel
170 | blow
171 | blue
172 | bluebell
173 | board
174 | boat
175 | body
176 | boiling
177 | bomb
178 | bone
179 | book
180 | bookkeeper
181 | boot
182 | both
183 | bottle
184 | bottom
185 | box
186 | boy
187 | brain
188 | brake
189 | branch
190 | brass
191 | brave
192 | bread
193 | break
194 | breakfast
195 | breast
196 | breath
197 | brick
198 | bridge
199 | bright
200 | broken
201 | broker
202 | brother
203 | brown
204 | brush
205 | brushwood
206 | bubble
207 | bucket
208 | bud
209 | budget
210 | builder
211 | building
212 | bulb
213 | bunch
214 | buoyancy
215 | burial
216 | burn
217 | burned
218 | burner
219 | burning
220 | burst
221 | business
222 | busy
223 | but
224 | butter
225 | buttercup
226 | button
227 | by
228 | cafe
229 | cake
230 | calculation
231 | calendar
232 | call
233 | camera
234 | canvas
235 | capacity
236 | capital
237 | card
238 | cardboard
239 | care
240 | carefree
241 | caretaker
242 | carpet
243 | carriage
244 | cart
245 | carter
246 | cartilage
247 | case
248 | cast
249 | cat
250 | catarrh
251 | category
252 | cause
253 | cave
254 | cavity
255 | cell
256 | centi-
257 | ceremony
258 | certain
259 | certificate
260 | chain
261 | chair
262 | chalk
263 | champagne
264 | chance
265 | change
266 | character
267 | charge
268 | chauffeur
269 | cheap
270 | check
271 | cheese
272 | chemical
273 | chemist
274 | chemistry
275 | chest
276 | chief
277 | child
278 | chimney
279 | chin
280 | china
281 | chocolate
282 | choice
283 | chorus
284 | church
285 | cigarette
286 | circle
287 | circuit
288 | circulation
289 | circumference
290 | circus
291 | citron
292 | civilization
293 | claim
294 | claw
295 | clay
296 | clean
297 | clear
298 | cleavage
299 | clever
300 | client
301 | climber
302 | clip
303 | clock
304 | clockwork
305 | cloth
306 | clothier
307 | clothing
308 | cloud
309 | club
310 | coal
311 | coat
312 | cocktail
313 | code
314 | coffee
315 | cognac
316 | coil
317 | cold
318 | collar
319 | collection
320 | college
321 | collision
322 | colony
323 | color
324 | column
325 | comb
326 | combination
327 | combine
328 | come
329 | comfort
330 | committee
331 | common
332 | commonsense
333 | communications
334 | company
335 | comparison
336 | competition
337 | complaint
338 | complete
339 | complex
340 | component
341 | compound
342 | concept
343 | concrete
344 | condition
345 | conductor
346 | congruent
347 | connection
348 | conscious
349 | conservation
350 | consignment
351 | constant
352 | consumer
353 | continuous
354 | contour
355 | control
356 | convenient
357 | conversion
358 | cook
359 | cooked
360 | cooker
361 | cooking
362 | cool
363 | copper
364 | copy
365 | copyright
366 | cord
367 | cork
368 | corner
369 | correlation
370 | corrosion
371 | cost
372 | cotton
373 | cough
374 | country
375 | court
376 | cover
377 | cow
378 | crack
379 | credit
380 | creeper
381 | crime
382 | crop
383 | cross
384 | cruel
385 | crush
386 | cry
387 | crying
388 | cunning
389 | cup
390 | cupboard
391 | current
392 | curtain
393 | curve
394 | cushion
395 | cusp
396 | customs
397 | cut
398 | damage
399 | damping
400 | dance
401 | dancer
402 | dancing
403 | danger
404 | dark
405 | date
406 | daughter
407 | day
408 | daylight
409 | dead
410 | dear
411 | death
412 | debit
413 | debt
414 | december
415 | deci-
416 | decision
417 | deck
418 | decrease
419 | deep
420 | defect
421 | deficiency
422 | deflation
423 | degenerate
424 | degree
425 | degree
426 | delicate
427 | delivery
428 | demand
429 | denominator
430 | density
431 | department
432 | dependent
433 | deposit
434 | desert
435 | design
436 | designer
437 | desire
438 | destruction
439 | detail
440 | determining
441 | development
442 | dew
443 | diameter
444 | difference
445 | different
446 | difficulty
447 | digestion
448 | dike
449 | dilution
450 | dinner
451 | dip
452 | direct
453 | direction
454 | dirty
455 | disappearance
456 | discharge
457 | disclaimers
458 | discount
459 | discovery
460 | discussion
461 | disease
462 | disgrace
463 | disgust
464 | dislike
465 | dissipation
466 | distance
467 | distribution
468 | disturbance
469 | ditch
470 | dive
471 | division
472 | divisor
473 | divorce
474 | do
475 | dog
476 | doll
477 | domesticating
478 | dominion
479 | door
480 | doubt
481 | down
482 | downfall
483 | drain
484 | drawer
485 | dreadful
486 | dream
487 | dress
488 | dressing
489 | drift
490 | drink
491 | driver
492 | driving
493 | drop
494 | dropped
495 | dropper
496 | dry
497 | duct
498 | dull
499 | dust
500 | duster
501 | duty
502 | dynamite
503 | each
504 | ear
505 | early
506 | earring
507 | earth
508 | earthwork
509 | east
510 | easy
511 | economy
512 | edge
513 | education
514 | effect
515 | efficiency
516 | effort
517 | egg
518 | eight
519 | either
520 | elastic
521 | electric
522 | electricity
523 | eleven
524 | elimination
525 | embassy
526 | empire
527 | employer
528 | empty
529 | encyclopedia
530 | end
531 | enemy
532 | engine
533 | engineer
534 | enough
535 | envelope
536 | environment
537 | envy
538 | equal
539 | equation
540 | erosion
541 | error
542 | eruption
543 | evaporation
544 | even
545 | evening
546 | event
547 | ever
548 | evergreen
549 | every
550 | everybody
551 | everyday
552 | everyone
553 | everything
554 | everywhere
555 | exact
556 | example
557 | exchange
558 | excitement
559 | exercise
560 | existence
561 | expansion
562 | experience
563 | experiment
564 | expert
565 | explanation
566 | explosion
567 | export
568 | expression
569 | extinction
570 | eye
571 | eyeball
572 | eyebrow
573 | eyelash
574 | face
575 | fact
576 | factor
577 | failure
578 | fair
579 | fall
580 | false
581 | family
582 | famous
583 | fan
584 | far
585 | farm
586 | farmer
587 | fastening
588 | fat
589 | father
590 | fatherland
591 | fault
592 | fear
593 | feather
594 | february
595 | feeble
596 | feeling
597 | female
598 | ferment
599 | fertile
600 | fertilizing
601 | fever
602 | fiber
603 | fiction
604 | field
605 | fifteen
606 | fifth
607 | fifty
608 | fight
609 | figure
610 | fin
611 | financial
612 | finger
613 | fingerprint
614 | fire
615 | fire-engine
616 | firearm
617 | fired
618 | firefly
619 | fireman
620 | fireplace
621 | firework
622 | firing
623 | first
624 | first-rate
625 | fish
626 | fisher
627 | fisherman
628 | five
629 | fixed
630 | flag
631 | flame
632 | flash
633 | flask
634 | flat
635 | flesh
636 | flight
637 | flint
638 | flood
639 | flood
640 | floor
641 | flour
642 | flow
643 | flower
644 | fly
645 | focus
646 | fold
647 | folder
648 | foliation
649 | food
650 | foolish
651 | foot
652 | football
653 | footlights
654 | footman
655 | footnote
656 | footprint
657 | footstep
658 | for
659 | force
660 | forecast
661 | forehead
662 | foreign
663 | forgiveness
664 | fork
665 | form
666 | forty
667 | forward
668 | four
669 | fourteen
670 | fourth
671 | fowl
672 | fraction
673 | fracture
674 | frame
675 | free
676 | frequent
677 | fresh
678 | friction
679 | friday
680 | friend
681 | from
682 | front
683 | frost
684 | frozen
685 | fruit
686 | full
687 | fume
688 | funnel
689 | funny
690 | fur
691 | furnace
692 | furniture
693 | fusion
694 | future
695 | garden
696 | gardener
697 | gas
698 | gasworks
699 | gate
700 | general
701 | generation
702 | geography
703 | geology
704 | geometry
705 | germ
706 | germinating
707 | get
708 | gill
709 | girl
710 | give
711 | glacier
712 | gland
713 | glass
714 | glossary
715 | glove
716 | glycerin
717 | go
718 | goat
719 | god
720 | gold
721 | goldfish
722 | good
723 | good-morning
724 | goodlooking
725 | goodnight
726 | government
727 | grain
728 | gram
729 | grand
730 | grass
731 | grateful
732 | grating
733 | gravel
734 | gray
735 | grease
736 | great
737 | green
738 | grey
739 | grief
740 | grip
741 | grocery
742 | groove
743 | gross
744 | ground
745 | group
746 | growth
747 | guarantee
748 | guard
749 | guess
750 | guide
751 | gum
752 | gun
753 | gun-carriage
754 | gunboat
755 | gunmetal
756 | gunpowder
757 | habit
758 | hair
759 | half
760 | hammer
761 | hand
762 | handbook
763 | handkerchief
764 | handle
765 | handwriting
766 | hanger
767 | hanging
768 | happy
769 | harbor
770 | hard
771 | harmony
772 | hat
773 | hate
774 | have
775 | he
776 | head
777 | headdress
778 | headland
779 | headstone
780 | headway
781 | healthy
782 | hearing
783 | heart
784 | heat
785 | heated
786 | heater
787 | heating
788 | heavy
789 | hedge
790 | help
791 | help
792 | here
793 | hereafter
794 | herewith
795 | high
796 | highlands
797 | highway
798 | hill
799 | himself
800 | hinge
801 | hire
802 | hiss
803 | history
804 | hold
805 | hole
806 | holiday
807 | hollow
808 | home
809 | honest
810 | honey
811 | hoof
812 | hook
813 | hope
814 | horn
815 | horse
816 | horseplay
817 | horsepower
818 | hospital
819 | host
820 | hotel
821 | hour
822 | hourglass
823 | house
824 | houseboat
825 | housekeeper
826 | how
827 | however
828 | human
829 | humor
830 | hundred
831 | hunt
832 | hurry
833 | hurt
834 | husband
835 | hyena
836 | hygiene
837 | hysteria
838 | ice
839 | idea
840 | if
841 | igneous
842 | ill
843 | image
844 | imagination
845 | imperial
846 | import
847 | important
848 | impulse
849 | impurity
850 | in
851 | inasmuch
852 | inclusion
853 | income
854 | increase
855 | index
856 | individual
857 | indoors
858 | industry
859 | inferno
860 | infinity
861 | inflation
862 | influenza
863 | inheritance
864 | ink
865 | inland
866 | inlet
867 | inner
868 | innocent
869 | input
870 | insect
871 | inside
872 | instep
873 | institution
874 | instrument
875 | insulator
876 | insurance
877 | integer
878 | intelligent
879 | intercept
880 | interest
881 | international
882 | interpretation
883 | intersection
884 | into
885 | intrusion
886 | invention
887 | inverse
888 | investigation
889 | investment
890 | invitation
891 | iron
892 | island
893 | itself
894 | jam
895 | january
896 | jaw
897 | jazz
898 | jealous
899 | jelly
900 | jerk
901 | jewel
902 | jeweler
903 | join
904 | joiner
905 | joint
906 | journey
907 | judge
908 | jug
909 | juice
910 | july
911 | jump
912 | june
913 | jury
914 | justice
915 | keep
916 | keeper
917 | kennel
918 | kettle
919 | key
920 | kick
921 | kidney
922 | kilo-
923 | kind
924 | king
925 | kiss
926 | kitchen
927 | knee
928 | knife
929 | knock
930 | knot
931 | knowledge
932 | lace
933 | lag
934 | lake
935 | lame
936 | lamp
937 | land
938 | landmark
939 | landslip
940 | language
941 | large
942 | last
943 | late
944 | latitude
945 | latitude
946 | laugh
947 | laughing
948 | lava
949 | law
950 | lawyer
951 | layer
952 | lazy
953 | lead
954 | leaf
955 | learner
956 | learning
957 | least
958 | leather
959 | lecture
960 | left
961 | leg
962 | legal
963 | length
964 | lens
965 | less
966 | lesson
967 | let
968 | letter
969 | level
970 | lever
971 | lever
972 | liability
973 | library
974 | license
975 | lid
976 | life
977 | lift
978 | light
979 | lighthouse
980 | like
981 | lime
982 | limestone
983 | limit
984 | line
985 | linen
986 | link
987 | lip
988 | liqueur
989 | liquid
990 | list
991 | liter
992 | little
993 | liver
994 | living
995 | load
996 | loan
997 | local
998 | lock
999 | locker
1000 | locking
1001 | locus
1002 | long
1003 | longitude
1004 | longitude
1005 | look
1006 | looking-glass
1007 | loose
1008 | loss
1009 | loud
1010 | love
1011 | low
1012 | luck
1013 | lump
1014 | lunch
1015 | lung
1016 | macaroni
1017 | machine
1018 | madam
1019 | magic
1020 | magnetic
1021 | magnitude
1022 | make
1023 | malaria
1024 | male
1025 | man
1026 | manager
1027 | manhole
1028 | mania
1029 | manner
1030 | many
1031 | map
1032 | marble
1033 | march
1034 | margin
1035 | mark
1036 | marked
1037 | market
1038 | marriage
1039 | married
1040 | mass
1041 | mast
1042 | match
1043 | material
1044 | mathematics
1045 | mattress
1046 | mature
1047 | may
1048 | may
1049 | meal
1050 | mean
1051 | meaning
1052 | measure
1053 | meat
1054 | medical
1055 | medicine
1056 | medium
1057 | meeting
1058 | melt
1059 | member
1060 | memory
1061 | meow
1062 | mess
1063 | message
1064 | metabolism
1065 | metal
1066 | meter
1067 | micro-
1068 | microscope
1069 | middle
1070 | military
1071 | milk
1072 | mill
1073 | milli-
1074 | million
1075 | mind
1076 | mine
1077 | miner
1078 | mineral
1079 | minute
1080 | minute
1081 | mist
1082 | mixed
1083 | mixture
1084 | model
1085 | modern
1086 | modest
1087 | momentum
1088 | monday
1089 | money
1090 | monkey
1091 | monopoly
1092 | month
1093 | mood
1094 | moon
1095 | moral
1096 | more
1097 | morning
1098 | most
1099 | mother
1100 | motion
1101 | mountain
1102 | moustache
1103 | mouth
1104 | move
1105 | much
1106 | mud
1107 | multiple
1108 | multiplication
1109 | murder
1110 | muscle
1111 | museum
1112 | music
1113 | myself
1114 | nail
1115 | name
1116 | narrow
1117 | nasty
1118 | nation
1119 | natural
1120 | nature
1121 | navy
1122 | near
1123 | nearer
1124 | neat
1125 | necessary
1126 | neck
1127 | need
1128 | needle
1129 | neglect
1130 | neighbor
1131 | nerve
1132 | nest
1133 | net
1134 | network
1135 | neutron
1136 | new
1137 | news
1138 | newspaper
1139 | next
1140 | nice
1141 | nickel
1142 | nicotine
1143 | night
1144 | nine
1145 | no
1146 | nobody
1147 | node
1148 | noise
1149 | normal
1150 | north
1151 | nose
1152 | nostril
1153 | not
1154 | note
1155 | noted
1156 | nothing
1157 | november
1158 | now
1159 | nowhere
1160 | nucleus
1161 | number
1162 | numerator
1163 | nurse
1164 | nut
1165 | obedient
1166 | observation
1167 | october
1168 | of
1169 | off
1170 | offer
1171 | office
1172 | officer
1173 | offspring
1174 | oil
1175 | old
1176 | olive
1177 | omelet
1178 | on
1179 | once
1180 | oncoming
1181 | one
1182 | oneself
1183 | onlooker
1184 | only
1185 | onto
1186 | open
1187 | opera
1188 | operation
1189 | opinion
1190 | opium
1191 | opposite
1192 | or
1193 | orange
1194 | orchestra
1195 | orchestra
1196 | order
1197 | ore
1198 | organ
1199 | organism
1200 | organization
1201 | origin
1202 | ornament
1203 | other
1204 | out
1205 | outburst
1206 | outcome
1207 | outcrop
1208 | outcry
1209 | outdoor
1210 | outer
1211 | outgoing
1212 | outhouse
1213 | outlaw
1214 | outlet
1215 | outlier
1216 | outline
1217 | outlook
1218 | output
1219 | outside
1220 | outskirts
1221 | outstretched
1222 | oval
1223 | oven
1224 | over
1225 | overacting
1226 | overall
1227 | overbalancing
1228 | overbearing
1229 | overcoat
1230 | overcome
1231 | overdo
1232 | overdressed
1233 | overfull
1234 | overhanging
1235 | overhead
1236 | overland
1237 | overlap
1238 | overleaf
1239 | overloud
1240 | overseas
1241 | overseer
1242 | overshoe
1243 | overstatement
1244 | overtake
1245 | overtaxed
1246 | overtime
1247 | overturned
1248 | overuse
1249 | overvalued
1250 | overweight
1251 | overworking
1252 | own
1253 | owner
1254 | oxidation
1255 | packing
1256 | pad
1257 | page
1258 | pain
1259 | paint
1260 | painter
1261 | painting
1262 | pair
1263 | pajamas
1264 | pan
1265 | paper
1266 | paradise
1267 | paraffin
1268 | paragraph
1269 | parallel
1270 | parcel
1271 | parent
1272 | park
1273 | part
1274 | particle
1275 | parting
1276 | partner
1277 | party
1278 | passage
1279 | passport
1280 | past
1281 | paste
1282 | patent
1283 | path
1284 | patience
1285 | payment
1286 | peace
1287 | pedal
1288 | pen
1289 | pencil
1290 | pendulum
1291 | penguin
1292 | pension
1293 | people
1294 | perfect
1295 | person
1296 | petal
1297 | petroleum
1298 | phonograph
1299 | physical
1300 | physics
1301 | physiology
1302 | piano
1303 | picture
1304 | pig
1305 | pin
1306 | pincushion
1307 | pipe
1308 | piston
1309 | place
1310 | plain
1311 | plan
1312 | plane
1313 | plant
1314 | plaster
1315 | plate
1316 | platinum
1317 | play
1318 | played
1319 | playing
1320 | plaything
1321 | please
1322 | pleased
1323 | pleasure
1324 | plough
1325 | plow
1326 | plug
1327 | pocket
1328 | poetry
1329 | point
1330 | pointer
1331 | pointing
1332 | poison
1333 | police
1334 | policeman
1335 | polish
1336 | political
1337 | pollen
1338 | pool
1339 | poor
1340 | population
1341 | porcelain
1342 | porter
1343 | position
1344 | possible
1345 | post
1346 | postman
1347 | postmark
1348 | postmaster
1349 | postoffice
1350 | pot
1351 | potash
1352 | potato
1353 | potter
1354 | powder
1355 | power
1356 | practice
1357 | praise
1358 | prayer
1359 | present
1360 | president
1361 | pressure
1362 | price
1363 | prick
1364 | priest
1365 | prime
1366 | prince
1367 | princess
1368 | print
1369 | printer
1370 | prison
1371 | prisoner
1372 | private
1373 | probability
1374 | probable
1375 | process
1376 | produce
1377 | producer
1378 | product
1379 | profit
1380 | program
1381 | progress
1382 | projectile
1383 | projection
1384 | promise
1385 | proof
1386 | propaganda
1387 | property
1388 | prose
1389 | protest
1390 | proud
1391 | psychology
1392 | public
1393 | pull
1394 | pulley
1395 | pump
1396 | punishment
1397 | pupil
1398 | purchase
1399 | pure
1400 | purpose
1401 | purr
1402 | push
1403 | put
1404 | pyramid
1405 | quack
1406 | quality
1407 | quantity
1408 | quarter
1409 | queen
1410 | question
1411 | quick
1412 | quiet
1413 | quinine
1414 | quite
1415 | quotient
1416 | race
1417 | radiation
1418 | radio
1419 | radium
1420 | rail
1421 | rain
1422 | raining
1423 | range
1424 | rat
1425 | rate
1426 | ratio
1427 | ray
1428 | reaction
1429 | reader
1430 | reading
1431 | reading
1432 | ready
1433 | reagent
1434 | real
1435 | reason
1436 | receipt
1437 | receiver
1438 | reciprocal
1439 | record
1440 | rectangle
1441 | recurring
1442 | red
1443 | reference
1444 | referendum
1445 | reflux
1446 | regret
1447 | regular
1448 | reinforcement
1449 | relation
1450 | relative
1451 | religion
1452 | remark
1453 | remedy
1454 | rent
1455 | repair
1456 | representative
1457 | reproduction
1458 | repulsion
1459 | request
1460 | residue
1461 | resistance
1462 | resolution
1463 | respect
1464 | responsible
1465 | rest
1466 | restaurant
1467 | result
1468 | retail
1469 | revenge
1470 | reversible
1471 | reward
1472 | rheumatism
1473 | rhythm
1474 | rice
1475 | rich
1476 | right
1477 | rigidity
1478 | ring
1479 | rise
1480 | rival
1481 | river
1482 | road
1483 | rock
1484 | rod
1485 | roll
1486 | roller
1487 | roof
1488 | room
1489 | root
1490 | rot
1491 | rotation
1492 | rough
1493 | round
1494 | royal
1495 | rub
1496 | rubber
1497 | rude
1498 | rule
1499 | ruler
1500 | rum
1501 | run
1502 | runaway
1503 | rust
1504 | sac
1505 | sad
1506 | safe
1507 | sail
1508 | sailor
1509 | salad
1510 | sale
1511 | salt
1512 | same
1513 | sample
1514 | sand
1515 | sardine
1516 | satisfaction
1517 | saturated
1518 | saturday
1519 | saucer
1520 | saving
1521 | say
1522 | scale
1523 | scale
1524 | scarp
1525 | schist
1526 | school
1527 | science
1528 | scissors
1529 | scratch
1530 | screen
1531 | screw
1532 | sea
1533 | seal
1534 | seaman
1535 | search
1536 | seat
1537 | second
1538 | second
1539 | secondhand
1540 | secret
1541 | secretary
1542 | secretion
1543 | section
1544 | security
1545 | sedimentary
1546 | see
1547 | seed
1548 | seem
1549 | selection
1550 | self
1551 | selfish
1552 | send
1553 | sense
1554 | sensitivity
1555 | sentence
1556 | sepal
1557 | separate
1558 | september
1559 | serious
1560 | serum
1561 | servant
1562 | service
1563 | set
1564 | seven
1565 | sex
1566 | shade
1567 | shadow
1568 | shake
1569 | shale
1570 | shame
1571 | share
1572 | sharp
1573 | shave
1574 | shear
1575 | sheep
1576 | sheet
1577 | shelf
1578 | shell
1579 | ship
1580 | shirt
1581 | shock
1582 | shocked
1583 | shocking
1584 | shoe
1585 | shore
1586 | short
1587 | shortcut
1588 | shorthand
1589 | shoulder
1590 | show
1591 | shut
1592 | side
1593 | sideboard
1594 | sidewalk
1595 | sight
1596 | sign
1597 | silk
1598 | sill
1599 | silver
1600 | similarity
1601 | simple
1602 | since
1603 | sir
1604 | sister
1605 | six
1606 | sixteen
1607 | size
1608 | skin
1609 | skirt
1610 | skull
1611 | sky
1612 | slate
1613 | sleep
1614 | sleeve
1615 | slide
1616 | slip
1617 | slope
1618 | slow
1619 | small
1620 | smash
1621 | smell
1622 | smile
1623 | smoke
1624 | smooth
1625 | snake
1626 | sneeze
1627 | snow
1628 | snowing
1629 | so
1630 | soap
1631 | social
1632 | society
1633 | sock
1634 | soft
1635 | soil
1636 | soldier
1637 | solid
1638 | solution
1639 | solvent
1640 | some
1641 | somebody
1642 | someday
1643 | somehow
1644 | someone
1645 | something
1646 | sometime
1647 | somewhat
1648 | somewhere
1649 | son
1650 | song
1651 | sorry
1652 | sort
1653 | sound
1654 | soup
1655 | south
1656 | space
1657 | spade
1658 | spark
1659 | special
1660 | specialization
1661 | specimen
1662 | speculation
1663 | spirit
1664 | spit
1665 | splash
1666 | sponge
1667 | spoon
1668 | sport
1669 | spot
1670 | spring
1671 | square
1672 | stable
1673 | stage
1674 | stain
1675 | stair
1676 | stalk
1677 | stamen
1678 | stamp
1679 | star
1680 | start
1681 | statement
1682 | station
1683 | statistics
1684 | steady
1685 | steam
1686 | steamer
1687 | steel
1688 | stem
1689 | step
1690 | stick
1691 | sticky
1692 | stiff
1693 | still
1694 | stimulus
1695 | stitch
1696 | stocking
1697 | stomach
1698 | stone
1699 | stop
1700 | stopper
1701 | stopping
1702 | store
1703 | storm
1704 | story
1705 | straight
1706 | strain
1707 | strange
1708 | straw
1709 | stream
1710 | street
1711 | strength
1712 | stress
1713 | stretch
1714 | stretcher
1715 | strike
1716 | string
1717 | strong
1718 | structure
1719 | study
1720 | subject
1721 | substance
1722 | substitution
1723 | subtraction
1724 | success
1725 | successive
1726 | such
1727 | suchlike
1728 | sucker
1729 | sudden
1730 | sugar
1731 | suggestion
1732 | sum
1733 | summer
1734 | sun
1735 | sunburn
1736 | sunday
1737 | sunlight
1738 | sunshade
1739 | supply
1740 | support
1741 | surface
1742 | surgeon
1743 | surprise
1744 | suspension
1745 | suspicious
1746 | sweet
1747 | sweetheart
1748 | swelling
1749 | swim
1750 | swing
1751 | switch
1752 | sympathetic
1753 | system
1754 | table
1755 | tail
1756 | tailor
1757 | take
1758 | talk
1759 | talk
1760 | talking
1761 | tall
1762 | tame
1763 | tap
1764 | tapioca
1765 | taste
1766 | tax
1767 | taxi
1768 | tea
1769 | teacher
1770 | teaching
1771 | tear
1772 | telegram
1773 | telephone
1774 | ten
1775 | tendency
1776 | tent
1777 | term
1778 | terrace
1779 | test
1780 | texture
1781 | than
1782 | that
1783 | the
1784 | theater
1785 | then
1786 | theory
1787 | there
1788 | thermometer
1789 | thick
1790 | thickness
1791 | thief
1792 | thimble
1793 | thin
1794 | thing
1795 | third
1796 | thirteen
1797 | thirty
1798 | this
1799 | thorax
1800 | though
1801 | thought
1802 | thousand
1803 | thread
1804 | threat
1805 | three
1806 | throat
1807 | through
1808 | thrust
1809 | thumb
1810 | thunder
1811 | thursday
1812 | ticket
1813 | tide
1814 | tie
1815 | tight
1816 | till
1817 | time
1818 | tin
1819 | tired
1820 | tissue
1821 | to
1822 | toast
1823 | tobacco
1824 | today
1825 | toe
1826 | together
1827 | tomorrow
1828 | tongs
1829 | tongue
1830 | tonight
1831 | too
1832 | tooth
1833 | top
1834 | torpedo
1835 | total
1836 | touch
1837 | touching
1838 | towel
1839 | tower
1840 | town
1841 | trade
1842 | trader
1843 | tradesman
1844 | traffic
1845 | tragedy
1846 | train
1847 | trainer
1848 | training
1849 | transmission
1850 | transparent
1851 | transport
1852 | trap
1853 | travel
1854 | tray
1855 | treatment
1856 | tree
1857 | triangle
1858 | trick
1859 | trouble
1860 | troubled
1861 | troubling
1862 | trousers
1863 | truck
1864 | true
1865 | tube
1866 | tuesday
1867 | tune
1868 | tunnel
1869 | turbine
1870 | turn
1871 | turning
1872 | twelve
1873 | twenty
1874 | twenty-one
1875 | twice
1876 | twin
1877 | twist
1878 | two
1879 | typist
1880 | ugly
1881 | umbrella
1882 | unconformity
1883 | under
1884 | underclothing
1885 | undercooked
1886 | undergo
1887 | undergrowth
1888 | undermined
1889 | undersigned
1890 | undersized
1891 | understanding
1892 | understatement
1893 | undertake
1894 | undervalued
1895 | undo
1896 | unit
1897 | universe
1898 | university
1899 | unknown
1900 | up
1901 | upkeep
1902 | uplift
1903 | upon
1904 | upright
1905 | uptake
1906 | use
1907 | used
1908 | valency
1909 | valley
1910 | value
1911 | valve
1912 | vanilla
1913 | vapor
1914 | variable
1915 | vascular
1916 | vegetable
1917 | velocity
1918 | verse
1919 | very
1920 | vessel
1921 | vestigial
1922 | victim
1923 | victory
1924 | view
1925 | viewpoint
1926 | violent
1927 | violin
1928 | visa
1929 | vitamin
1930 | vodka
1931 | voice
1932 | volt
1933 | volume
1934 | vortex
1935 | vote
1936 | waiter
1937 | waiting
1938 | waiting
1939 | walk
1940 | wall
1941 | war
1942 | warm
1943 | wash
1944 | waste
1945 | wasted
1946 | watch
1947 | water
1948 | waterfall
1949 | wave
1950 | wax
1951 | way
1952 | weak
1953 | weather
1954 | wedge
1955 | wednesday
1956 | week
1957 | weekend
1958 | weight
1959 | welcome
1960 | well
1961 | well-being
1962 | well-off
1963 | west
1964 | wet
1965 | what
1966 | whatever
1967 | wheel
1968 | when
1969 | whenever
1970 | where
1971 | whereas
1972 | whereby
1973 | wherever
1974 | whether
1975 | which
1976 | whichever
1977 | while
1978 | whip
1979 | whisky
1980 | whistle
1981 | white
1982 | whitewash
1983 | who
1984 | whoever
1985 | wholesale
1986 | why
1987 | wide
1988 | widow
1989 | wife
1990 | wild
1991 | will
1992 | wind
1993 | window
1994 | windpipe
1995 | wine
1996 | wing
1997 | winter
1998 | wire
1999 | wise
2000 | with
2001 | within
2002 | without
2003 | woman
2004 | wood
2005 | woodwork
2006 | wool
2007 | word
2008 | work
2009 | worker
2010 | workhouse
2011 | working
2012 | working
2013 | working
2014 | world
2015 | worm
2016 | wound
2017 | wreck
2018 | wrist
2019 | writer
2020 | writing
2021 | wrong
2022 | x-ray
2023 | yawn
2024 | year
2025 | yearbook
2026 | yellow
2027 | yes
2028 | yesterday
2029 | you
2030 | young
2031 | yourself
2032 | zebra
2033 | zinc
2034 | zookeeper
2035 | zoology
2036 |
--------------------------------------------------------------------------------
/test/lunatest.lua:
--------------------------------------------------------------------------------
1 | -----------------------------------------------------------------------
2 | --
3 | -- Copyright (c) 2009-12 Scott Vokes
4 | --
5 | -- Permission is hereby granted, free of charge, to any person
6 | -- obtaining a copy of this software and associated documentation
7 | -- files (the "Software"), to deal in the Software without
8 | -- restriction, including without limitation the rights to use,
9 | -- copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | -- copies of the Software, and to permit persons to whom the
11 | -- Software is furnished to do so, subject to the following
12 | -- conditions:
13 | --
14 | -- The above copyright notice and this permission notice shall be
15 | -- included in all copies or substantial portions of the Software.
16 | --
17 | -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18 | -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19 | -- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20 | -- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21 | -- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22 | -- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 | -- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24 | -- OTHER DEALINGS IN THE SOFTWARE.
25 | --
26 | ------------------------------------------------------------------------
27 | --
28 | -- This is a library for randomized testing with Lua.
29 | -- For usage and examples, see README and the test suite.
30 | --
31 | ------------------------------------------------------------------------
32 |
33 | ------------
34 | -- Module --
35 | ------------
36 |
37 | -- standard libraries used
38 | local debug, io, math, os, string, table =
39 | debug, io, math, os, string, table
40 |
41 | -- required core global functions
42 | local assert, error, ipairs, pairs, pcall, print, setmetatable, tonumber =
43 | assert, error, ipairs, pairs, pcall, print, setmetatable, tonumber
44 | local fmt, tostring, type, unpack = string.format, tostring, type, unpack
45 | local getmetatable, rawget, setmetatable, xpcall =
46 | getmetatable, rawget, setmetatable, xpcall
47 | local exit, next, require = os.exit, next, require
48 |
49 | -- Get containing env, Lua 5.1-style
50 | local getenv = getfenv
51 |
52 | ---Use lhf's random, if available. It provides an RNG with better
53 | -- statistical properties, and it gives consistent values across OSs.
54 | -- http://www.tecgraf.puc-rio.br/~lhf/ftp/lua/#lrandom
55 | pcall(require, "random")
56 | local random = random
57 |
58 | -- Use the debug API to get line numbers, if available.
59 | pcall(require, "debug")
60 | local debug = debug
61 |
62 | -- Use luasocket's gettime(), luaposix' gettimeofday(), or os.date for
63 | -- timestamps
64 | local now = pcall(require, "socket") and socket.gettime or
65 | pcall(require, "posix") and posix.gettimeofday and
66 | function ()
67 | local s, us = posix.gettimeofday()
68 | return s + us / 1000000
69 | end or
70 | function () return tonumber(os.date("%s")) end
71 |
72 | -- Get env immediately wrapping module, to put assert_ tests there.
73 | local _importing_env = getenv()
74 |
75 | -- Check command line arguments:
76 | -- -v / --verbose, default to verbose_hooks.
77 | -- -s or --suite, only run the named suite(s).
78 | -- -t or --test, only run tests matching the pattern.
79 | local lt_arg = arg
80 |
81 | -- #####################
82 | -- # Utility functions #
83 | -- #####################
84 |
85 | local function printf(...) print(string.format(...)) end
86 |
87 | local function result_table(name)
88 | return { name=name, pass={}, fail={}, skip={}, err={} }
89 | end
90 |
91 | local function combine_results(to, from)
92 | local s_name = from.name
93 | for _,set in ipairs{"pass", "fail", "skip", "err" } do
94 | local fs, ts = from[set], to[set]
95 | for name,val in pairs(fs) do
96 | ts[s_name .. "." .. name] = val
97 | end
98 | end
99 | end
100 |
101 | local function is_func(v) return type(v) == "function" end
102 |
103 | local function count(t)
104 | local ct = 0
105 | for _ in pairs(t) do ct = ct + 1 end
106 | return ct
107 | end
108 |
109 |
110 | -- ###########
111 | -- # Results #
112 | -- ###########
113 |
114 | local function msec(t)
115 | if t and type(t) == "number" then
116 | return fmt(" (%.2fms)", t * 1000)
117 | else
118 | return ""
119 | end
120 | end
121 |
122 |
123 | local RPass = {}
124 | local passMT = {__index=RPass}
125 | function RPass:tostring_char() return "." end
126 | function RPass:add(s, name) s.pass[name] = self end
127 | function RPass:type() return "pass" end
128 | function RPass:tostring(name)
129 | return fmt("PASS: %s%s%s",
130 | name or "(unknown)", msec(self.elapsed),
131 | self.msg and (": " .. tostring(self.msg)) or "")
132 | end
133 |
134 |
135 | local RFail = {}
136 | local failMT = {__index=RFail}
137 | function RFail:tostring_char() return "F" end
138 | function RFail:add(s, name) s.fail[name] = self end
139 | function RFail:type() return "fail" end
140 | function RFail:tostring(name)
141 | return fmt("FAIL: %s%s: %s%s%s",
142 | name or "(unknown)",
143 | msec(self.elapsed),
144 | self.reason or "",
145 | self.msg and (" - " .. tostring(self.msg)) or "",
146 | self.line and (" (%d)"):format(self.line) or "")
147 | end
148 |
149 |
150 | local RSkip = {}
151 | local skipMT = {__index=RSkip}
152 | function RSkip:tostring_char() return "s" end
153 | function RSkip:add(s, name) s.skip[name] = self end
154 | function RSkip:type() return "skip" end
155 | function RSkip:tostring(name)
156 | return fmt("SKIP: %s()%s", name or "unknown",
157 | self.msg and (" - " .. tostring(self.msg)) or "")
158 | end
159 |
160 |
161 | local RError = {}
162 | local errorMT = {__index=RError}
163 | function RError:tostring_char() return "E" end
164 | function RError:add(s, name) s.err[name] = self end
165 | function RError:type() return "error" end
166 | function RError:tostring(name)
167 | return self.msg or
168 | fmt("ERROR (in %s%s, couldn't get traceback)",
169 | msec(self.elapsed), name or "(unknown)")
170 | end
171 |
172 |
173 | local function Pass(t) return setmetatable(t or {}, passMT) end
174 | local function Fail(t) return setmetatable(t, failMT) end
175 | local function Skip(t) return setmetatable(t, skipMT) end
176 | local function Error(t) return setmetatable(t, errorMT) end
177 |
178 |
179 | -- ##############
180 | -- # Assertions #
181 | -- ##############
182 |
183 | ---Renamed standard assert.
184 | local checked = 0
185 | local TS = tostring
186 |
187 | local function wraptest(flag, msg, t)
188 | checked = checked + 1
189 | t.msg = msg
190 | if debug then
191 | local info = debug.getinfo(3, "l")
192 | t.line = info.currentline
193 | end
194 | if not flag then error(Fail(t)) end
195 | end
196 |
197 | ---Fail a test.
198 | -- @param no_exit Unless set to true, the presence of any failures
199 | -- causes the test suite to terminate with an exit status of 1.
200 | function fail(msg, no_exit)
201 | local line
202 | if debug then
203 | local info = debug.getinfo(2, "l")
204 | line = info.currentline
205 | end
206 | error(Fail { msg=msg, reason="(Failed)", no_exit=no_exit, line=line })
207 | end
208 |
209 |
210 | ---Skip a test, with a note, e.g. "TODO".
211 | function skip(msg) error(Skip { msg=msg }) end
212 |
213 |
214 | ---got == true.
215 | -- (Named "assert_true" to not conflict with standard assert.)
216 | -- @param msg Message to display with the result.
217 | function assert_true(got, msg)
218 | wraptest(got, msg, { reason=fmt("Expected success, got %s.", TS(got)) })
219 | end
220 |
221 | ---got == false.
222 | function assert_false(got, msg)
223 | wraptest(not got, msg,
224 | { reason=fmt("Expected false, got %s", TS(got)) })
225 | end
226 |
227 | --got == nil
228 | function assert_nil(got, msg)
229 | wraptest(got == nil, msg,
230 | { reason=fmt("Expected nil, got %s", TS(got)) })
231 | end
232 |
233 | --got ~= nil
234 | function assert_not_nil(got, msg)
235 | wraptest(got ~= nil, msg,
236 | { reason=fmt("Expected non-nil value, got %s", TS(got)) })
237 | end
238 |
239 | local function tol_or_msg(t, m)
240 | if not t and not m then return 0, nil
241 | elseif type(t) == "string" then return 0, t
242 | elseif type(t) == "number" then return t, m
243 | else error("Neither a numeric tolerance nor string")
244 | end
245 | end
246 |
247 |
248 | ---exp == got.
249 | function assert_equal(exp, got, tol, msg)
250 | tol, msg = tol_or_msg(tol, msg)
251 | if type(exp) == "number" and type(got) == "number" then
252 | wraptest(math.abs(exp - got) <= tol, msg,
253 | { reason=fmt("Expected %s +/- %s, got %s",
254 | TS(exp), TS(tol), TS(got)) })
255 | else
256 | wraptest(exp == got, msg,
257 | { reason=fmt("Expected %q, got %q", TS(exp), TS(got)) })
258 | end
259 | end
260 |
261 | ---exp ~= got.
262 | function assert_not_equal(exp, got, msg)
263 | wraptest(exp ~= got, msg,
264 | { reason="Expected something other than " .. TS(exp) })
265 | end
266 |
267 | ---val > lim.
268 | function assert_gt(lim, val, msg)
269 | wraptest(val > lim, msg,
270 | { reason=fmt("Expected a value > %s, got %s",
271 | TS(lim), TS(val)) })
272 | end
273 |
274 | ---val >= lim.
275 | function assert_gte(lim, val, msg)
276 | wraptest(val >= lim, msg,
277 | { reason=fmt("Expected a value >= %s, got %s",
278 | TS(lim), TS(val)) })
279 | end
280 |
281 | ---val < lim.
282 | function assert_lt(lim, val, msg)
283 | wraptest(val < lim, msg,
284 | { reason=fmt("Expected a value < %s, got %s",
285 | TS(lim), TS(val)) })
286 | end
287 |
288 | ---val <= lim.
289 | function assert_lte(lim, val, msg)
290 | wraptest(val <= lim, msg,
291 | { reason=fmt("Expected a value <= %s, got %s",
292 | TS(lim), TS(val)) })
293 | end
294 |
295 | ---#val == len.
296 | function assert_len(len, val, msg)
297 | wraptest(#val == len, msg,
298 | { reason=fmt("Expected #val == %d, was %d",
299 | len, #val) })
300 | end
301 |
302 | ---#val ~= len.
303 | function assert_not_len(len, val, msg)
304 | wraptest(#val ~= len, msg,
305 | { reason=fmt("Expected length other than %d", len) })
306 | end
307 |
308 | ---Test that the string s matches the pattern exp.
309 | function assert_match(pat, s, msg)
310 | s = tostring(s)
311 | wraptest(type(s) == "string" and s:match(pat), msg,
312 | { reason=fmt("Expected string to match pattern %s, was %s",
313 | pat,
314 | (s:len() > 30 and (s:sub(1,30) .. "...")or s)) })
315 | end
316 |
317 | ---Test that the string s doesn't match the pattern exp.
318 | function assert_not_match(pat, s, msg)
319 | wraptest(type(s) ~= "string" or not s:match(pat), msg,
320 | { reason=fmt("Should not match pattern %s", pat) })
321 | end
322 |
323 | ---Test that val is a boolean.
324 | function assert_boolean(val, msg)
325 | wraptest(type(val) == "boolean", msg,
326 | { reason=fmt("Expected type boolean but got %s",
327 | type(val)) })
328 | end
329 |
330 | ---Test that val is not a boolean.
331 | function assert_not_boolean(val, msg)
332 | wraptest(type(val) ~= "boolean", msg,
333 | { reason=fmt("Expected type other than boolean but got %s",
334 | type(val)) })
335 | end
336 |
337 | ---Test that val is a number.
338 | function assert_number(val, msg)
339 | wraptest(type(val) == "number", msg,
340 | { reason=fmt("Expected type number but got %s",
341 | type(val)) })
342 | end
343 |
344 | ---Test that val is not a number.
345 | function assert_not_number(val, msg)
346 | wraptest(type(val) ~= "number", msg,
347 | { reason=fmt("Expected type other than number but got %s",
348 | type(val)) })
349 | end
350 |
351 | ---Test that val is a string.
352 | function assert_string(val, msg)
353 | wraptest(type(val) == "string", msg,
354 | { reason=fmt("Expected type string but got %s",
355 | type(val)) })
356 | end
357 |
358 | ---Test that val is not a string.
359 | function assert_not_string(val, msg)
360 | wraptest(type(val) ~= "string", msg,
361 | { reason=fmt("Expected type other than string but got %s",
362 | type(val)) })
363 | end
364 |
365 | ---Test that val is a table.
366 | function assert_table(val, msg)
367 | wraptest(type(val) == "table", msg,
368 | { reason=fmt("Expected type table but got %s",
369 | type(val)) })
370 | end
371 |
372 | ---Test that val is not a table.
373 | function assert_not_table(val, msg)
374 | wraptest(type(val) ~= "table", msg,
375 | { reason=fmt("Expected type other than table but got %s",
376 | type(val)) })
377 | end
378 |
379 | ---Test that val is a function.
380 | function assert_function(val, msg)
381 | wraptest(type(val) == "function", msg,
382 | { reason=fmt("Expected type function but got %s",
383 | type(val)) })
384 | end
385 |
386 | ---Test that val is not a function.
387 | function assert_not_function(val, msg)
388 | wraptest(type(val) ~= "function", msg,
389 | { reason=fmt("Expected type other than function but got %s",
390 | type(val)) })
391 | end
392 |
393 | ---Test that val is a thread (coroutine).
394 | function assert_thread(val, msg)
395 | wraptest(type(val) == "thread", msg,
396 | { reason=fmt("Expected type thread but got %s",
397 | type(val)) })
398 | end
399 |
400 | ---Test that val is not a thread (coroutine).
401 | function assert_not_thread(val, msg)
402 | wraptest(type(val) ~= "thread", msg,
403 | { reason=fmt("Expected type other than thread but got %s",
404 | type(val)) })
405 | end
406 |
407 | ---Test that val is a userdata (light or heavy).
408 | function assert_userdata(val, msg)
409 | wraptest(type(val) == "userdata", msg,
410 | { reason=fmt("Expected type userdata but got %s",
411 | type(val)) })
412 | end
413 |
414 | ---Test that val is not a userdata (light or heavy).
415 | function assert_not_userdata(val, msg)
416 | wraptest(type(val) ~= "userdata", msg,
417 | { reason=fmt("Expected type other than userdata but got %s",
418 | type(val)) })
419 | end
420 |
421 | ---Test that a value has the expected metatable.
422 | function assert_metatable(exp, val, msg)
423 | local mt = getmetatable(val)
424 | wraptest(mt == exp, msg,
425 | { reason=fmt("Expected metatable %s but got %s",
426 | TS(exp), TS(mt)) })
427 | end
428 |
429 | ---Test that a value does not have a given metatable.
430 | function assert_not_metatable(exp, val, msg)
431 | local mt = getmetatable(val)
432 | wraptest(mt ~= exp, msg,
433 | { reason=fmt("Expected metatable other than %s",
434 | TS(exp)) })
435 | end
436 |
437 | ---Test that the function raises an error when called.
438 | function assert_error(f, msg)
439 | local ok, err = pcall(f)
440 | local got = ok or err
441 | wraptest(not ok, msg,
442 | { exp="an error", got=got,
443 | reason=fmt("Expected an error, got %s", TS(got)) })
444 | end
445 |
446 |
447 | ---Run a test case with randomly instantiated arguments,
448 | -- running the test function f opt.count (default: 100) times.
449 | -- @param opt A table with options, or just a test name string.
450 | -- opt.count: how many random trials to perform
451 | -- opt.seed: Start the batch of trials with a specific seed
452 | -- opt.always: Always test these seeds (for regressions)
453 | -- opt.show_progress: Whether to print a . after every opt.tick trials.
454 | -- opt.seed_limit: Max seed to allow.
455 | -- opt.max_failures, max_errors, max_skips: Give up after X of each.
456 | -- @param f A test function, run as f(unpack(randomized_args(...)))
457 | -- @param ... the arg specification. For each argument, creates a
458 | -- random instance of that type.
459 | -- boolean: return true or false
460 | -- number n: returns 0 <= x < n, or -n <= x < n if negative.
461 | -- If n has a decimal component, so will the result.
462 | -- string: Specifiedd as "(len[,maxlen]) (pattern)".
463 | -- "10 %l" means 10 random lowercase letters.
464 | -- "10,30 [aeiou]" means between 10-30 vowels.
465 | -- function: Just call (as f()) and return result.
466 | -- table or userdata: Call v.__random() and return result.
467 | -- @usage
468 | function assert_random(opt, f, ...)
469 | -- Stub. Exported to the same namespace, but code appears below.
470 | end
471 |
472 |
473 | -- ####################
474 | -- # Module beginning #
475 | -- ####################
476 |
477 | ---Unit testing module, with extensions for random testing.
478 | module("lunatest")
479 |
480 | VERSION = "0.94"
481 |
482 |
483 | -- #########
484 | -- # Hooks #
485 | -- #########
486 |
487 | local dot_ct = 0
488 | local cols = 70
489 |
490 | local iow = io.write
491 |
492 | -- Print a char ([.fEs], etc.), wrapping at 70 columns.
493 | local function dot(c)
494 | c = c or "."
495 | io.write(c)
496 | dot_ct = dot_ct + 1
497 | if dot_ct > cols then
498 | io.write("\n ")
499 | dot_ct = 0
500 | end
501 | io.stdout:flush()
502 | end
503 |
504 | local function print_totals(r)
505 | local ps, fs = count(r.pass), count(r.fail)
506 | local ss, es = count(r.skip), count(r.err)
507 | if checked == 0 then return end
508 | local el, unit = r.t_post - r.t_pre, "s"
509 | if el < 1 then unit = "ms"; el = el * 1000 end
510 | local elapsed = fmt(" in %.2f %s", el, unit)
511 | local buf = {"\n---- Testing finished%s, ",
512 | "with %d assertion(s) ----\n",
513 | " %d passed, %d failed, ",
514 | "%d error(s), %d skipped."}
515 | printf(table.concat(buf), elapsed, checked, ps, fs, es, ss)
516 | end
517 |
518 |
519 | ---Default behavior.
520 | default_hooks = {
521 | begin = false,
522 | begin_suite = function(s_env, tests)
523 | iow(fmt("\n-- Starting suite %q, %d test(s)\n ",
524 | s_env.name, count(tests)))
525 | end,
526 | end_suite = false,
527 | pre_test = false,
528 | post_test = function(name, res)
529 | dot(res:tostring_char())
530 | end,
531 | done = function(r)
532 | print_totals(r)
533 | for _,ts in ipairs{ r.fail, r.err, r.skip } do
534 | for name,res in pairs(ts) do
535 | printf("%s", res:tostring(name))
536 | end
537 | end
538 | end,
539 | }
540 |
541 |
542 | ---Default verbose behavior.
543 | verbose_hooks = {
544 | begin = function(res, suites)
545 | local s_ct = count(suites)
546 | if s_ct > 0 then
547 | printf("Starting tests, %d suite(s)", s_ct)
548 | end
549 | end,
550 | begin_suite = function(s_env, tests)
551 | dot_ct = 0
552 | printf("-- Starting suite %q, %d test(s)",
553 | s_env.name, count(tests))
554 | end,
555 | end_suite =
556 | function(s_env)
557 | local ps, fs = count(s_env.pass), count(s_env.fail)
558 | local ss, es = count(s_env.skip), count(s_env.err)
559 | dot_ct = 0
560 | printf(" Finished suite %q, +%d -%d E%d s%d",
561 | s_env.name, ps, fs, es, ss)
562 | end,
563 | pre_test = false,
564 | post_test = function(name, res)
565 | printf("%s", res:tostring(name))
566 | dot_ct = 0
567 | end,
568 | done = function(r) print_totals(r) end
569 | }
570 |
571 | setmetatable(verbose_hooks, {__index = default_hooks })
572 |
573 |
574 | -- ################
575 | -- # Registration #
576 | -- ################
577 |
578 | local suites = {}
579 | local failed_suites = {}
580 |
581 | ---Check if a function name should be considered a test key.
582 | -- Defaults to functions starting or ending with "test", with
583 | -- leading underscores allowed.
584 | function is_test_key(k)
585 | return type(k) == "string" and (k:match("^test.*") or k:match("test$"))
586 | end
587 |
588 | local function get_tests(mod)
589 | local ts = {}
590 | if type(mod) == "table" then
591 | for k,v in pairs(mod) do
592 | if is_test_key(k) and type(v) == "function" then
593 | ts[k] = v
594 | end
595 | end
596 | ts.setup = rawget(mod, "setup")
597 | ts.teardown = rawget(mod, "teardown")
598 | ts.ssetup = rawget(mod, "suite_setup")
599 | ts.steardown = rawget(mod, "suite_teardown")
600 | return ts
601 | end
602 | return {}
603 | end
604 |
605 | ---Add a file as a test suite.
606 | -- @param modname The module to load as a suite. The file is
607 | -- interpreted in the same manner as require "modname".
608 | -- Which functions are tests is determined by is_test_key(name).
609 | function suite(modname)
610 | local ok, err = pcall(
611 | function()
612 | local mod, r_err = require(modname)
613 | suites[modname] = get_tests(mod)
614 | end)
615 | if not ok then
616 | print(fmt(" * Error loading test suite %q:\n%s",
617 | modname, tostring(err)))
618 | failed_suites[#failed_suites+1] = modname
619 | end
620 | end
621 |
622 |
623 | -- ###########
624 | -- # Running #
625 | -- ###########
626 |
627 | local ok_types = { pass=true, fail=true, skip=true }
628 |
629 | local function err_handler(name)
630 | return function (e)
631 | if type(e) == "table" and e.type and ok_types[e.type()] then return e end
632 | local msg = fmt("ERROR in %s():\n\t%s", name, tostring(e))
633 | msg = debug.traceback(msg, 3)
634 | return Error { msg=msg }
635 | end
636 | end
637 |
638 |
639 | local function run_test(name, test, suite, hooks, setup, teardown)
640 | local result
641 | if is_func(hooks.pre_test) then hooks.pre_test(name) end
642 | local t_pre, t_post, elapsed --timestamps. requires luasocket.
643 | local ok, err
644 |
645 | if is_func(setup) then
646 | ok, err = xpcall(function() setup(name) end, err_handler(name))
647 | else
648 | ok = true
649 | end
650 |
651 | if ok then
652 | t_pre = now()
653 | ok, err = xpcall(test, err_handler(name))
654 | t_post = now()
655 | elapsed = t_post - t_pre
656 |
657 | if is_func(teardown) then
658 | if ok then
659 | ok, err = xpcall(function() teardown(name, elapsed) end,
660 | err_handler(name))
661 | else
662 | xpcall(function() teardown(name, elapsed) end,
663 | function(info)
664 | print "\n==============================================="
665 | local msg = fmt("ERROR in teardown handler: %s", info)
666 | print(msg)
667 | os.exit(1)
668 | end)
669 | end
670 | end
671 | end
672 |
673 | if ok then err = Pass() end
674 | result = err
675 | result.elapsed = elapsed
676 |
677 | -- TODO: log tests w/ no assertions?
678 | result:add(suite, name)
679 |
680 | if is_func(hooks.post_test) then hooks.post_test(name, result) end
681 | end
682 |
683 |
684 | local function cmd_line_switches(arg)
685 | arg = arg or {}
686 | local opts = {}
687 | for i=1,#arg do
688 | local v = arg[i]
689 | if v == "-v" or v == "--verbose" then opts.verbose=true
690 | elseif v == "-s" or v == "--suite" then
691 | opts.suite_pat = arg[i+1]
692 | elseif v == "-t" or v == "--test" then
693 | opts.test_pat = arg[i+1]
694 | end
695 | end
696 | return opts
697 | end
698 |
699 |
700 | local function failure_or_error_count(r)
701 | local t = 0
702 | for k,f in pairs(r.err) do
703 | t = t + 1
704 | end
705 | for k,f in pairs(r.fail) do
706 | if not f.no_exit then t = t + 1 end
707 | end
708 | return t
709 | end
710 |
711 | local function run_suite(hooks, opts, results, sname, tests)
712 | local ssetup, steardown = tests.ssetup, tests.steardown
713 | tests.ssetup, tests.steardown = nil, nil
714 |
715 | if not opts.suite_pat or sname:match(opts.suite_pat) then
716 | local run_suite = true
717 | local res = result_table(sname)
718 |
719 | if ssetup then
720 | local ok, err = pcall(ssetup)
721 | if not ok or (ok and err == false) then
722 | run_suite = false
723 | local msg = fmt("Error in %s's suite_setup: %s", sname, tostring(err))
724 | failed_suites[#failed_suites+1] = sname
725 | results.err[sname] = Error{msg=msg}
726 | end
727 | end
728 |
729 | if run_suite and count(tests) > 0 then
730 | local setup, teardown = tests.setup, tests.teardown
731 | tests.setup, tests.teardown = nil, nil
732 |
733 | if hooks.begin_suite then hooks.begin_suite(res, tests) end
734 | res.tests = tests
735 | for name, test in pairs(tests) do
736 | if not opts.test_pat or name:match(opts.test_pat) then
737 | run_test(name, test, res, hooks, setup, teardown)
738 | end
739 | end
740 | if steardown then pcall(steardown) end
741 | if hooks.end_suite then hooks.end_suite(res) end
742 | combine_results(results, res)
743 | end
744 | end
745 | end
746 |
747 | ---Run all known test suites, with given configuration hooks.
748 | -- @param hooks Override the default hooks.
749 | -- @param opts Override command line arguments.
750 | -- @usage If no hooks are provided and arg[1] == "-v", the verbose_hooks will
751 | -- be used. opts is expected to be a table of command line arguments.
752 | function run(hooks, opts)
753 | -- also check the namespace it's run in
754 | local opts = opts and cmd_line_switches(opts) or cmd_line_switches(lt_arg)
755 |
756 | -- Make stdout line-buffered for better interactivity when the output is
757 | -- not going to the terminal, e.g. is piped to another program.
758 | --io.stdout:setvbuf("line")
759 |
760 | if hooks == true or opts.verbose then
761 | hooks = verbose_hooks
762 | else
763 | hooks = hooks or {}
764 | end
765 |
766 | setmetatable(hooks, {__index = default_hooks})
767 |
768 | local results = result_table("main")
769 | results.t_pre = now()
770 |
771 | -- If it's all in one test file, check its environment, too.
772 | local env = getenv(3)
773 | if env then suites.main = get_tests(env) end
774 |
775 | if hooks.begin then hooks.begin(results, suites) end
776 |
777 | for sname,suite in pairs(suites) do
778 | run_suite(hooks, opts, results, sname, suite)
779 | end
780 | results.t_post = now()
781 | if hooks.done then hooks.done(results) end
782 |
783 | local failures = failure_or_error_count(results)
784 | return failures + #failed_suites
785 | end
786 |
787 |
788 | -- ########################
789 | -- # Randomization basics #
790 | -- ########################
791 |
792 | local _r
793 | if random then
794 | _r = random.new()
795 | end
796 |
797 | ---Set random seed.
798 | function set_seed(s) _r:seed(s) end
799 |
800 | ---Get a random value low <= x <= high.
801 | function random_int(low, high)
802 | if not high then high = low; low = 0 end
803 | return _r:value(low, high)
804 | end
805 |
806 | ---Get a random bool.
807 | function random_bool() return random_int(0, 1) == 1 end
808 |
809 | ---Get a random float low <= x < high.
810 | function random_float(low, high)
811 | return random_int(low, high - 1) + _r:value()
812 | end
813 |
814 |
815 | if not random then
816 | set_seed = math.randomseed
817 | random_bool = function() return math.random(0, 1) == 1 end
818 | random_float = function(l, h)
819 | return random_int(l, h - 1) + math.random()
820 | end
821 | random_int = function(l, h)
822 | if not h then h = l; l = 0 end
823 | return math.random(l, h)
824 | end
825 | end
826 |
827 | -- Lua_number's bits of precision. IEEE 754 doubles have 52.
828 | local function determine_accuracy()
829 | for i=1,128 do
830 | if 2^i == (2^i + 1) then return i - 1 end
831 | end
832 | return 128 --long long ints?
833 | end
834 | local bits_of_accuracy = determine_accuracy()
835 |
836 |
837 | -- ##################
838 | -- # Random strings #
839 | -- ##################
840 |
841 |
842 | -- For valid char classes, see Lua Reference Manual 5.1, p. 77
843 | -- or http://www.lua.org/manual/5.1/manual.html#5.4.1 .
844 | local function charclass(pat)
845 | local m = {}
846 |
847 | local match, char = string.match, string.char
848 | for i=0,255 do
849 | local c = char(i)
850 | if match(c, pat) then m[#m+1] = c end
851 | end
852 |
853 | return table.concat(m)
854 | end
855 |
856 |
857 | -- Return a (() -> random char) iterator from a pattern.
858 | local function parse_pattern(pattern)
859 | local cs = {} --charset
860 | local idx = 1
861 | local len = string.len(pattern)
862 | assert(len > 0, "Cannot generate pattern from empty string.")
863 |
864 | local function at_either_end() return #cs == 0 or #cs == len end
865 | local function slice(i) return string.sub(pattern, i, i) end
866 |
867 | while idx <= len do
868 | local c = slice(idx)
869 |
870 | if c == "-" then
871 | if at_either_end() then
872 | cs[#cs+1] = c --literal - at start or end
873 | else --range
874 | local low = string.byte(slice(idx-1)) + 1
875 | local high = string.byte(slice(idx+1))
876 | assert(low < high, "Invalid character range: " .. pattern)
877 | for asc=low,high do
878 | cs[#cs+1] = string.char(asc)
879 | end
880 | idx = idx + 1
881 | end
882 |
883 | elseif c == "%" then
884 | local nextc = slice(idx + 1)
885 | cs[#cs+1] = charclass("%" .. nextc)
886 | idx = idx + 1
887 |
888 | else
889 | cs[#cs+1] = c
890 | end
891 | idx = idx + 1
892 | end
893 |
894 | cs = table.concat(cs)
895 | local len = string.len(cs)
896 | assert(len > 0, "Empty charset")
897 |
898 | return function()
899 | local idx = random_int(1, len)
900 | return string.sub(cs, idx, idx)
901 | end
902 | end
903 |
904 |
905 | -- Read a random string spec, return a config table.
906 | local function parse_randstring(s)
907 | local low, high, rest = string.match(s, "([0-9]+),?([0-9]*) (.*)")
908 | if low then --any match
909 | if high == "" then high = low end
910 | return { low = tonumber(low),
911 | high = tonumber(high),
912 | gen = parse_pattern(rest) }
913 | else
914 | local err = "Invalid random string spec: " .. s
915 | error(err, 2)
916 | end
917 | end
918 |
919 |
920 | -- Generate a random string.
921 | -- @usage e.g. "20 listoftwentycharstogenerate" or "10,20 %l".
922 | function random_string(spec)
923 | local info = parse_randstring(spec)
924 | local ct, diff
925 | diff = info.high - info.low
926 | if diff == 0 then ct = info.low else
927 | ct = random_int(diff) + info.low
928 | end
929 |
930 | local acc = {}
931 | for i=1,ct do
932 | acc[i] = info.gen(self)
933 | end
934 | local res = table.concat(acc)
935 | assert(res:len() == ct, "Bad string gen")
936 | return res
937 | end
938 |
939 |
940 | -- #########################
941 | -- # General random values #
942 | -- #########################
943 |
944 | -- Generate a random number, according to arg.
945 | local function gen_number(arg)
946 | arg = arg or math.huge
947 | local signed = (arg < 0)
948 | local float
949 | if signed then float = (math.ceil(arg) ~= arg) else
950 | float = (math.floor(arg) ~= arg)
951 | end
952 |
953 | local f = float and random_float or random_int
954 | if signed then
955 | return f(arg, -arg)
956 | else
957 | return f(0, arg)
958 | end
959 | end
960 |
961 |
962 | -- Create an arbitrary instance of a value.
963 | local function generate_arbitrary(arg)
964 | local t = type(arg)
965 | if t == "number" then
966 | return gen_number(arg)
967 | elseif t == "function" then
968 | return arg(gen_number()) -- assume f(number) -> val
969 | elseif t == "string" then
970 | return random_string(arg)
971 | elseif t == "table" or t == "userdata" then
972 | assert(arg.__random, t .. " has no __random method")
973 | -- assume arg.__random(number) -> val
974 | return arg.__random(gen_number())
975 | elseif t == "boolean" then
976 | return random_bool()
977 | else
978 | error("Cannot randomly generate values of type " .. t .. ".")
979 | end
980 | end
981 |
982 |
983 | local random_test_defaults = {
984 | count = 100,
985 | max_failures = 10,
986 | max_errors = 5,
987 | max_skips = 50,
988 | random_bound = 2^bits_of_accuracy,
989 | seed_limit = math.min(1e13, 2^bits_of_accuracy),
990 | always = {},
991 | seed = nil,
992 | show_progress = true
993 | }
994 |
995 |
996 | local function random_args(args)
997 | local as = {}
998 | for i=1,#args do
999 | as[i] = generate_arbitrary(args[i])
1000 | end
1001 | return as
1002 | end
1003 |
1004 |
1005 | local function new_seed(limit)
1006 | limit = limit or 1e13
1007 | return random_int(0, limit)
1008 | end
1009 |
1010 |
1011 | local function get_seeds_and_args(t)
1012 | local ss = {}
1013 | for _,r in ipairs(t) do
1014 | if r.seed then
1015 | ss[#ss+1] = fmt("%s %s\n Seed: %s",
1016 | r.reason or "", r.msg and ("\n " .. r.msg) or "", r.seed)
1017 | end
1018 | if r.args then
1019 | for i,arg in ipairs(r.args) do
1020 | ss[#ss+1] = " * " .. arg
1021 | end
1022 | end
1023 | ss[#ss+1] = ""
1024 | end
1025 | return ss
1026 | end
1027 |
1028 |
1029 | local function run_randtest(seed, f, args, r, limit)
1030 | local try_ct = 0
1031 | while r.tried[seed] and try_ct < 50 do
1032 | seed = new_seed(limit)
1033 | try_ct = try_ct + 1
1034 | end
1035 | if try_ct >= 50 then
1036 | error(Fail { reason = "Exhausted all seeds" })
1037 | end
1038 | set_seed(seed)
1039 | r.tried[seed] = true
1040 |
1041 | local result
1042 | local r_args = random_args(args)
1043 | local ok, err = pcall(function() f(unpack(r_args)) end)
1044 | if ok then
1045 | result = Pass()
1046 | result.seed = seed
1047 | r.ps[#r.ps+1] = result
1048 | else
1049 | -- So errors in the suite itself get through...
1050 | if type(err) == "string" then error(err) end
1051 | result = err
1052 | result.seed = seed
1053 | local rt = result:type()
1054 | if rt == "pass" then r.ps[#r.ps+1] = result
1055 | elseif rt == "fail" then r.fs[#r.fs+1] = result
1056 | elseif rt == "error" then r.es[#r.es+1] = result
1057 | elseif rt == "skip" then r.ss[#r.ss+1] = result
1058 | else error("unmatched")
1059 | end
1060 | end
1061 |
1062 | seed = new_seed(limit)
1063 | r.ts = r.ts + 1
1064 | local str_args = {}
1065 | -- Convert args to strs (for display) and add to result.
1066 | for i,v in ipairs(r_args) do
1067 | str_args[i] = tostring(v)
1068 | end
1069 | result.args = str_args
1070 | return seed
1071 | end
1072 |
1073 |
1074 | local function report_trial(r, opt)
1075 | if #r.es > 0 then
1076 | local seeds = get_seeds_and_args(r.es)
1077 | error(Fail { reason = fmt("%d tests, %d error(s).\n %s",
1078 | r.ts, #r.es,
1079 | table.concat(seeds, "\n ")),
1080 | seeds = seeds})
1081 | elseif #r.fs > 0 then
1082 | local seeds = get_seeds_and_args(r.fs)
1083 | error(Fail { reason = fmt("%d tests, %d failure(s).\n %s",
1084 | r.ts, #r.fs,
1085 | table.concat(seeds, "\n ")),
1086 | seeds = seeds})
1087 | elseif #r.ss >= opt.max_skips then
1088 | error(Fail { reason = fmt("Too many cases skipped.")})
1089 | else
1090 | error(Pass { reason = fmt(": %d cases passed.", #r.ps) })
1091 | end
1092 | end
1093 |
1094 |
1095 | local function assert_random(opt, f, ...)
1096 | local args = { ... }
1097 | if type(opt) == "string" then
1098 | opt = { name=opt }
1099 | elseif type(opt) == "function" then
1100 | table.insert(args, 1, f)
1101 | f = opt
1102 | opt = {}
1103 | end
1104 |
1105 | setmetatable(opt, { __index=random_test_defaults })
1106 |
1107 | local seed = opt.seed or os.time()
1108 | local r = { ps={}, fs={}, es={}, ss={}, ts=0, tried={} }
1109 |
1110 | -- Run these seeds every time, for easy regression testing.
1111 | for _,s in ipairs(opt.always) do
1112 | run_randtest(s, f, args, r, opt.seed_limit)
1113 | end
1114 |
1115 | set_seed(seed)
1116 | local tick = opt.tick or opt.count / 10
1117 |
1118 | for i=1,opt.count do
1119 | seed = run_randtest(seed, f, args, r, opt.seed_limit)
1120 | if #r.ss >= opt.max_skips or
1121 | #r.fs >= opt.max_failures or
1122 | #r.es >= opt.max_errors then break
1123 | end
1124 | if opt.show_progress and i % tick == 0 then
1125 | dot(".")
1126 | end
1127 | end
1128 | local overall_status = (passed == count and "PASS" or "FAIL")
1129 |
1130 | report_trial(r, opt)
1131 | end
1132 |
1133 |
1134 | -- Put it in the same namespace as the other assert_ functions.
1135 | _importing_env.assert_random = assert_random
1136 |
1137 |
--------------------------------------------------------------------------------