Skip to content

Instantly share code, notes, and snippets.

@zserge
Created October 24, 2025 08:27
Show Gist options
  • Select an option

  • Save zserge/f97d7d00bd63ba0ee695ec06a1868717 to your computer and use it in GitHub Desktop.

Select an option

Save zserge/f97d7d00bd63ba0ee695ec06a1868717 to your computer and use it in GitHub Desktop.
NAMES = {
'@': 'Fighter'
'}': 'Archer'
'&': 'Monk'
'h': 'Hobgoblin'
'o': 'Orc'
'g': 'Gargoile'
'H': 'Hobgoblin Lord'
'O': 'Orc Lord'
'G': 'Gargoile Lord'
'*': 'Gold'
'+': 'Potion'
'/': 'Bow'
'~': 'Magic spell'
}
class Evil
randof: (list, prob) ->
sum = 0
for p in prob
sum = sum + p
i = Math.random() * sum
index = 0
for p in prob
if i < p then return list[index]
i = i - p
index = index + 1
produceMonster: (lvl) ->
if lvl < 10 then return @kobold(lvl)
else if lvl < 20 then return @randof([@kobold, @orc], [2, 1])(lvl)
else if lvl < 30 then return @randof([@kobold, @orc, @gargoile], [2, 1, 1])(lvl)
else if lvl < 40 then return @randof([@kobold, @orc, @gargoile], [2, 1, 2])(lvl)
else return @randof([@kobold, @orc, @gargoile], [2, 3, 2])(lvl)
kobold: (lvl) ->
if lvl < 10 or Math.random() < 0.5
m = new Creature('h', 2, 2, 0)
m.reward = (c, game) ->
c.addxp(2)
game.gold =game.gold + 1
else
m = new Creature('H', 4, 2, 1)
m.reward = (c, game) ->
c.addxp(4)
game.gold =game.gold + 2
return m
orc: (lvl) ->
if lvl < 30 or Math.random() < 0.5
m = new Creature('o', 5, 3, 1)
else
m = new Creature('O', 10, 4, 1)
m.counter = 0
m.ai = (game) ->
m.counter = m.counter + 1
if m.counter % 2 == 0 then return
distance = null
for e in game.enemies
if e != m and not e.peaceful and e.hp > 0 and e.line == m.line
d = e.pos - m.pos
if distance == null or Math.abs(d) < Math.abs(distance)
distance = d
if distance > 0
pos = m.pos + distance/Math.abs(distance)
if not game.at(m.line, pos)
m.pos = pos
m.reward = (c, game) ->
c.addxp(m.c == 'o'&&10||20)
game.gold =game.gold + 1
return m
gargoile: (lvl) ->
if lvl < 50 or Math.random() < 0.5
m = new Creature('g', 5, 4, 1)
else
m = new Creature('G', 10, 5, 2)
m.counter = 0
m.ai = (game) ->
dx = dy = null
for p in game.players
_dx = p.pos - m.pos
_dy = p.line - m.line
if dx == null or (_dx*_dx + _dy*_dy) < (dx*dx+dy*dy)
dx = _dx
dy = _dy
atx = game.at(m.line, m.pos + dx/Math.abs(dx))
aty = game.at(m.line + dy/Math.abs(dy), m.pos)
if dx != 0 and not atx then m.pos = m.pos + dx/Math.abs(dx)
else if dy != 0 and not aty then m.line = m.line + dy/Math.abs(dy)
for p in game.players
if Math.abs(p.pos - m.pos) < 2 and Math.abs(p.line - m.line) < 2
game.fight(p, m)
m.reward = (c, game) ->
c.addxp(m.c == 'g'&&5||20)
game.gold = game.gold + 1
return m
produceItem: (lvl) ->
if lvl < 5
return @gold()
else if lvl < 10
return @randof([@gold, @potion, @bow], [2, 1, 1])()
return @randof([@gold, @potion, @bow, @spell], [2, 2, 1, 1])()
gold: () ->
item = new Creature('*', 1, 0, 0)
item.peaceful = true
item.reward = (c, game) ->
game.gold = game.gold + 1
return item
potion: () ->
item = new Creature('+', 5, 0, 0)
item.peaceful = true
item.reward = (c, game) ->
c.hp = Math.min(c.hp + item.hp, c.maxhp)
return item
bow: () ->
item = new Creature('/', 1, 0, 0)
item.peaceful = true
item.reward = (c, game) ->
if c == game.a
c.weapon = 'Bow'
c.shoot = () ->
c.weapon = c.shoot = undefined
game.ui.msg('Whoooosh!')
hit = false
for i in [c.pos+1..(game.length-1)]
e = game.at(c.line, i)
if e and not e.peaceful then hit = true
if hit
for line in [0..2]
e = game.at(line, i)
if e and not e.peaceful
e.hp = e.hp - c.attack
if e.hp <= 0
e.hp = 0
game.ui.msg(NAMES[e.c] + ' killed with an arrow')
e.reward(c, game)
else
game.ui.msg(NAMES[e.c] + ' loses ' + c.attack + 'hp')
game.ui.render()
return item
spell: () ->
item = new Creature('~', 1, 0, 0)
item.peaceful = true
item.reward = (c, game) ->
if c == game.m
c.weapon = 'Spell'
c.shoot = () ->
c.weapon = c.shoot = undefined
game.ui.msg('Abyrvalg!')
for e in game.enemies
if not e.peaceful
e.hp = e.hp - c.defence
if e.hp <= 0
e.hp = 0
game.ui.msg(NAMES[e.c] + ' killed with magic')
e.reward(c, game)
else
game.ui.msg(NAMES[e.c] + ' loses ' + c.attack + 'hp')
game.ui.render()
return item
class Creature
constructor: (c, hp, a, d) ->
@c = c
@hp = hp
@xp = 0
@xplevel = 0
@maxhp = hp
@attack = a
@defence = d
addxp: (x) ->
@xp = @xp + x
if @xp == 10*Math.pow(2, @xplevel)
@promote && @promote()
class Game
constructor: (ui) ->
@level = 0
@gold = 0
@ui = ui
@ui.game = @
@evil = new Evil()
@f = new Creature('@', 10, 2, 0)
@f.promote = () =>
@f.maxhp = @f.maxhp + 3
@f.attack = @f.attack + 2
@a = new Creature('}', 10, 1, 2)
@a.promote = () =>
@a.maxhp = @a.maxhp + 5
@a.attack = @a.attack + 1
@a.defence = @a.defence + 1
@m = new Creature('&', 10, 1, 1)
@m.promote = () =>
@m.mmxhp = @m.mmxhp + 2
@m.defence = @m.defence + 2
@f.isplayer = @a.isplayer = @m.isplayer = true
@players = [@f, @a, @m]
@ui.msg('Welcome to the game')
try
require('./config')(@)
catch error
@nextLevel()
nextLevel: () ->
@level = @level + 1
@length = 42
@ui.msgs = []
@enemies = []
if @level == 1
for i in [0..4]
e = @evil.produceItem(@level)
e.pos = Math.floor((Math.random()*(@length-1))+1)
e.line = Math.floor((Math.random()*3))
if not @at(e.line, e.pos) then @enemies.push(e)
@ui.msg('Move to the right to complete the level')
@ui.msg('Collect gold (*) and potions (+)')
else
if @level == 2 then @ui.msg('Fight the monsters. Good luck!')
for i in [0..Math.round(Math.log(@level))]
e = @evil.produceItem(@level)
e.pos = Math.floor((Math.random()*(@length-1))+1)
e.line = Math.floor((Math.random()*3))
if not @at(e.line, e.pos) then @enemies.push(e)
for i in [0..Math.round(Math.log(@level))*2]
e = @evil.produceMonster(@level)
e.pos = Math.floor((Math.random()*(@length-1))+1)
e.line = Math.floor((Math.random()*3))
if not @at(e.line, e.pos) then @enemies.push(e)
for p in @players
p.hp = Math.min(p.hp+1, p.maxhp)
@ui.initPlayers()
at: (line, pos) ->
for e in @enemies
if e.line == line and e.pos == pos and e.hp > 0
return e
for p in @players
if p.line == line and p.pos == pos and p.hp > 0
return p
move: (c, dx, dy) ->
dx = dx || 0
c.pos = c.pos + dx
c.line = c.line + dy
if c.pos < 0 then c.pos = 0
if c.pos > @length then c.pos = @length
if c.line < 0 then c.line = 0
if c.line > 2 then c.line = 2
checkEndOfLevel: () ->
for p in @players
if p.hp > 0 and p.pos < @length then return false
return true
movePlayer: (step) ->
for p in @players
if p.hp == 0 then continue
e = @at(p.line, p.pos+step) # safe to go out of bounds
if e
if e.peaceful
@fight(p, e)
@move(p, step, 0)
else
if step == 2
if @at(p.line, p.pos+1)
@move(@at(p.line, p.pos+1), -1, 0)
@move(p, 1, 0)
@fight(p, e)
else
@move(p, step, 0)
for e in @enemies
if e.hp > 0 and e.ai
e.ai(@)
if @checkEndOfLevel() then @nextLevel()
@ui.render()
fight: (c1, c2) ->
if c2.peaceful
@ui.msg(NAMES[c1.c] + ' found ' + NAMES[c2.c])
c2.reward(c1, @)
c2.hp = 0
return
hp1 = Math.max(c2.attack - c1.defence, 0)
hp2 = Math.max(c1.attack - c2.defence, 0)
c1.hp = c1.hp - hp1
c2.hp = c2.hp - hp2
@ui.msg(NAMES[c1.c] + ' loses ' + hp1 + 'hp' + ((' and dies' if c1.hp <= 0)||'') + ', ' +\
(NAMES[c2.c] + ' loses ' + hp2 + 'hp' + ((' and dies' if c2.hp <= 0)||'')))
if c1.hp <= 0 then c1.hp = 0
if c2.hp <= 0
c2.hp = 0
c2.reward(c1, @)
if @f.hp == 0 and @a.hp == 0 and @m.hp == 0
@ui.msg('All warriors died. Game over')
@ui.exit()
magic: () ->
if not @m.weapon then return
@m.shoot()
@ui.render()
shoot: () ->
if not @a.weapon then return
@a.shoot()
@ui.render()
exit: () ->
if typeof module != 'undefined' and module.exports
module.exports = Game
else
window.Game = Game
class Ui
constructor: () ->
@msgs = []
@game = new Game(@)
@preprateInput()
@render()
preprateInput: () ->
if process
process.stdin.setRawMode(true)
process.stdin.resume()
process.stdin.setEncoding('utf8')
process.stdin.on 'data', (key) =>
@onKey(key)
clrscr: () ->
if process
@out('')
out: (s) ->
if process
process.stdout.write(s)
render: () ->
@clrscr()
@out('LEVEL:' + @game.level + '\n')
@out('GOLD:' + @game.gold+ '\n')
@out('@:' + @game.f.hp + '/' + @game.f.attack + '/' + @game.f.defence + '/' + @game.f.xp + '\n')
@out('}:' + @game.a.hp + '/' + @game.a.attack + '/' + @game.a.defence + '/' + @game.a.xp + \
' ' + (@game.a.weapon ||'Nothing') + '\n')
@out('&:' + @game.m.hp + '/' + @game.m.attack + '/' + @game.m.defence + '/' + @game.m.xp + \
' ' + (@game.m.weapon ||'Nothing') + '\n')
@out('\n')
for i in [0..2]
for j in [[email protected]]
e = @game.at(i, j)
if e then @out(e.c) else @out('.')
@out('\n')
@out('\n')
for m in @msgs
@out(' ' + m + '\n')
@out('\n\n')
@out('h/l:move j:jump k:shoot m:magic q:quit')
onKey: (k) ->
if k == 'q'
@game.exit()
@exit()
if k == 'h' then @game.movePlayer(-1)
if k == 'j' then @game.movePlayer(2)
if k == 'l' then @game.movePlayer(1)
if k == 'k' then @game.shoot()
if k == 'm' then @game.magic()
exit: () ->
@render()
if process
process.exit(0)
initPlayers: () ->
onKey = @onKey
@msg('Which row to put fighter? [Enter 1, 2 or 3]')
@game.f.pos = -1
@game.a.pos = -1
@game.m.pos = -1
choices = [0, 1, 2]
keys = ['1', '2', '3']
offset = '1'.charCodeAt(0)
@onKey = (k) ->
if k == 'q' then @exit()
for c in choices
if k == keys[c]
@game.f.line = c
choices.splice(c, 1)
@msg('Which row to put archer? [Enter ' + keys[choices[0]] + ' or ' + keys[choices[1]] + ']')
@render()
@onKey = (k) ->
if k == 'q' then @exit()
if k == keys[choices[0]]
@game.a.line = choices[0]
@game.m.line = choices[1]
@game.f.pos = @game.a.pos = @game.m.pos = 0
@onKey = onKey
@render()
if k == keys[choices[1]]
@game.a.line = choices[1]
@game.m.line = choices[0]
@game.f.pos = @game.a.pos = @game.m.pos = 0
@onKey = onKey
@render()
msg: (s) ->
@msgs.push(s)
@msgs = @msgs.slice(Math.max(@msgs.length - 5, 0))
#
# Main
#
ui = new Ui()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment